C語言的函數調用過程(棧幀的創建與銷毀)


從匯編的角度解析函數調用過程

看看下面這個簡單函數的調用過程:

 1 int Add(int x,int y)
 2 {
 3     int sum = 0;
 4     sum = x + y;
 5     return sum;
 6 }
 7 
 8 int main ()
 9 {
10     int a = 10;
11     int b = 12;
12     int ret = 0;
13     ret = Add(a,b);
14     return 0;
15 }

今天主要用匯編代碼去講述這個過程,首先介紹幾個寄存器和簡單的匯編指令的意思。 
先看幾個函數調用過程涉及到的寄存器: 
(1)esp:棧指針寄存器(extended stack pointer),其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。 
(2)ebp:基址指針寄存器(extended base pointer),其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的底部。 
(3)eax 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。 
(4)ebx 是”基地址”(base)寄存器, 在內存尋址時存放基地址。 
(5)ecx 是計數器(counter), 是重復(REP)前綴指令和LOOP指令的內定計數器。 
(6)edx 則總是被用來放整數除法產生的余數。 
(7)esi/edi分別叫做”源/目標索引寄存器”(source/destination index),因為在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目標串. 
在32位平台上,ESP每次減少4字節。 
再看幾條簡單的匯編指令: 
mov :數據傳送指令,也是最基本的編程指令,用於將一個數據從源地址傳送到目標地址(寄存器間的數據傳送本質上也是一樣的) 
sub:減法指令 
lea:取偏移地址 
push:實現壓入操作的指令是PUSH指令 
pop:實現彈出操作的指令 
call:用於保存當前指令的下一條指令並跳轉到目標函數。 
這些指令當然能看懂最好,可以讓你很深刻的理解函數調用過程,不能看懂就只能通過我的描述去理解了。 
進行分析之前,先來了解下內存地址空間的分布: 

棧空間是向低地址增長的,主要是用來保存函數棧幀。 棧空間的大小很有限,僅有區區幾MB大小 
匯編代碼實現: 
main函數匯編代碼:

int main ()
{
011B26E0  push        ebp  
011B26E1  mov         ebp,esp 
011B26E3  sub         esp,0E4h 
011B26E9  push        ebx  
011B26EA  push        esi  
011B26EB  push        edi  
011B26EC  lea         edi,[ebp-0E4h] 
011B26F2  mov         ecx,39h 
011B26F7  mov         eax,0CCCCCCCCh 
011B26FC  rep stos    dword ptr es:[edi] 
    int a = 10;
011B26FE  mov         dword ptr [a],0Ah 
    int b = 12;
011B2705  mov         dword ptr [b],0Ch 
    int ret = 0;
011B270C  mov         dword ptr [ret],0 
    ret = Add(a,b);
011B2713  mov         eax,dword ptr [b] 
011B2716  push        eax  
011B2717  mov         ecx,dword ptr [a] 
011B271A  push        ecx  
011B271B  call        @ILT+640(_Add) (11B1285h) 
011B2720  add         esp,8 
011B2723  mov         dword ptr [ret],eax 
    return 0;
011B2726  xor         eax,eax 
}
011B2728  pop         edi  
011B2729  pop         esi  
011B272A  pop         ebx  
011B272B  add         esp,0E4h 
011B2731  cmp         ebp,esp 
011B2733  call        @ILT+450(__RTC_CheckEsp) (11B11C7h) 
011B2738  mov         esp,ebp 
011B273A  pop         ebp  
011B273B  ret            

Add函數匯編代碼:

int Add(int x,int y)
{
011B26A0  push        ebp  
011B26A1  mov         ebp,esp 
011B26A3  sub         esp,0CCh 
011B26A9  push        ebx  
011B26AA  push        esi  
011B26AB  push        edi  
011B26AC  lea         edi,[ebp-0CCh] 
011B26B2  mov         ecx,33h 
011B26B7  mov         eax,0CCCCCCCCh 
011B26BC  rep stos    dword ptr es:[edi] 
    int sum = 0;
011B26BE  mov         dword ptr [sum],0 
    sum = x + y;
011B26C5  mov         eax,dword ptr [x] 
011B26C8  add         eax,dword ptr [y] 
011B26CB  mov         dword ptr [sum],eax 
    return sum;
011B26CE  mov         eax,dword ptr [sum] 
}
011B26D1  pop         edi  
011B26D2  pop         esi  
011B26D3  pop         ebx  
011B26D4  mov         esp,ebp 
011B26D6  pop         ebp  
011B26D7  ret              

下面圖中詳細描述了調用過程地址變化(此處所有地址是取自32位windows系統vs編輯器下的調試過程。): 

過程描述: 
1、參數拷貝(參數實例化)。 
2、保存當前指令的下一條指令,並跳轉到被調函數。 
這些操作均在main函數中進行。

接下來是調用Add函數並執行的一些操作,包括: 
1、移動ebp、esp形成新的棧幀結構。 
2、壓棧(push)形成臨時變量並執行相關操作。 
3、return一個值。 
這些操作在Add函數中進行。

被調函數完成相關操作后需返回到原函數中執行下一條指令,操作如下: 
1、出棧(pop)。 
2、回復main函數的棧幀結構。(pop ) 
3、返回main函數 
這些操作也在Add函數中進行。 至此,在main函數中調用Add函數的整個過程已經完成。 
總結起來整個過程就三步: 
1)根據調用的函數名找到函數入口; 
2)在棧中審請調用函數中的參數及函數體內定義的變量的內存空間 
3)函數執行完后,釋放函數在棧中的審請的參數和變量的空間,最后返回值(如果有的話) 
如果你學了微機原理,你會想到cpu中斷處理過程,是的,函數調用過程和中斷處理過程一模一樣。

函數調用約定: 
這里再補充一下各種調用規定的基本內容。 
_stdcall調用約定

所有參數按照從右到左壓入堆棧,由被調用的子程序清理堆棧

_cdecl調用約定(The C default calling convention,C調用規定)

參數也是從右到左壓入堆棧,但由調用者清理堆棧。

_fastcall調用約定

顧名思義,_fastcall的目的主要是為了更快的調用函數。它主要依靠寄存器傳遞參數,剩下的參數依然按照從右到左的順序壓入堆棧,並由被調用的子程序清理堆棧。

本篇博文是按調用約定__stdcall 調用函數。

 

csdn博客地址:http://blog.csdn.net/qq_38646470


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM