C開發基礎--函數調用棧

發現有一些問題幾乎是全部的新人都會遇到,並且也常由於缺少一些基本的知識而無從下手。函數調用棧的內容就是其中之一。因而花點時間把之前寫的內容整理出來。數據結構

程序在運行期間,內存中有一塊區域,用來實現程序的函數調用機制。這塊區域是一塊LIFO的數據結構區域,咱們能夠叫函數棧(調用棧)。每一個未退出的函數都會在函數棧中擁有一塊數據區,咱們叫函數的棧幀。函數的調用棧幀中,保存了相應的函數的一些重要信息:函數中使用的局部變量,函數的參數,另外還有一些維護函數棧所須要的數據,好比EBP指針,函數的返回地址。以下圖。咱們假設程序當前執行的函數是Z函數,那麼在函數調用棧中就會存在相似像這樣的結構(EBP所指向的實際上是「父函數」的調用棧幀,如何作到的後面會解釋):函數

image

編譯器把C/C++代碼編譯成彙編指令時,會生成一系列(頗有規則)的指令來支持函數調用的機制。當一個函數調用發生時(咱們假設是Z函數內調用了A函數):this

    1. 會執行零到多個PUSH指令(用於參數入棧),而後有執行一個CALL指令。CALL指令內部其實還暗含了一個將返回地址(即CALL指令下一條指令的地址)壓棧的動做。以後,IP(instruction point)指向要跳轉的指令的地址(CALL指令的目標地址,也就是下一個函數)spa

    2. 大部分的本地編譯器都會在每一個函數體以前插入相似以下指令:PUSH EBP; MOV EBP ESP;即,在程式執行到一個函數的真正函數體時,已有如下數據順序入棧:參數,返回地址,EBP。(注意,EBP是如何做到指向上個函數調用棧幀的)debug

    3. 將棧頂指針進行上移,「空」出一塊區域,用於臨時地存放函數的局部變量。3d

這幾步完成(也就是一個函數調用發生了),函數調用棧就會變成這個樣子,函數調用結束的時候,相應的作「反向的動做」就能夠了。指針

image

 

咱們用一段很是簡單的真實代碼看來:code

  1. int increase(int a) {
  2.  
  3.     int temp = 4;
  4.  
  5.     return a + 3;
  6. }
  7.  
  8. int main(int argc, char* const argv[])
  9. {
  10.     int sum = increase(3);
  11.  
  12.     return 0;
  13. }

在main函數中調用 increase 函數。用VS單步斷點打開彙編模式,能夠看到以下的代碼blog

  1.     int sum = increase(3);
  2. 00D2561E  push        3  
  3. 00D25620  call        increase (0D2142Eh)  
  4. 00D25625  add         esp,4  
  5. 00D25628  mov         dword ptr [sum],eax  

對照前面的說明,咱們能夠看到,調用函數前有 push 指令先把函數參數壓棧。以後才真正的call increase 。而後咱們進入 increase 函數再看看函數體是什麼樣的。內存

  1. int increase(int a) {
  2. 000455C0  push        ebp  
  3. 000455C1  mov         ebp,esp  
  4. 000455C3  sub         esp,0CCh  
  5. 000455C9  push        ebx  
  6. 000455CA  push        esi  
  7. 000455CB  push        edi  
  8. 000455CC  lea         edi,[ebp-0CCh]  
  9. 000455D2  mov         ecx,33h  
  10. 000455D7  mov         eax,0CCCCCCCCh  
  11. 000455DC  rep stos    dword ptr es:[edi]  
  12.  
  13.     int temp = 4;
  14. 000455DE  mov         dword ptr [temp],4  
  15.  
  16.     return a + temp;
  17. 000455E5  mov         eax,dword ptr [a]  
  18. 000455E8  add         eax,dword ptr [temp]  
  19. }
  20. 000455EB  pop         edi  
  21. 000455EC  pop         esi  
  22. 000455ED  pop         ebx  
  23. 000455EE  mov         esp,ebp  
  24. 000455F0  pop         ebp  
  25. 000455F1  ret  

進入函數前,作的動做主要是保存各寄存器,注意「sub esp,0xcch」就是移動ESP,空出局部變量的「位置」,爲何只有一個局部變量,卻生成了這麼大塊區域呢?

Stackoverflow上有解釋:

This extra space is generated by the /Zi compile option. Which enables Edit + Continue. The extra space is available for local variables that you might add when you edit code while debugging.

You are also seeing the effect of /RTC, it initializes all local variables to 0xcccccccc so that it is easier to diagnose problems due to forgetting to initialize variables. Of course none of this code is generated in the default Release configuration settings.

從這段簡單的代碼中,咱們能夠知道函數調用大概是什麼回事了。經過上面的內容,咱們仔細體會下ESP和EBP兩個寄存器的變化,也就下面向個指令

013D55C0  push        ebp      // 構建新的調用幀
013D55C1  mov         ebp, esp

013D55EE  mov         esp, ebp // 恢復到原來的調用幀
013D55F0  pop         ebp

再加上參數,返回地址,局部變量的入棧出棧,經過這樣一種統一的、並不複雜代碼生成模式和數據結構,能夠應對任意複雜的函數調用狀況,極其靈活。我一直以爲這是計算機科學中很是漂亮的一個創造,也是以簡馭繁的一個經曲例子。

相關文章
相關標籤/搜索