天天看點

c++ 編譯器_C|編譯器是如何實作函數調用的?8 ret7 堆棧平衡9 中間值存儲到寄存器10 表達式的最後結果儲存在寄存器中

任何程式設計語言,函數都是很重要的一個概念。

算法要借助函數來實作。

面向過程程式設計的函數是其基本子產品。

面向對象程式設計的函數(方法)是類或對象對其屬性(資料的處理)。

函數式程式設計自然更是以函數為核心。

通常,程式的控制結構會確定一個入口、一個出口,支援函數的嵌套調用。如何確定代碼的正确流動(包括正确傳回原函數調用處)?編譯器會維護一個棧的記憶體結構。

另外,關于堆棧平衡,可由調用函數負責,也可由被調函數負責。當有多個參數時,參數按什麼順序計算?這些都可由調用約定來進行規定,如:

void __stdcall add(int a,int b);

函數聲明中的__stdcall就是關于調用約定的聲明。其中标準C函數的預設調用約定是__stdcall,C++全局函數和靜态成員函數的預設調用約定是__cdecl,類的成員函數的調用約定是__thiscall。剩下的還有__fastcall,__naked等。

調用約定指明了函數調用中的參數傳遞方式和堆棧平衡方式。

調用約定 堆棧平衡方式

__stdcall 函數自己平衡

__cdecl 調用者負責平衡

__thiscall 調用者負責平衡

__fastcall 調用者負責平衡

__naked 編譯器不負責平衡,由編寫者自己負責

簡單的一個函數調用語句,其實對于編譯器來說,是一個比較複雜的過程。

以下是一個函數嵌套調用的執行個體:

#include using namespace std;int combinations(int n, int k);int fact(int n);int main() {    int n, k;    cout << "Enter the number of objects (n): ";    cin >> n;    cout << "Enter the number to be chosen (k): ";    cin >> k;    cout << "C(n, k) = " << combinations(n, k) << endl; // 在這裡設一斷點    return 0;}int combinations(int n, int k) // C(n, k){    return fact(n) / (fact(k) * fact(n - k));}int fact(int n) // factorial of n{    int result = 1;    for (int i = 1; i <= n; i++)        result *= i;    return result;}
           

整體流程如下:

c++ 編譯器_C|編譯器是如何實作函數調用的?8 ret7 堆棧平衡9 中間值存儲到寄存器10 表達式的最後結果儲存在寄存器中

編譯後在上述備注處插入一斷點(F9)→運作(F5),按提示輸入:

Enter the number of objects (n): 6Enter the number to be chosen (k): 2
           

運作至斷點處,調出反彙編調試視窗,跟蹤fact(6)的内部流程:

此時的調用堆棧是:

c++ 編譯器_C|編譯器是如何實作函數調用的?8 ret7 堆棧平衡9 中間值存儲到寄存器10 表達式的最後結果儲存在寄存器中

1 參數壓棧(傳參時,可能存在隐式類型轉換)

c++ 編譯器_C|編譯器是如何實作函數調用的?8 ret7 堆棧平衡9 中間值存儲到寄存器10 表達式的最後結果儲存在寄存器中

push eax,表示将eax的值壓和棧、記憶體棧,具體位置由寄存器esp給出。

寄存器ebp(base pointer )可稱為“幀指針”或“基址指針”,其實語意是相同的。ebp指向了本次函數調用開始時的棧頂指針,它也是本次函數調用時的“棧底”(這裡的意思是,在一次函數調用中,ebp向下是函數的臨時變量使用的空間)。在函數調用開始時,我們會使用mov ebp,esp,把目前的esp儲存在ebp中。

寄存器esp(stack pointer)可稱為“ 棧指針”。esp指向目前的棧頂,它是動态變化的,随着我們申請更多的臨時變量,esp值不斷減小(棧是向下生長的)。函數調用結束,我們使用mov esp,ebp,來還原之前儲存的esp。

在函數調用過程中,ebp和esp之間的空間被稱為本次函數調用的“棧幀”。函數調用結束後,處于棧幀之前的所有内容都是本次函數調用過程中配置設定的臨時變量,都需要被“返還”。這樣在概念上,給了函數調用一個更明顯的分界。

2 call fact(6)

0040193C   call        @ILT+165(fact) (004010aa)00401941   add         esp,4
           

call 相當于 push+jmp。

2.1 push 傳回位址00401941

2.2 jmp (fact) (004010aa)

004010AA jmp fact (004019a0)

也就是,首先把call指令的下一條指令位址作為本次函數調用的傳回位址壓棧,然後使用jmp指令修改指令指針寄存器EIP,使cpu執行 fact函數的指令代碼。

