- 魔王的介紹:??????一名雙非本科大一小白。
- 魔王的目標:??努力趕上周圍卷王的腳步。
- 魔王的主頁:??????大魔王.??????
?????大魔王與你分享:莫泊桑說過,生活可能不像你想象的那么好,但是也不會像你想象的那么糟。人的脆弱和堅強都超乎了自己的想象。有時候可能脆弱的一句話就淚流滿面,有時候你發(fā)現(xiàn)自己咬著牙已經(jīng)走過了很長的路。
一、前言
我們在編譯代碼時會有很多不清楚的地方,例如我們創(chuàng)建變量時我們只知道會開辟空間,卻不知道要在哪開辟,怎么開辟,在我們調用函數(shù)時,我們也知道要在棧區(qū)開辟空間,但是依然不知道怎么開辟,參數(shù)如何拷貝,為什么值傳遞不會改變原數(shù)值。本篇博客帶你理解棧區(qū)(局部變量和函數(shù)調用開辟空間的地方)是怎樣工作的。
二、問題舉例
- 局部變量是怎么創(chuàng)建的?
- 為什么局部變量的值是隨機值?
- 函數(shù)時怎么傳參的?傳參的順序是怎樣的?
- 形參和實參是什么關系?
- 函數(shù)調用時怎么做的?
- 函數(shù)調用時結束后怎樣返回的?
- 通過下面的講解你將全部明白這些問題
三、介紹
- 首先說明一些比較陌生的東西,以便后續(xù)的理解。
1.寄存器
- 寄存器:中央處理器內的組成部分。寄存器是有限存貯量的高速存貯部件,它們可以來暫存指令、數(shù)據(jù)和地址。寄存器是獨立于內存的,不會因為棧區(qū)函數(shù)空間的銷毀而銷毀。
- 寄存器舉例:
eax、ebx、ecx、edx、ebp、esp等
其中三個寄存器比較重要:
1.eax:接收函數(shù)返回值,使得函數(shù)的返回值不會因為棧區(qū)的銷毀而消失。
2.ebp:棧底寄存器(高地址),和esp共同維護棧區(qū)新開辟的空間。
3.esp:棧頂寄存器(低地址),和ebp共同維護棧區(qū)新開辟的空間。
2.匯編指令
push:壓棧,放入棧頂一個元素(寄存器的內容,并不是寄存器)。
pop:出棧,把棧頂數(shù)據(jù)彈出,并把彈出的元素賦值到寄存器中。
move:給一個寄存器賦值。
sub:減法指令。
add:加法指令。
lea:即load effective address,加載有效地址。
call:調用函數(shù),并且把該函數(shù)之后的地址進行壓棧(目的是為了在函數(shù)銷毀后可以回到函數(shù)之后的下一步)。
dword:即double word,可以理解為兩倍的字的大小,我們知道一個漢字兩個字節(jié),那么兩倍的就是四個字節(jié)。
rep stors:重復拷貝寄存器中的數(shù)據(jù)。
注意:esp永遠指向棧頂,每次壓棧后esp就會自動減去4個字節(jié)(因為棧區(qū)的使用是高地址向低地址使用),每次出棧后esp就會加上4個字節(jié)。
四、講解
- 以此代碼為標準進行詳細說明:
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
1.main函數(shù)并不是最終函數(shù)
我們一直認為main函數(shù)結束,程序就會結束,main函數(shù)是程序的最后一個函數(shù),你是否想過這樣一個問題:為什么main函數(shù)會有返回值呢?那自然是因為main函數(shù)并不是最終函數(shù),main函數(shù)也是被其他函數(shù)所調用的。我們轉到調用堆棧就會觀察到,如圖:
-
那么到底是什么函數(shù)調用的main函數(shù)呢?
是__tmainCRTStartup函數(shù)調用的main函數(shù)。__tmainCRTStartup函數(shù)又是被mainCRTStartup函數(shù)調用的。
- 也就是main函數(shù)后其實還有兩個函數(shù),如圖:
所畫的圖只表示調用順序和棧幀使用順序(先使用高地址在使用低地址)。
2.函數(shù)棧幀的創(chuàng)建
函數(shù)棧幀怎樣創(chuàng)建,我們可以通過反匯編觀察得到。要進入反匯編,首先要進入調試,然后按照圖片操作即可進入,如圖:
- main函數(shù)的棧幀創(chuàng)建
函數(shù)棧幀創(chuàng)建的邏輯基本一樣,所以詳細說一個怎么創(chuàng)建,其他就基本也是這樣,這里以主函數(shù)舉例。通過圖一點點進行分析,如圖:
所畫的15個指令是在Add函數(shù)之前的匯編指令,前10個指令是創(chuàng)建主函數(shù)的棧幀空間并賦值為CCCCCCCC,這就是當我們不給一個變量賦值時,會打印出燙燙燙燙的原因。第11、12這兩個指令是新增的,在VS2013上并沒有,所以無視就行。那么從第13個指令開始,就該使用所開辟好的空間了。詳細如下:
- 用ebp的值進行壓棧。然后esp-4(沒個元素都是4個字節(jié),棧區(qū)的使用是從高地址向低地址使用的)。
- 把esp的值賦給ebp。
- 讓esp減去0E4h這個十六進制數(shù)字。(目的是給main函數(shù)開辟空間,esp是維護棧頂?shù)模砸宔sp移動到新開辟的棧頂位置)
- 用ebx的值進行壓棧。
- 用esi的值進行壓棧。
- 用edi的值進行壓棧。
- load effective address,加載有效地址。放入edi中。
- 把9賦值到ecx中。
- 把CCCCCCCC賦值到eax中。
- 重復拷貝:從edi(加載的那個有效地址)開始,拷貝ecx次(9),每次拷貝eax(CCCCCCCC),每次拷貝的大小為dword(4字節(jié))。每拷貝一次,edi(存放有效地址的這個寄存器都加4),ecx(存放拷貝次數(shù)的寄存器都減1),直到ecx減到0,停止拷貝,執(zhí)行下一個匯編指令。
前十個是為main函數(shù)開辟空間并賦值為CCCCCCCC的匯編碼。接下來才開始讓我們寫的程序分配空間。- 在ebp-8位置處賦值0Ah(也就是10).
- 在ebp-14h(ebp-20)位置處賦值14h(也就是20)
- 在ebp-20h(ebp-32)位置處賦值0
-
如圖:
第一個圖其實__tmainCRTStartup函數(shù)是被mainCRTStartup函數(shù)調用的,不過這里就不畫了,我們主要是理解main函數(shù)及main函數(shù)內的函數(shù)調用如何在棧區(qū)開辟空間就好了,main函數(shù)之前的那兩個函數(shù)知道有就行了。
這個圖如果在VS2013是正確的,開辟的空間全部先初始化,但是在VS2022中,其實開辟的主函數(shù)的空間并沒有全部賦值,如圖:
-
最后一次拷貝后的內存展示:
3.函數(shù)調用
開辟空間前執(zhí)行的操作:
- 函數(shù)調用的時候,我們都知道會開辟空間,但是他是怎么開辟的呢?
- 我們知道形參是實參的臨時拷貝,那么它是怎樣拷貝的呢?
- 我們知道函數(shù)結束后該函數(shù)所處的棧幀空間就要被銷毀回收,那么如果該函數(shù)有返回值,明明已經(jīng)被銷毀了,它是怎樣再返回去的呢?
接著往下看,你就會全部知道。
- 在調用函數(shù)這個操作中,剛開始進行的是讓實參(實參地址中的內容那就是實參的數(shù)值)賦給eax,ecx,從右向左壓棧,也就是先壓棧b的值,再壓棧a的值,這就是反匯編的前四行。其實這四行目的就是拷貝實參。第五行call指令是調用Add這個函數(shù)并且把調用Add函數(shù)后的下一個操作指令進行壓棧,目的是為了在調用函數(shù)結束后,可以回到Add函數(shù)之后的下一步指令。
![]()
開辟空間及銷毀前的操作:
- 匯編指令如圖:
紅色框不含黃色部分:
還是和之前main函數(shù)開辟一樣,先是壓棧ebp,然后讓ebp移動到esp位置,然后esp減去一個數(shù)值(即開辟的空間大?。?,然后進行(部分)(不同編譯器可能不一樣,有可能使部分賦值,有可能是整個空間賦值)賦值,賦值為CCCCCCCC,然后就是為Add函數(shù)中的變量分配空間(如下圖)。紅色框中的黃色部分:
這個部分是關于函數(shù)返回值的匯編指令,兩個圖結合看,ebp+8的值賦給eax(是寄存器,獨立于內存,不會因為棧幀空間銷毀而銷毀),然后讓eax的值加上ebp+0Ch這個地址的值,這個地址轉換為十進制也就是ebp+12,再讓eax的值賦給該函數(shù)中的變量z,所以函數(shù)的返回值也就暫時儲存在了寄存器eax中(因此當z被銷毀后,eax里還存著函數(shù)的返回值,等待之后的指令),那么你是否發(fā)現(xiàn)了,ebp+8其實就是在開辟函數(shù)空間前拷貝的10,ebp+12就是開辟空間前拷貝的20,所以eax就變成了30,也就是說函數(shù)使用的實參其實在自己內部空間根本就沒有,他們只是通過地址訪問了在開辟函數(shù)空間前壓棧的那些形參,僅僅是使用而已,因此值傳遞不改變原數(shù)值。
銷毀操作:
- 按照老編譯器(老編譯器中沒有劃掉的那三行)來說:
- 第1步:把z的值賦給eax,因為z要銷毀了,但是返回值要被保留,所以要借助寄存器保留下來。
- 2~4步:pop指令,也就是出棧,最上面的三個元素,并把元素的值分別賦給edi、esi、ebx,其實這三個pop指令中的賦值操作沒用,因為彈出的是edi,edi里邊本來存的就是edi的值。其他兩個也是這樣。但是對于倒數(shù)第二行的pop指令中的賦值操作就用處很大了,接著看,等會會說到(第6步)。
- 第5步:把ebp的值賦給esp,那就是說esp離開了正在維護的函數(shù)的棧幀空間,那么這一部分就讓函數(shù)棧幀空間被銷毀了。
- 第6步:出棧,并且把出棧的值(是ebp指向main函數(shù)底部時壓棧上去的那個地址,也就是說出棧的這個元素其實存放的是main函數(shù)的棧底指針)賦給寄存器ebp。那么ebp就回到原來的位置了。(main函數(shù)的棧底指針處)
- 第七步:ret,即返回,這一步也很重要,它的意義是彈出并回到彈出的這個地址所指向的指令,因為第六步我們只是讓ebp回到了main的棧底位置,此時esp和ebp位置都就緒了,但是指令卻還沒有返回,而這一步其實就是彈出了調用函數(shù)之后的那個指令的地址(之前壓棧上去的那個),并且回到彈出這個指令的位置,也就是調用函數(shù)的下一個指令的位置,那么一切便結束了,后面就是調用函數(shù)之后的操作了。
4.調用函數(shù)后的匯編指令:
-
最后兩行指令:
-
目前的棧幀圖:
第一個指令esp加8,那么就是說esp跳過了臨時拷貝的變量,也就是銷毀了臨時拷貝形參的棧幀空間。
第二個指令就是把eax寄存器中存的函數(shù)的返回值賦到main函數(shù)中接收Add函數(shù)返回值的地址中,也就是變量c的位置,往上翻看看前面你就會發(fā)現(xiàn)變量c的地址就是eax要賦的地址處。
- 那么相信你看完函數(shù)棧幀的創(chuàng)建于銷毀后對棧幀空間的運行原理有了更深層次的理解。
五、總結
文章來源:http://www.zghlxwxcb.cn/news/detail-618163.html
?請點擊下面進入主頁關注大魔王
如果感覺對你有用的話,就點我進入主頁關注我吧!文章來源地址http://www.zghlxwxcb.cn/news/detail-618163.html
到了這里,關于函數(shù)棧幀的創(chuàng)建與銷毀的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!