首頁 > 軟體

C語言函數棧幀的建立和銷燬詳解

2022-02-15 19:00:04

寫在前面

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

而棧是從高地址向低地址延伸的。每個函數的每次呼叫,都有它自己獨立的一個棧幀,這個棧幀中維持著所需要的各種資訊。暫存器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("%dn", 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("%dn", 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函數棧幀銷燬的過程的思想都是一樣的,這裡就不做多贅述了。

總結

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

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!       


IT145.com E-mail:sddin#qq.com