天天看點

【C語言】函數運作過程-----棧幀調用

每次函數調用,都為函數開辟一塊空間,成為棧幀。

首先應該明白,棧是從高位址向低位址延伸的。每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持着所需要的各種資訊。寄存器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在他們所在的位置

【C語言】函數運作過程-----棧幀調用

(1) push ebp

push就是壓棧,把ebp 的位址壓入棧中,

注:每次壓棧後,esp都指向最新的棧頂位置

(2) mov ebp,esp

使ebp=esp,即ebp也指向棧頂位置

【C語言】函數運作過程-----棧幀調用

(3) 為函數預開辟空間

sub         esp,Ch
           
【C語言】函數運作過程-----棧幀調用

(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初始化,是以未初始化的字元串,經常看到“燙燙”

【C語言】函數運作過程-----棧幀調用

可以檢視記憶體值的變化,來驗證

【C語言】函數運作過程-----棧幀調用

(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
           
【C語言】函數運作過程-----棧幀調用

(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壓入棧中

【C語言】函數運作過程-----棧幀調用

(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步驟差不多

【C語言】函數運作過程-----棧幀調用

(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指令。