指令指針寄存器也叫程式計數器,是用于存放下一條指令所在單元的位址的地方。

當執行一條指令時,首先需要根據PC中存放的指令位址,将指令由記憶體取到指令寄存器中,此過程稱為“取指令”。與此同時,PC中的位址或自動加1或由轉移指針給出下一條指令的位址。此後經過分析指令,執行指令。完成第一條指令的執行,而後根據PC取出第二條指令的位址,如此循環,執行每一條指令。

可以看到,程式計數器是一個cpu執行指令代碼過程中的關鍵寄存器:它指向了目前計算機要執行的指令位址,CPU總是從程式計數器取出目前指令來執行。當指令執行後,程式計數器的值自動增加,指向下一條将要執行的指令。

在x86彙編中,執行程式計數器功能的寄存器被叫做EIP,也叫作指令指針寄存器。

3 一些寄存器壓棧,儲存其狀态資訊

004019A0   push        ebp004019A1   mov         ebp,esp004019A3   sub         esp,48h004019A6   push        ebx004019A7   push        esi004019A8   push        edi
           

4 棧幀配置設定,并初始化

004019A9   lea         edi,[ebp-48h]004019AC   mov         ecx,12h004019B1   mov         eax,0CCCCCCCCh004019B6   rep stos    dword ptr [edi]
           

rep指令的目的是重複其上面的指令,ECX的值是重複的次數。

STOS指令的作用是将eax中的值拷貝到ES:EDI指向的位址。

5 局部變量壓棧

004019B8   mov         dword ptr [ebp-4],126:      for (int i = 1; i <= n; i++)004019BF   mov         dword ptr [ebp-8],1
           

6 傳回值(或位址)儲存到寄存器eax

c++ 編譯器_C|編譯器是如何實作函數調用的?8 ret7 堆棧平衡9 中間值存儲到寄存器10 表達式的最後結果儲存在寄存器中

可以察看此時寄存器調試視窗:

c++ 編譯器_C|編譯器是如何實作函數調用的?8 ret7 堆棧平衡9 中間值存儲到寄存器10 表達式的最後結果儲存在寄存器中

傳回值傳回時,可能存在隐式資料類型轉換。

7 一些寄存器值從棧上恢複

004019E8   pop         edi004019E9   pop         esi004019EA   pop         ebx004019EB   mov         esp,ebp004019ED   pop         ebp
           

8 ret

ret = pop + jmp

004019EE ret

00401941 add esp,4

表示取出目前棧頂值,作為傳回位址,并将指令指針寄存器EIP修改為該值,實作函數傳回。

7 堆棧平衡

00401941   add         esp,4
           

9 中間值存儲到寄存器

c++ 編譯器_C|編譯器是如何實作函數調用的?8 ret7 堆棧平衡9 中間值存儲到寄存器10 表達式的最後結果儲存在寄存器中

10 表達式的最後結果儲存在寄存器中

c++ 編譯器_C|編譯器是如何實作函數調用的?8 ret7 堆棧平衡9 中間值存儲到寄存器10 表達式的最後結果儲存在寄存器中

以下是combinations(int n, int k)整體的彙編代碼(不包括函數調用時的進入):

18:   int combinations(int n, int k) // C(n, k)19:   {00401920   push        ebp00401921   mov         ebp,esp00401923   sub         esp,40h00401926   push        ebx00401927   push        esi00401928   push        edi00401929   lea         edi,[ebp-40h]0040192C   mov         ecx,10h00401931   mov         eax,0CCCCCCCCh00401936   rep stos    dword ptr [edi]20:      return fact(n) / (fact(k) * fact(n - k));00401938   mov         eax,dword ptr [ebp+8]0040193B   push        eax0040193C   call        @ILT+165(fact) (004010aa)00401941   add         esp,400401944   mov         esi,eax00401946   mov         ecx,dword ptr [ebp+0Ch]00401949   push        ecx0040194A   call        @ILT+165(fact) (004010aa)0040194F   add         esp,400401952   mov         edi,eax00401954   mov         edx,dword ptr [ebp+8]00401957   sub         edx,dword ptr [ebp+0Ch]0040195A   push        edx0040195B   call        @ILT+165(fact) (004010aa)00401960   add         esp,400401963   imul        edi,eax00401966   mov         eax,esi00401968   cdq00401969   idiv        eax,edi21:   }0040196B   pop         edi0040196C   pop         esi0040196D   pop         ebx0040196E   add         esp,40h00401971   cmp         ebp,esp00401973   call        __chkesp (00422890)00401978   mov         esp,ebp0040197A   pop         ebp0040197B   ret
           

-End-

繼續閱讀