什麼是棧幀
棧幀也叫過程活動記錄,是編譯器用來實作過程/函數調用的一種資料結構。C語言中,每個棧幀對應着一個未運作完的函數。棧幀中儲存了該函數的傳回位址和局部變量。從邏輯上講,棧幀就是一個函數執行的環境:函數參數、函數的局部變量、函數執行完後傳回到哪裡等等。
棧是從高位址向低位址延伸的。每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持着所需要的各種資訊。寄存器ebp指向目前棧幀的底部(高位址),寄存器esp指向目前的棧幀的頂部(低位址)。
注意:ebp指向目前位于系統棧最上邊一個棧幀的底部,而不是系統棧的底部。嚴格說來,“棧幀底部”和“棧底”是不同的概念;esp所指的棧幀頂部和系統棧的頂部是同一個位置。
函數調用過程
示例代碼
#include <stdio.h>
#include <stdlib.h>
int Add(int left, int right){
int sum = 0;
sum = left + right;
return sum;
}
int main(){
int num1 = 1;
int num2 = 2;
int result = 0;
result = Add(num1, num2);
printf("result: %d\n", result);
system("pause");
return 0;
}
_tmainCRTStartup函數的調用
我們對該代碼進行調試,檢視調用堆棧如下:

我們可以發現,main函數是在_tmainCRTStartup函數中調用的。而_tmainCRTStartup是在mainCRTStartup中調用的。
下面看一下_tmainCRTStartup函數的棧幀。
main函數的調用
既然main函數被調用的,那麼這個程式第一步應該會為main函數配置設定棧空間。調試中檢視反彙編。
int main(){
/* 将ebp壓棧處理(友善函數傳回之後的現場恢複) */
00C51420 push ebp
/* 這裡使esp的值賦給ebp,産生新的ebp */
00C51421 mov ebp,esp
/* 給esp減去一個16進制數字0E4H,産生新的esp */
00C51423 sub esp,0E4h
00C51429 push ebx
00C5142A push esi
00C5142B push edi
/* 将棧幀與開辟的空間全部初始化為0xCCCCCCCC */
00C5142C lea edi,[ebp-0E4h]
00C51432 mov ecx,39h
00C51437 mov eax,0CCCCCCCCh
00C5143C rep stos dword ptr es:[edi]
/* 建立局部變量num1、num2、result */
int num1 = 1;
00C5143E mov dword ptr [num1],1
int num2 = 2;
00C51445 mov dword ptr [num2],2
int result = 0;
00C5144C mov dword ptr [result],0
圖解如下
過程分析
- 首先_tmainCRTStartup函數調用main函數。
- 将ebp壓棧處理,儲存指向棧底的ebp的位址(友善函數傳回之後的現場恢複)。
- 将esp的值賦給ebp,産生新的ebp。
- 給esp減去一個16進制數0E4H(為main函數預開辟空間)。
- push ebx、esi、edi。
- lea指令,加載有效位址。
- 初始化預開辟的空間為0xCCCCCCCC。
- 建立變量num1、num2和result。
Add函數的調用
先檢視main函數中調用Add之前的反彙編:
result = Add(num1, num2);
/* 參數壓棧,先壓num2 */
00C51453 mov eax,dword ptr [num2]
00C51456 push eax
/* 參數壓棧,壓num1 */
00C51457 mov ecx,dword ptr [num1]
00C5145A push ecx
/* call指令調用Add函數 */
00C5145B call _Add (0C510E6h)
00C51460 add esp,8
00C51463 mov dword ptr [result],eax
逐語句執行到call指令,如下,跳轉到Add函數。
再來看Add函數的反彙編:
int Add(int left, int right){
/* 将ebp壓棧處理(友善函數傳回之後的現場恢複) */
00C513D0 push ebp
/* 這裡使esp的值賦給ebp,産生新的ebp */
00C513D1 mov ebp,esp
/* 給esp減去一個16進制數字0CCH,産生新的esp */
00C513D3 sub esp,0CCh
00C513D9 push ebx
00C513DA push esi
00C513DB push edi
/* 将棧幀與開辟的空間全部初始化為0xCCCCCCCC */
00C513DC lea edi,[ebp-0CCh]
00C513E2 mov ecx,33h
00C513E7 mov eax,0CCCCCCCCh
00C513EC rep stos dword ptr es:[edi]
/* 建立局部變量sum */
int sum = 0;
00C513EE mov dword ptr [sum],0
/* 擷取形參left和right的值,相加之後存到sum中 */
sum = left + right;
00C513F5 mov eax,dword ptr [left]
00C513F8 add eax,dword ptr [right]
00C513FB mov dword ptr [sum],eax
/* 将結果存到寄存器,通過寄存器帶回函數的傳回值 */
return sum;
00C513FE mov eax,dword ptr [sum]
}
/* 寄存器edi、esi、ebx出棧 */
00C51401 pop edi
00C51402 pop esi
00C51403 pop ebx
/* 将ebp賦給esp,使esp指向ebp指向的地方,釋放Add函數的棧幀 */
00C51404 mov esp,ebp
/* ebp出棧,将出棧的内容給ebp(即main函數的ebp),回到main函數的棧幀 */
00C51406 pop ebp
/* 出棧一次,将出棧的内容當做位址,并跳轉到該位址處(call指令的下一條) */
00C51407 ret
圖解如下
注意:C語言函數調用參數的壓棧順序為從右向左,上述例子中就是num2先壓棧,num1後壓棧。
過程分析
- 将num2存入eax寄存器,将eax壓棧。
- 将num1存入ecx寄存器,将ecx壓棧。
- call指令的調用,先将call指令的下一條指令的位址壓棧。然後跳轉到Add函數。
- 将main函數的ebp壓棧,友善函數傳回之後的現場恢複。
- 将esp的值賦給ebp,産生新的ebp,即Add函數的ebp。
- 給esp減去一個16進制的數0CCH,為Add函數預開辟空間。
- push ebx、esi、edi。
- lea指令,加載有效位址。
- 初始化預開辟的空間為0xCCCCCCCC。
- 建立變量sum。
- 擷取形參left和right的值再相加,将結果存到sum中。
- 将結果存儲到eax寄存器,通過寄存器帶回函數的傳回值。
- pop edi、esi、ebx。
- 将ebp的值賦給esp,是esp指向ebp指向的地方。釋放Add函數的棧幀。
- ebp出棧,将出棧的内容給ebp(即main函數的ebp),回到main函數的棧幀。
- ret指令,出棧一次,将出棧的内容當做位址,并跳轉到該位址處(call指令的下一條指令的位址)。
總結
- ebp寄存器:擴充基址指針寄存器(extended base pointer)其記憶體放一個指針,該指針指向系統棧最上面一個棧幀的底部。
- esp寄存器:擴充棧指針寄存器(extended stack pointer),是指針寄存器的一種,用于存放函數棧頂指針。esp為棧指針,用于指向棧的棧頂(下一個壓入棧的活動記錄的頂部)。