淺析c++的函數調用

來源:互聯網
上載者:User

以前學彙編,很清楚函數是怎麼調用的,但是久不用之,又忘了~~不知其他人有沒有這種經曆,寫c/c++程式時如果瞭解許多編譯器底層細節,是很爽的;否則,有時會很沮喪。雖然是比較簡單的內容,讓我們也來回憶一下...

我們知道,函數調用最通常的傳遞參數的方式莫過於使用堆棧;函數的局部變數也是在棧上建立。具體怎麼做呢?

假如我們有這麼一個小小的程式:

void Test(int a){
 int b;
}

int main(int argc, char* argv[])
{
 int a;
 Test(a);
 return 0;
}

在VC6.0的調試版中反編譯結果如下(加了些注釋):

6:    void Test(int a){;函數Tes
00401020   push        ebp ;儲存主調函數棧幀指標
00401021   mov         ebp,esp ;啟用當前棧幀指標
00401023   sub         esp,44h ;為Test留出44h的“私人空間”:44h=4*17=4*11h(聯絡下面)
00401026   push        ebx ;儲存寄存器值
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-44h]
0040102C   mov         ecx,11h ;11h=17為迴圈次數
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi] ;迴圈,對“私人空間”填充(int)3
7:        int b;
8:    }

.......

10:   int main(int argc, char* argv[])
11:   {
00401050   push        ebp;
00401051   mov         ebp,esp ;啟用當前棧幀指標
00401053   sub         esp,44h ;為main函數留出44h的“私人空間”
00401056   push        ebx   
00401057   push        esi
00401058   push        edi
00401059   lea         edi,[ebp-44h]
0040105C   mov         ecx,11h
00401061   mov         eax,0CCCCCCCCh
00401066   rep stos    dword ptr [edi]
12:       int a;//變數定義沒有對應的彙編代碼,編譯器會以EBP-4來表示(int)a
13:       Test(a);
00401068   mov         eax,dword ptr [ebp-4] ;dword ptr [ebp-4]也即main函數中int a的值
0040106B   push        eax ;int a 作為參數壓棧
0040106C   call        @ILT+0(Test) (00401005) ;調用Test函數
00401071   add         esp,4 ;因為在call之前壓入了一個參數int a,佔四個位元組,Test函數退出後esp要回退到未壓入參數前的位置。
14:       return 0;
00401074   xor         eax,eax
15:   }
00401076   pop         edi
00401077   pop         esi
00401078   pop         ebx
00401079   add         esp,44h
0040107C   cmp         ebp,esp
0040107E   call        __chkesp (004010a0)
00401083   mov         esp,ebp
00401085   pop         ebp
00401086   ret
在堆棧中,每個函數都有一個相關的棧楨(stack frame)來儲存它所有的局部對象和運算式計算過程中用到的臨時對象。通常編譯器使用EBP寄存器來指示當前活動的棧楨。編譯器在編譯時間將所有局部對象解析成相對於棧楨指標(EBP)的固定位移,函數則通過棧楨指標來間接訪問局部對象。

編譯器編譯一個函數時,會在它的開頭添加一些代碼來為其建立並初始化棧楨,這些代碼被稱為序言(prologue);同樣,它也會在函數的結尾處放上代碼來清除棧楨,這些代碼叫做尾聲(epilogue)。
一般情況下,序言是這樣的:

Push EBP ; 把原來的棧楨指標儲存到棧上 
Mov EBP, ESP ; 啟用新的棧楨 
Sub ESP, 10 ; 減去一個數字,讓ESP指向棧楨的末尾
第一條指令把原來的棧楨指標EBP儲存到棧上;第二條指令通過讓EBP指向主調函數的EBP的儲存位置來啟用被調函數的棧楨;第三條指令把ESP減去了一個數字,這樣ESP就指向了當前棧楨的末尾,而這個數字是函數要用到的所有局部對象和臨時對象的大小。編譯時間,編譯器知道函數的所有局部對象的類型和“體積”,所以,它能很容易的計算出棧楨的大小。 
尾聲所做的正好和序言相反,它必須把當前棧楨從棧上清除掉:
Mov ESP, EBP 
Pop EBP ; 啟用主調函數的棧楨 
Ret ; 返回主調函數

它讓ESP指向主調函數的棧楨指標的儲存位置(也就是被調函數的棧楨指標指向的位置),彈出EBP從而啟用主調函數的棧楨,然後返回主調函數。
一旦CPU遇到返回指令,它就要做以下兩件事:把返回地址從棧中彈出,然後跳轉到那個地址去。返回地址是主調函數執行call指令調用被調函數時自動壓棧的。Call指令執行時,會先把緊隨在它後面的那條指令的地址(被調函數的返回地址)壓入棧中,然後跳轉到被調函數的開始位置。主調函數把被調函數的參數也壓進了堆棧,所以參數也是棧楨的一部分。函數返回後,主調函數需要移除這些參數,它通過把所有參數的總體積加到ESP上來達到目的。

如果有一個函數調用鏈,foo()->bar()->widget();則其堆棧如下:

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.