C語言函數棧幀的創建和銷毀詳解

寫在前面

我們知道,每一次函數調用都需要在棧區上為其開辟一塊空間,這塊空間就叫做這個函數的棧幀。

而棧是從高地址向低地址延伸的。每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持著所需要的各種信息。寄存器ebp指向當前的棧幀的底部(高地址),寄存器esp指向當前的棧幀的頂部(低地址)。

這樣我們就瞭解瞭寄存器ebp和寄存器esp中存放的是地址,這兩個地址是用來維護函數棧幀的。比如:調用main函數, 我們為main函數分配棧幀空間, 那麼棧幀維護如下:

在這裡插入圖片描述

下面我們通過一段代碼分析一下,函數棧幀創建和銷毀的過程:(棧幀這部分內容在不同的編譯器上實現存在差異, 但是思想大致都是一致的。本文是在vs2013編譯器下實現的。)

#include <stdio.h>
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main(void)
{
	int a = 10;
	int b = 20;
	int ret = 0;
	ret = Add(a, b);//計算a+b
	printf("%d\n", ret);
	return 0;
}

我們在調試過程打開調用堆棧

在這裡插入圖片描述

可以看出,main函數是在__tmainCRTStartup函數內部被調用的,而__tmainCRTStartup函數又是在mainCRTStartup函數內部調用的。

為瞭能更加清楚的看到棧幀創建和銷毀的過程,我們轉到上面代碼對應的反匯編代碼:

int main(void)
{
009D3F40  push        ebp  //將edp壓入棧幀
009D3F41  mov         ebp,esp  //將esp的值賦給edp
009D3F43  sub         esp,0E4h  //esp-0E4h
009D3F49  push        ebx  
009D3F4A  push        esi  
009D3F4B  push        edi  
009D3F4C  lea         edi,[ebp+FFFFFF1Ch]  
009D3F52  mov         ecx,39h  
009D3F57  mov         eax,0CCCCCCCCh  
009D3F5C  rep stos    dword ptr es:[edi]  
	int a = 10;
009D3F5E  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
009D3F65  mov         dword ptr [ebp-14h],14h  
	int ret = 0;
009D3F6C  mov         dword ptr [ebp-20h],0  
	ret = Add(a, b);//計算a+b
009D3F73  mov         eax,dword ptr [ebp-14h]  
009D3F76  push        eax  
009D3F77  mov         ecx,dword ptr [ebp-8]  
009D3F7A  push        ecx  
009D3F7B  call        009D11F9  
009D3F80  add         esp,8  
009D3F83  mov         dword ptr [ebp-20h],eax  
	printf("%d\n", ret);
009D3F86  mov         esi,esp  
009D3F88  mov         eax,dword ptr [ebp-20h]  
009D3F8B  push        eax  
009D3F8C  push        9D5860h  
009D3F91  call        dword ptr ds:[009D9118h]  
009D3F97  add         esp,8  
009D3F9A  cmp         esi,esp  
009D3F9C  call        009D1140  
	return 0;
009D3FA1  xor         eax,eax  
}
009D3FA3  pop         edi  
009D3FA4  pop         esi  
009D3FA5  pop         ebx  
009D3FA6  add         esp,0E4h  
009D3FAC  cmp         ebp,esp  
009D3FAE  call        009D1140  
009D3FB3  mov         esp,ebp  
009D3FB5  pop         ebp  
009D3FB6  ret  

main函數的調用 main函數棧幀的創建

經過剛才我們的理解,在準備調用main函數的時候,調用main函數的那個函數的棧幀已經開辟好瞭。

在這裡插入圖片描述

然後將ebp壓入棧幀,保存瞭指向棧底的ebp的地址,而此時esp指向新的棧頂位置;接著將esp的值賦給瞭ebp,產生瞭新的ebp;用esp減去一個16進制數0E4H(這裡就是為main函數預開辟空間)。緊接著三個壓棧指令,分別將ebx,esi,edi,壓入棧幀。加載完有效地址以後,將為main函數預開辟空間全部初始化為0xCCCCCCCC。最後創建瞭三個局部變量a,b,ret並進行瞭初始化。

Add函數的調用

函數傳參

在這裡插入圖片描述

將b的值存入寄存器eax中,再將eax壓入棧中;將a的值存入寄存器ecx中,再將將ecx壓入棧中;這裡看出參數是從右向左傳遞的。緊接著執行call指令,這裡就是調用Add函數,同時將call指令的下一條指令的地址壓入棧中,然後執行call指令的時候按F11 , 就進入瞭Add函數內部。

Add函數棧幀的創建

在這裡插入圖片描述

首先將main()函數的ebp壓入棧,保存指向main()函數棧幀底部的ebp的地址,此時esp指向新的棧頂位置;將esp的值賦給ebp,產生新的ebp,即Add()函數棧幀的ebp;給esp減去一個16進制數0E4H,這裡是為Add()函數預開辟空間;緊接著三個壓棧指令,分別將ebx,esi,edi,壓入棧幀。加載完有效地址以後,將為Add函數預開辟空間全部初始化0xCCCCCCCC。在緊接著創建瞭變量z,將形參的a和b相加的結果存儲到z中;最後將結果存儲到eax寄存器中,通過寄存器帶回瞭函數的返回值。

Add函數棧幀的銷毀

在這裡插入圖片描述

edi、esi、ebx依次出棧,esp 會向下移動;然後將ebp的值賦給esp,使esp指向ebp指向的地方;接著ebp 出棧,同時將出棧的內容給ebp,此時ebp又指向瞭main函數棧幀的底部,最後執行ret 指令,表示出棧一次,並跳轉到出棧的內容的地址處,也就是call指令的下一條指令處。

main函數棧幀的銷毀

在這裡插入圖片描述

main函數棧幀的銷毀和Add函數棧幀銷毀的過程的思想都是一樣的,這裡就不做多贅述瞭。

總結

通過上面的例子,我們知道瞭局部變量是如何創建的,知道瞭為什麼創建局部變量不初始化,會導致裡面的內容是隨機值;對函數是如何傳參的,以及傳參順序是如何也有瞭較為深入的瞭解。

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!       

推薦閱讀: