一、 什么是函數(shù)棧幀?
函數(shù)棧幀是用于在計(jì)算機(jī)程序中實(shí)現(xiàn)函數(shù)調(diào)用的一種數(shù)據(jù)結(jié)構(gòu)。在函數(shù)調(diào)用過程中,每個(gè)函數(shù)都需要在內(nèi)存中創(chuàng)建一個(gè)棧幀,用于存儲(chǔ)局部變量、返回地址和參數(shù)等。
-
具體來說,函數(shù)棧幀通常包含以下部分:
-
局部變量表:存儲(chǔ)函數(shù)的局部變量,包括基本數(shù)據(jù)類型(如整數(shù)、浮點(diǎn)數(shù)等)和對(duì)象引用(如指針)。
-
返回地址:存儲(chǔ)函數(shù)的返回地址,即函數(shù)執(zhí)行完畢后需要跳轉(zhuǎn)到的地址。
-
參數(shù)表:存儲(chǔ)函數(shù)的輸入?yún)?shù),通常按照傳遞的順序排列。
-
操作數(shù)棧:用于存儲(chǔ)函數(shù)的臨時(shí)數(shù)據(jù)和中間結(jié)果,通常使用棧結(jié)構(gòu)進(jìn)行操作。
-
當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),會(huì)在內(nèi)存中創(chuàng)建一個(gè)新的棧幀,并將其壓入調(diào)用該函數(shù)的棧中。當(dāng)函數(shù)執(zhí)行完畢后,該棧幀會(huì)被彈出棧并銷毀。因此,函數(shù)棧幀在函數(shù)調(diào)用過程中起到了存儲(chǔ)和傳遞數(shù)據(jù)的作用。
函數(shù)棧幀的實(shí)現(xiàn)方式取決于具體的編程語言和編譯器。在一些高級(jí)編程語言中,編譯器通常會(huì)為每個(gè)函數(shù)自動(dòng)創(chuàng)建和銷毀棧幀,而無需程序員手動(dòng)管理。而在低級(jí)編程語言或手動(dòng)控制內(nèi)存分配的情況下,程序員需要手動(dòng)創(chuàng)建和銷毀棧幀。
二、 理解函數(shù)棧幀能解決什么問題呢?
理解函數(shù)棧幀有什么用呢?
只要理解了函數(shù)棧幀的創(chuàng)建和銷毀,以下問題就能夠很好的額理解了:
- 局部變量是如何創(chuàng)建的?
- 為什么局部變量不初始化內(nèi)容是隨機(jī)的?
- 函數(shù)調(diào)用時(shí)參數(shù)時(shí)如何傳遞的?傳參的順序是怎樣的?
- 函數(shù)的形參和實(shí)參分別是怎樣實(shí)例化的?
- 函數(shù)調(diào)用是怎么做的?函數(shù)的返回值是如何帶會(huì)的?
讓我們一起走進(jìn)函數(shù)棧幀的創(chuàng)建和銷毀的過程中。
三、 函數(shù)棧幀的創(chuàng)建和銷毀解析
3.1、什么是棧?
棧(stack)是現(xiàn)代計(jì)算機(jī)程序里最為重要的概念之一,幾乎每一個(gè)程序都使用了棧,沒有棧就沒有函數(shù),沒有局部變量,也就沒有我們?nèi)缃窨吹降乃械挠?jì)算機(jī)語言。
- 在經(jīng)典的計(jì)算機(jī)科學(xué)中,棧被定義為一種特殊的容器,用戶可以將數(shù)據(jù)壓入棧(Push):將數(shù)據(jù)項(xiàng)添加到棧的頂部。這相當(dāng)于把數(shù)據(jù)放到棧的最上面。出棧(Pop):從棧的頂部移除數(shù)據(jù)項(xiàng)。這相當(dāng)于移除棧頂?shù)臄?shù)據(jù)項(xiàng)。但是棧這個(gè)容器必須遵守一條規(guī)則:先入棧的數(shù)據(jù)后出棧(First In Last Out, FIFO)。就像一個(gè)桶,先放的東西最后才能拿出
- 在計(jì)算機(jī)系統(tǒng)中,棧則是一個(gè)具有以上屬性的動(dòng)態(tài)內(nèi)存區(qū)域。程序可以將數(shù)據(jù)壓入棧中,也可以將數(shù)據(jù)從棧頂彈出。壓棧操作使得棧增大,而彈出操作使得棧減小。
在經(jīng)典的操作系統(tǒng)中,??偸窍蛳略鲩L(zhǎng)(由高地址向低地址)的
在我們常見的i386或者x86-64下,棧頂由成為 esp 的寄存器進(jìn)行定位的
3.2、認(rèn)識(shí)相關(guān)寄存器和匯編指令
3.2.1 相關(guān)寄存器
- 【eax】:通用寄存器,保留臨時(shí)數(shù)據(jù),常用于返回值
- 【ebx】 :通用寄存器,保留臨時(shí)數(shù)據(jù)
- 【ebp】:棧底寄存器
- 【esp】:棧頂寄存器
- 【eip】:指令寄存器,保存當(dāng)前指令的下一條指令的地址
3.2.2 相關(guān)匯編命令
- 【mov】:數(shù)據(jù)轉(zhuǎn)移指令
- 【push】:數(shù)據(jù)入棧,同時(shí)esp棧頂寄存器也要發(fā)生改變
- 【pop】:數(shù)據(jù)彈出至指定位置,同時(shí)esp棧頂寄存器也要發(fā)生改變
- 【add】:加法命令
- 【sub】:減法命令
- 【lea】 :load effective address,加載有效地址
- 【call】:函數(shù)調(diào)用,1. 壓入返回地址 2. 轉(zhuǎn)入目標(biāo)函數(shù)
- 【jump】:通過修改eip,轉(zhuǎn)入目標(biāo)函數(shù),進(jìn)行調(diào)用
- 【ret】:恢復(fù)返回地址,壓入eip,類似pop eip命令
3.3、 解析函數(shù)棧幀的創(chuàng)建和銷毀
- 首先我們達(dá)成一些預(yù)備知識(shí)才能有效的幫助我們理解,函數(shù)棧幀的創(chuàng)建和銷毀。
3.3.1 預(yù)備知識(shí)
- 每一次函數(shù)調(diào)用,都要為本次函數(shù)調(diào)用開辟空間,就是函數(shù)棧幀的空間
- 這塊空間的維護(hù)是使用了2個(gè)寄存器:
esp
和ebp
,【ebp】 記錄的是棧底的地址,esp
記錄的是棧頂的地址
如圖所示:
- 函數(shù)棧幀的創(chuàng)建和銷毀過程,在不同的編譯器上實(shí)現(xiàn)的方法大同小異,本次演示以VS2022為例。
3.3.2 代碼和環(huán)境搭建
- 這段代碼,如果我們?cè)赩S2019編譯器上調(diào)試,調(diào)試進(jìn)入Add函數(shù)后,我們就可以觀察到函數(shù)的調(diào)用堆棧
#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;
}
- 首先我們來做一些環(huán)境的搭建工作
-
首先直接在鍵盤上按下F10【筆記本按下Fn + F10】。
-
以往寫代碼的時(shí)候,我們都知道要寫這么一個(gè)main函數(shù),程序就是從這里開始運(yùn)行的
-
接下去在按下F10后到監(jiān)視窗口打開【調(diào)用堆?!康拇翱?/p>
- 然后就出現(xiàn)了這樣的界面。此時(shí)我們的main函數(shù)就從第13行開始運(yùn)行了
-
一直按F10,當(dāng)調(diào)試箭頭運(yùn)行到第【22行】的時(shí)候,就會(huì)自動(dòng)進(jìn)入到exe_common.inl,此時(shí)我們就可以觀察到底是哪個(gè)函數(shù)調(diào)用了main函數(shù)
-
通過下圖可知是invoke_main這個(gè)函數(shù)調(diào)用的,我們了解到這里就可以了~~
- 然后,關(guān)掉這個(gè)【調(diào)用堆棧】的窗口后,重新調(diào)試起來
- 調(diào)出【反匯編】【內(nèi)存】【監(jiān)視】這三個(gè)窗口
【反匯編】
【內(nèi)存】
【監(jiān)視】
好,現(xiàn)在我們的環(huán)境已經(jīng)全部搭建好了
3.3.3 函數(shù)棧幀的創(chuàng)建
- 接下去,我們正式開始分析函數(shù)棧幀究竟是如何創(chuàng)建的
- 去掉符號(hào)名,方便看內(nèi)存
- 從上圖看到已經(jīng)進(jìn)入到main函數(shù)了
- main函數(shù)是由invoke_main這個(gè)函數(shù)來進(jìn)行調(diào)用的,所以我們先畫出它的函數(shù)棧幀
- 首先看到左邊的兩個(gè)寄存器【esp】和【ebp】,分別用來維護(hù)棧頂和棧頂。
- 對(duì)于棧來說是從【高地址】向【低地址】使用的。
- 好,接下去的話就要執(zhí)行第一條指令。將棧中push一個(gè)ebp,也就是將ebp中的值進(jìn)行一個(gè)壓棧的操作,此時(shí)的ebp中存放的是invoke_main函數(shù)棧幀的ebp
00EE18D0 push ebp
-
隨著push入棧的操作,維護(hù)棧頂?shù)?strong>esp就要往上
-
然后我們看寄存器的變化
- 我們?cè)倮^續(xù)執(zhí)行一下push這句指令,你就會(huì)發(fā)現(xiàn)【esp】中所存放的地址變小了,原來存的是【ebp】中的值,只是這個(gè)存放的形式是倒著存放的,是因?yàn)橛写笮《舜鎯?chǔ)的問題
- 接下來第二條,【mov】,我們?cè)谏厦嬗兄v到過是一個(gè)數(shù)據(jù)轉(zhuǎn)移指令,這條指令的含義就是把esp的值存放到ebp中去
00EE18D1 mov ebp,esp
- 此時(shí)相當(dāng)于產(chǎn)生了main函數(shù)的【ebp】,這個(gè)值就是invoke_main函數(shù)棧幀的【esp】,從這里開始就要開始維護(hù)main函數(shù)的函數(shù)棧幀了
- 通過VS再來看一下,【ebp】中就會(huì)存放【esp】的地址了
第三條指令
- 接下來第三條,sub是一條減法命令,那意思就是讓esp中的地址減去一個(gè)16進(jìn)制數(shù)字【0xe4】,產(chǎn)生新的esp,此時(shí)的esp是main函數(shù)棧幀的esp
00EE18D3 sub esp,0E4h
-
此時(shí)結(jié)合上一條指令的ebp和當(dāng)前的esp,ebp和esp之間維護(hù)了一個(gè)塊棧空間,這塊??臻g就是為main函數(shù)開辟的,就是main函數(shù)的棧幀空間,這一段空間中將存儲(chǔ)main函數(shù)中的局部變量,臨時(shí)數(shù)據(jù)以及調(diào)試信息等
-
通過圖,此時(shí)你也可以認(rèn)為【esp】指向了低地址的一塊空間
-
來看一下寄存器中存放的內(nèi)存變化
第四、五、六條指令
00EE18D9 push ebx //將寄存器ebx的值壓棧,esp-4
00EE18DA push esi //將寄存器esi的值壓棧,esp-4
00EE18DB push edi //將寄存器edi的值壓棧,esp-4
- 上面3條指令保存了3個(gè)寄存器的值在棧區(qū),這3個(gè)寄存器的在函數(shù)隨后執(zhí)行中可能會(huì)被修改,所以先保存寄存器原來的值,以便在退出函數(shù)時(shí)恢復(fù)
- 那隨著寄存器的入棧,維護(hù)棧頂?shù)募拇嫫饕矊l(fā)生變化
- 此時(shí)esp也隨著壓棧而變化
- 到VS里來看一下三次的變化:
第七、八、九、十條指令
- 下面的代碼是在初始化main函數(shù)的棧幀空間,【非常重要】
00EE18DC lea edi,[ebp-24h]
00EE18DF mov ecx,9
00EE18E4 mov eax,0CCCCCCCCh
00EE18E9 rep stos dword ptr es:[edi]
上面的這段代碼最后4句,等價(jià)于下面的偽代碼:
edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{
*(int*)edi = eax;
}
- 首先要來看的就是【lea】就是我們?cè)谏厦嬷v到過的【load effective address】加載有效地址的意思,那也就是從【ebp】這個(gè)維護(hù)棧頂?shù)募拇嫫鳒p去24h的位置,加載到寄存器【edi】里面去
-
然后再將9放到【ecx】中去;以及將【0CCCCCCCCh】這塊地址存到【eax】中去;
-
從【edi】所存放的這塊地址的開始,每次初始化4個(gè)字節(jié)的數(shù)據(jù),dword值就是4個(gè)字節(jié)的大小
-
這4句話的操作就是從edi開始,每次初始化4個(gè)字節(jié)的數(shù)據(jù),總共初始化ecx次,初始化的內(nèi)容為【0xCCCCCCCC】,總共初始化到ebp的地址結(jié)束
- 到這里,main函數(shù)才剛剛被初始化完成
- 那么里面的cccccccc是初始化的什么內(nèi)容呢?–>我們來看一下
char arr[20];
printf("%s",arr);
- 可以看到上面的程序輸出“燙燙燙燙燙燙燙燙燙燙”這一串,是因?yàn)閙ain函數(shù)調(diào)用時(shí),在棧區(qū)開辟的空間的其中每一個(gè)字節(jié)都被初始化為0xCC,上圖中arr數(shù)組是一個(gè)未初始化的數(shù)組,恰好在這塊空間上創(chuàng)建的,0xCCCC(兩個(gè)連續(xù)排列的0xCC)的漢字編碼就是“燙”,所以0xCCCC被當(dāng)作文本就是“燙”,燙燙燙就這么來的
第十一、十二、十三條指令
- 我們開始初始化三個(gè)變量,每條指令對(duì)應(yīng)上一條代碼
int a = 10;
00EE18F5 mov dword ptr [ebp-8],0Ah
int b = 20;
00EE18FC mov dword ptr [ebp-14h],14h
int c = 0;
00EE1903 mov dword ptr [ebp-20h],0
-
其中【mov】是數(shù)據(jù)轉(zhuǎn)移指令,也就是是將10這個(gè)值【ebp - 8】這塊地址上
-
為什么說0Ah就是10呢?因?yàn)?Ah是10的十六進(jìn)制表示形式,在十六進(jìn)制中A值得就是10
-
對(duì)于14h的話就是16 * 1 + 4 = 20,那就是將20這個(gè)值放到【ebp - 14】這塊地址上去
-
最后一句就是將0這個(gè)值放到【ebp - 20】這塊地址上去
-
對(duì)于為什么-8,-14,-20呢,這是取決于編譯器本身的,我是用的是VS2022,可能你到其他編譯器上就不一樣了
-
這就可以得出一個(gè)結(jié)論:我們所定義的變量在棧內(nèi)存中并不是呈現(xiàn)一個(gè)連續(xù)存放的,可能是分散的,
-
接下去繼續(xù)來看這三次的存放值的變化~~
- 我們?cè)賮砜磮D,也將這些畫出來。
第十四、十五、十六、十七條指令
- 此時(shí)main函數(shù)中的變量創(chuàng)建好了,那就要調(diào)用Add函數(shù)了
00EE190A mov eax,dword ptr [ebp-14h]
00EE190D push eax
00EE190E mov ecx,dword ptr [ebp-8]
00EE1911 push ecx
- 來看第一條,將【ebp-14h】這塊地址的內(nèi)容放到寄存器【eax】中去,那這個(gè)時(shí)候你就會(huì)想到這個(gè)【ebp-14】是剛才放數(shù)值20,然后壓棧。
- 第三條就是將【ebp-8】中的內(nèi)容放到寄存器【ecx】中去,它【ebp-8】的地方存放的就是我們剛才放10的地方,然后壓棧。
- 這樣就可以看出,這兩個(gè)變量相當(dāng)于實(shí)參的一份臨時(shí)拷貝,這里就回到我們前面學(xué)的函數(shù)的形參就是實(shí)參的一份臨時(shí)拷貝
再來到VS中看看
第十八條指令
00EE1912 call 00EE10B9
- 對(duì)于這條【call】指令而言,比較特殊,它有兩個(gè)作用
①壓入返回地址
②轉(zhuǎn)入目標(biāo)函數(shù)
- 這里就是要壓的是 call指令的下一條地址
00EE1917 //這條就是要壓入的地址
- 然后我們來在vs中看一下,當(dāng)運(yùn)行到圖中的那條語句的時(shí)候就要按F11,不能按F10,和調(diào)試一個(gè)道理
- 把這塊地址壓入棧中
第十九、二十、二一條指令
- 到19條指令開始,就進(jìn)入Add函數(shù)了,這里的函數(shù)前面和在main函數(shù)中的前面也是非常的相似
- 所以這個(gè)就是在開辟棧幀
00EE1790 push ebp
00EE1791 mov ebp,esp
00EE1793 sub esp,0CCh
00EE1799 push ebx
00EE179A push esi
00EE179B push edi
- 首先來看第一條指令。也就是將之前的【ebp】棧底寄存器的值壓入到棧頂中
00EE1790 push ebp
- 對(duì)于此處的【ebp】,自從它在維護(hù)main函數(shù)的棧底后就沒有再動(dòng)過來,所以這里push上來的就是main函數(shù)的【ebp】
00EE1791 mov ebp,esp
- 接著再來看第二條,也就是將main函數(shù)的【esp】重新賦給【ebp】,這里要注意了,不要搞混,此時(shí)的【ebp】應(yīng)該算是在維護(hù)Add函數(shù)的棧底了
- 于是,棧就變成了這樣:
00EE1793 sub esp,0CCh
- 接著第三條,【sub】命令使得【esp】存放的地址塊減去一個(gè)CC的大小,繼續(xù)結(jié)合上面那條指令,此時(shí)Add函數(shù)的棧頂和棧底都被找到了
- 此時(shí)就相當(dāng)于是在做一個(gè)迭代的操作
第二二、二三、二四條指令
00EE1799 push ebx
00EE179A push esi
00EE179B push edi
- 接下去還是一樣的三條壓棧操作
- 來到VS中觀看【esp】的變化
- 接著將這三個(gè)寄存器壓入棧
第二五、二六、二七、二八條指令
- 對(duì)于這四條指令和上面main函數(shù)的創(chuàng)建過程類似,便不做不過分析
00EE179C lea edi,[ebp-0Ch]
00EE179F mov ecx,3
00EE17A4 mov eax,0CCCCCCCCh
00EE17A9 rep stos dword ptr es:[edi]
- 繼續(xù)到VS中觀看的變化
第二十九條指令
- 接下去我們進(jìn)入第二十九條指令,也就是對(duì)Add函數(shù)中存放計(jì)算總和的變量z進(jìn)行初始化操作。
- 【mov】做數(shù)據(jù)轉(zhuǎn)移,將0放到【ebp-8】這塊地址上去
int z = 0;
00EE17B5 mov dword ptr [ebp-8],0
- 然后我們?cè)贏dd的棧幀中初始化這個(gè)變量z
第三十、三十一、三十二條指令
- 接下去的三條指令就是對(duì)兩個(gè)形參的值進(jìn)行一個(gè)相加
z = x + y;
00EE17BC mov eax,dword ptr [ebp+8]
00EE17BF add eax,dword ptr [ebp+0Ch]
00EE17C2 mov dword ptr [ebp-8],eax
- 那么上面不是只初始化了一個(gè)變量z嗎,變量x和變量y在哪里呢?
- 我們之前有做過了一步操作,也就是將這兩個(gè)實(shí)參的拷貝進(jìn)行了一個(gè)壓棧操作,那時(shí)就說了對(duì)于這個(gè)就是形參
00EE190A mov eax,dword ptr [ebp-14h]
00EE190D push eax
00EE190E mov ecx,dword ptr [ebp-8]
00EE1911 push ecx
- 此時(shí)我們就要通過這三句指令去找回這兩個(gè)形參的值,關(guān)鍵的就是【ebp+8】和【ebp+0Ch】。因?yàn)槲覀冊(cè)谌霔5臅r(shí)候【ebp】寄存器存放的地址都是逐漸變小的,因?yàn)?棧是從高地址往低地址生長(zhǎng)的,所以我們要去找回之前壓入的內(nèi)容,就要把地址加回去
- 如下圖所示
- 找到這兩個(gè)值之后,首先將【10】放到【eax】寄存器中去,然后再將【20】在加到寄存器【eax】原有的值上去,此時(shí)【eax】中存放的便是【30】
- 注意看寄存器【eax】的變化
- 這里還可以直接到指令這里來看。直接將鼠標(biāo)放到【z】上面就可以看到了
- 然后再將計(jì)算出來存放在【eax】中的值再放回【ebp-8】這塊地址上去
00EE17C2 mov dword ptr [ebp-8],eax
- 首先到VS中來看看變化
- 然后修改一下之前Add函數(shù)棧幀中存放z的內(nèi)容
第三十三條指令
-
z
計(jì)算出來了,此時(shí)就要執(zhí)行【return z】這句代碼,將z返回給main函數(shù),但是函數(shù)棧幀中可不是這么做的
return z;
00EE17C5 mov eax,dword ptr [ebp-8]
- 看上面的指令可以看到,是將【ebp-8】中的內(nèi)容轉(zhuǎn)存到寄存器【eax】中去
- 從【eax】~【ebx】這些寄存器都可以用來存放臨時(shí)數(shù)據(jù),并不是說上一次用過了就不能再用了,這其實(shí)和我們?cè)诙x一個(gè)變量后進(jìn)行反復(fù)使用是一個(gè)道理。
- 然后在Add函數(shù)調(diào)用結(jié)束后,它所對(duì)應(yīng)的函數(shù)棧幀就會(huì)被銷毀,此時(shí)被創(chuàng)建出來的臨時(shí)變量【z】就不復(fù)存在了,因?yàn)椤緕】也是存放在Add的函數(shù)棧幀中的,所以這一步的操作其實(shí)就是將我們?cè)贏dd函數(shù)中計(jì)算出來的值給保存起來,因?yàn)榧拇嫫鞫猿绦驔]有結(jié)束的話它是不會(huì)被銷毀的,我們后面還可以到這個(gè)寄存器中去取數(shù)據(jù)
3.3.4 函數(shù)棧幀的銷毀
接下去要進(jìn)行的就是函數(shù)棧幀的銷毀操作
第三十四、三十五、三十六條指令
- 接下來就是三條pop的指令,也就是在棧頂彈出對(duì)應(yīng)的值,然后放到對(duì)應(yīng)的寄存器中去
00EE17C8 pop edi //在棧頂彈出一個(gè)值,存放到edi中,esp+4
00EE17C9 pop esi //在棧頂彈出一個(gè)值,存放到esi中,esp+4
00EE17CA pop ebx //在棧頂彈出一個(gè)值,存放到ebx中,esp+4
- 我們先到VS中來看看
- 通過圖示來看一下
第三十七條指令
- 當(dāng)給Add函數(shù)預(yù)開辟函數(shù)棧幀的時(shí)候,最后一步是把【esp】中存放的內(nèi)容給到【ebp】,也就是相當(dāng)于就是讓【ebp】指向和【esp】的同一塊空間
- 下面這句指令就是將【ebp】中存放的內(nèi)容給到【esp】,那其實(shí)就是讓【esp】指向和【ebp】的同一塊空間
00EE17D8 mov esp,ebp
- 通過圖示來看一下
- 到VS中來看一下
第三十八條指令
00EE17DA pop ebp
- 這句指令很重要,因?yàn)榇藭r(shí)Add函數(shù)的函數(shù)棧幀已經(jīng)被銷毀了,此時(shí)我們要回到main函數(shù)的函數(shù)棧幀,那么兩個(gè)維護(hù)棧頂和棧底的寄存器就要發(fā)生變化,此時(shí)我們要pop的【ebp】是之前壓棧進(jìn)來的main函數(shù)的ebp
- pop的作用:數(shù)據(jù)彈出至指定位置,同時(shí)esp棧頂寄存器也要發(fā)生改變
- pop了之后【esp】也要發(fā)生一個(gè)變化
- 到VS中再來看一下變化。此時(shí)不要混淆了,棧是從高地址往低地址增長(zhǎng)的,所以棧底的地址來的大一些
第三十九條指令
- 這里只有一個(gè)【ret】,這個(gè)指令會(huì)從棧頂彈出一個(gè)值,那這個(gè)時(shí)候從上圖其實(shí)可以看到此時(shí)的【esp】棧頂寄存器指向的這塊地址,這塊地址是call指令的下一條指令地址,就是我們?cè)谶M(jìn)入Add函數(shù)前提前壓入的地址
00EE17DB ret
- 此時(shí)就會(huì)直接跳轉(zhuǎn)到call指令下一條指令的地址處,繼續(xù)往下執(zhí)行
- 再來看看【esp】的變化
第四十條指令
- 有的同學(xué)看到的就是一個(gè)【esp】的變化,【add】是加法命令,也就是將【esp】的位置加上一個(gè)8,一塊內(nèi)存空間是4,加8的話那此時(shí)【esp】是不是就來到了【edi】的位置
- 這其實(shí)就是在【銷毀Add函數(shù)的函數(shù)形參x,y】,這下你應(yīng)該明白函數(shù)形參是在什么時(shí)候銷毀的了吧,沒錯(cuò),就是從Add函數(shù)回到main函數(shù)之后
0046185D 83 C4 08 add esp,8
- 我們來看看示意圖:
- 一樣,VS也來看看【esp】的變化
第四十一條指令
00EE191A mov dword ptr [ebp-20h],eax
-
將eax中值,存檔到ebp-0x20的地址處,其實(shí)就是存儲(chǔ)到main函數(shù)中ret變量中,而此時(shí)eax中就是Add函數(shù)中計(jì)算的x和y的和,可以看出來,本次函數(shù)的返回值是由eax寄存器帶回來的。程序是在函數(shù)調(diào)用返回之后,在eax中去讀取返回值的。
-
先前在Add函數(shù)中計(jì)算出來的30,首先放到【eax】寄存器中保存起來,現(xiàn)在過來好幾條指令后,它還保存在里面,我們只需要使用【mov】將數(shù)據(jù)做一個(gè)轉(zhuǎn)移即可
-
到VS里來看看變化
- 最后main函數(shù)棧幀的銷毀也同理,這里就不再介紹了
- 以下是這個(gè)棧的全局瀏覽圖
拓展了解:
其實(shí)返回對(duì)象時(shí)內(nèi)置類型時(shí),一般都是通過寄存器來帶回返回值的,返回對(duì)象如果時(shí)較大的對(duì)象時(shí),一般會(huì)在主調(diào)函數(shù)的棧幀中開辟一塊空間,然后把這塊空間的地址,隱式傳遞給被調(diào)函數(shù),在被調(diào)函數(shù)中通過地址找到主調(diào)函數(shù)中預(yù)留的空間,將返回值直接保存到主調(diào)函數(shù)的。具體可以參考《程序員的自我修養(yǎng)》一書的第10章。
到這里我們給大家完整的演示了main函數(shù)棧幀的創(chuàng)建,Add函數(shù)站真的額創(chuàng)建和銷毀的過程,相信大家已經(jīng)能夠基本理解函數(shù)的調(diào)用過程,函數(shù)傳參的方式,也能夠回答最開始的問題了
四、總結(jié)與開局疑難解答
① 局部變量是如何創(chuàng)建的?
- 首先為函數(shù)分配好棧幀空間,將這塊棧幀空間初始化好后,然后給局部在棧幀里分配空間
② 為什么局部變量不初始化內(nèi)容是隨機(jī)的?
- 因?yàn)楹瘮?shù)棧幀中的空間是預(yù)先初始化好的【0xCCCCCCCCh】,若是不為變量初始化內(nèi)容,那使用的就是初始化好后的內(nèi)容,以字符的形式打印出來便是燙燙燙燙燙燙
③ 函數(shù)調(diào)用時(shí)參數(shù)時(shí)如何傳遞的?傳參的順序是怎樣的?
- 當(dāng)還沒有進(jìn)入函數(shù)的時(shí)候,就已經(jīng)將函數(shù)實(shí)參做了一份臨時(shí)拷貝,并從右向左壓入棧中【FILO】,當(dāng)真正進(jìn)入到函數(shù)棧幀中時(shí),通過指針的偏移量,就可以順著找回來,找到這份臨時(shí)拷貝的形參
④ 函數(shù)的形參和實(shí)參分別是怎樣實(shí)例化的?
- 形參確實(shí)是我在壓棧的時(shí)候開辟的一塊空間,它和實(shí)參只是值相同,但是空間是獨(dú)立的,所以形參是實(shí)參的一份臨時(shí)拷貝,改變形參的值不會(huì)影響到實(shí)參
⑤ 函數(shù)調(diào)用是怎么做的?返回值是如何帶會(huì)的?文章來源:http://www.zghlxwxcb.cn/news/detail-818835.html
- 當(dāng)執(zhí)行到【call】指令的時(shí)候,把call指令的下一條指令地址壓入棧中,相當(dāng)于記住了這個(gè)地址。接著進(jìn)入到函數(shù)中,當(dāng)函數(shù)執(zhí)行結(jié)束的時(shí)候,回到主函數(shù)中,再執(zhí)行【ret】指令就可以回到call指令的下一條指令地址
- 返回值是通過寄存器帶回來的、將函數(shù)中計(jì)算出來的返回值存放到寄存器中,因?yàn)榧拇嫫鞑粫?huì)隨著函數(shù)的調(diào)用結(jié)束而被銷毀,最后再將寄存器中存放的數(shù)據(jù)轉(zhuǎn)存回對(duì)應(yīng)的內(nèi)存塊中即可
好了,函數(shù)的棧幀的創(chuàng)建與銷毀所有內(nèi)容就到這里就結(jié)束了
如果有什么問題可以私信我或者評(píng)論里交流~~
感謝大家的收看,希望我的文章可以幫助到正在閱讀的你??????文章來源地址http://www.zghlxwxcb.cn/news/detail-818835.html
到了這里,關(guān)于C語言之反匯編查看函數(shù)棧幀的創(chuàng)建與銷毀的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!