每次函數調用,都為函數開辟一塊空間,成為棧幀。
首先應該明白,棧是從高位址向低位址延伸的。每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持着所需要的各種資訊。寄存器ebp指向目前的棧幀的底部(高位址),我們稱為棧底指針,寄存器esp指向目前的棧幀的頂部(低位址),我們稱為棧頂指針。
注意:EBP指向目前位于系統棧最上邊一個棧幀的底部,而不是系統棧的底部。嚴格說來,“棧幀底部”和“棧底”是不同的概念;ESP所指的棧幀頂部和系統棧的頂部是同一個位置。
給段代碼,剖析下面函數運作過程。
運作環境:VC6.0,(相比VS,更容易檢視記憶體)
#include<stdio.h>
int Sub(int x,int y)
{
int t=;
t=x-y;
return t;
}
int main()
{
int a=;
int b=;
int c=;
c=Sub(a,b);
return ;
}
給出這段代碼的彙編代碼
: int main()
: {
push ebp
mov ebp,esp
sub esp,Ch
push ebx
push esi
push edi
lea edi,[ebp-Ch]
C mov ecx,h
mov eax,CCCCCCCCh
rep stos dword ptr [edi]
: int a=;
mov dword ptr [ebp-],Ah
: int b=;
F mov dword ptr [ebp-],h
: int c=;
mov dword ptr [ebp-Ch],
: c=Sub(a,b);
D mov eax,dword ptr [ebp-]
push eax
mov ecx,dword ptr [ebp-]
push ecx
call @ILT+(_Sub) ()
A add esp,
D mov dword ptr [ebp-Ch],eax
:
: return ;
接下來分析這段彙編代碼
在這裡我們要知道在VC++下,連接配接器對控制台程式設定的入口函數是 mainCRTStartup,mainCRTStartup 再調用main 函數;
是以當我們操作時,首先會給mainCRTStartup()函數開辟一段空間,然後esp和ebp在他們所在的位置
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICdzFWRoRXdvN1LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX0EkaNh3YtJGasd1Y1ZlMkZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39jMwkTOxQTN3EDNwcDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
(1) push ebp
push就是壓棧,把ebp 的位址壓入棧中,
注:每次壓棧後,esp都指向最新的棧頂位置
(2) mov ebp,esp
使ebp=esp,即ebp也指向棧頂位置
(3) 為函數預開辟空間
sub esp,Ch
(4)3個push 以及初始化開辟的空間
push ebx
push esi
push edi
lea edi,[ebp-Ch]
mov ecx,h
mov eax,CCCCCCCCh
rep stos dword ptr [edi]
解釋一下,3個push 分别把ebx,esi,edi 3個寄存器壓入棧中。
lea 就是把 [ebp-4Ch]的位址放在edi中,ebp-4Ch是3個push之前esp的位置
2個move操作,ecx寄存器的值為13h,eax為初始化值0ccccccccch
然後rep stos:實際上就是把初始化開辟的空間,初始值為eax寄存器内的值0CCCCCCCCh,
從edi開始(edi儲存的esp的位置),向高位址的部分進行位元組拷貝,每一次拷貝4個位元組。
拷貝的内容就是eax的内容,拷貝次數為13h次。
注:用0xccccccccch初始化,是以未初始化的字元串,經常看到“燙燙”
可以檢視記憶體值的變化,來驗證
(5)實參入棧
10: int a=;
00401078 mov dword ptr [ebp-4],0Ah
11: int b=;
0040107F mov dword ptr [ebp-8],14h
12: int c=;
00401086 mov dword ptr [ebp-0Ch],0
(6)調用sub函數準備,形參入棧
形參從右向左入棧的,看出形參是實參的一份拷貝
: c=Sub(a,b);
D mov eax,dword ptr [ebp-]
push eax
mov ecx,dword ptr [ebp-]
push ecx
ebp-8就是b的位置,ebp-4就是a的位置
(7)call指令
00401095 call @ILT+(_Sub) ()
A add esp,
call指令就是把下一條指令add的位址0040109A壓入棧中
(8)進入sub函數
: int Sub(int x,int y)
: {
push ebp
mov ebp,esp
sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
4: int t=0;
8 mov dword ptr [ebp-],
: t=x-y;
F mov eax,dword ptr [ebp+]
sub eax,dword ptr [ebp+0Ch]
00401045 mov dword ptr [ebp-4],eax
6: return t;
8 mov eax,dword ptr [ebp-]
: }
B pop edi
C pop esi
D pop ebx
E mov esp,ebp
pop ebp
ret
步驟其實大緻和main函數一樣
(8.1) 為sub函數準備
push ebp
此時ebp指向的main函數的棧底指針
mov ebp,esp
sub esp,h
push ebx
push esi
push edi
lea edi,[ebp-h]
C mov ecx,h
mov eax,CCCCCCCCh
rep stos dword ptr [edi]
以上代碼 就不細細分析,大概和main函數2,3,4步驟差不多
(8.2)指向sub函數,計算內插補點
4: int t=;
00401038 mov dword ptr [ebp-4],0
5: t=x-y;
0040103F mov eax,dword ptr [ebp+8]
00401042 sub eax,dword ptr [ebp+0Ch]
00401045 mov dword ptr [ebp-4],eax
6: return t;
00401048 mov eax,dword ptr [ebp-4]
看出,計算機隻認識位址,不認識變量名,
把t初始化為0,然後計算t=x-y,把ebp+8的值(a) 存放在eax,然後把eax值為ebp+12的值(b) 相減 放在eax中,
然後把eax值儲存在t中
傳回值 t,把ebp-4内的值(t)取出放在eax中
(9)函數調用結束,釋放棧幀
這裡先介紹一個概念
現場保護 當出現中斷時,把CPU現在的狀态,也就是中斷的入口位址儲存在寄存器中,随後轉向執行其他任務,當任務完成,從寄存器中取出位址繼續執行。保護現場其實就是儲存中斷前一時刻的狀态不被破壞。保護現場通過利用一系列PUSH指令保護CPU現場,即将相關寄存器的内容入棧保護起來。
是以要把ebp 入棧push
B pop edi
C pop esi
D pop ebx
E mov esp,ebp
pop ebp
接下來的指令就是傳回,先進行3次出棧,把棧頂的指令分别給了edi,esi,ebx三個寄存器。然後把ebp給了esp,這時也就是讓esp指向了ebp的位置,這是ebp和esp指向同一位置,這個位置就是你所儲存的main()函數的ebp,然後再pop ebp,這樣ebp就維護到main函數的棧幀了
在這,當ret指令執行之後,會pop一下,把這個位址pop以後,就從Sub函數傳回了main()函數,這也是最初為什麼要儲存這個位址的原因。這樣call指令就完成了。此時指向mian函數中call指令的下一條指令add
A add esp,
D mov dword ptr [ebp-Ch],eax
main函數中
esp+8 :把形參a,b 釋放
mov dword ptr [ebp-0Ch],eax:把eax中值(傳回值t)儲存在ebp-12(c的位置)中
接下來,和對函數的傳回類似,對main()函數的傳回,然後再銷毀main()函數,執行ret指令。