標籤:
發現有一些問題幾乎是所有的新人都會遇到,而且也常因為缺乏一些基本的知識而無從下手。函數調用棧的內容就是其中之一。於是花點時間把以前寫的內容整理出來。
程式在運行期間,記憶體中有一塊地區,用來實現程式的函數調用機制。這塊地區是一塊LIFO的資料結構地區,我們可以叫函數棧(調用棧)。每個未退出的函數都會在函數棧中擁有一塊資料區,我們叫函數的棧幀。函數的調用棧幀中,儲存了相應的函數的一些重要訊息:函數中使用的局部變數,函數的參數,另外還有一些維護函數棧所需要的資料,比如EBP指標,函數的返回地址。如。我們假設程式當前執行的函數是Z函數,那麼在函數調用棧中就會存在類似像這樣的結構(EBP所指向的其實是“父函數”的調用棧幀,如何做到的後面會解釋):
編譯器把C/C++代碼編譯成彙編指令時,會產生一系列(很有規則)的指令來支援函數調用的機制。當一個函數調用發生時(我們假設是Z函數內調用了A函數):
-
會執行零到多個PUSH指令(用於參數入棧),然後有執行一個CALL指令。CALL指令內部其實還暗含了一個將返回地址(即CALL指令下一條指令的地址)壓棧的動作。之後,IP(instruction point)指向要跳轉的指令的地址(CALL指令的目標地址,也就是下一個函數)
-
大部分的本地編譯器都會在每個函數體之前插入類似如下指令:PUSH EBP; MOV EBP ESP;即,在程式執行到一個函數的真正函數體時,已有以下資料順序入棧:參數,返回地址,EBP。(注意,EBP是如何作到指向上個函數調用棧幀的)
-
將棧頂指標進行上移,“空”出一塊地區,用於臨時地存放函數的局部變數。
這幾步完成(也就是一個函數調用發生了),函數調用棧就會變成這個樣子,函數調用結束的時候,相應的做“反向的動作”就可以了。
我們用一段非常簡單的真實代碼看來:
- int increase(int a) {
-
- ????int temp = 4;
-
- ????return a + 3;
- }
-
- int main(int argc, char* const argv[])
- {
- ????int sum = increase(3);
-
- ????return 0;
- }
在main函數中調用 increase 函數。用VS單步斷點開啟彙編模式,可以看到如下的代碼
- ????int sum = increase(3);
- 00D2561E??push????????3??
- 00D25620??call????????increase (0D2142Eh)??
- 00D25625??add?????????esp,4??
- 00D25628??mov?????????dword ptr [sum],eax??
對照前面的說明,我們可以看到,調用函數前有 push 指令先把函數參數壓棧。之後才真正的call increase 。然後我們進入 increase 函數再看看函數體是什麼樣的。
- int increase(int a) {
- 000455C0??push????????ebp??
- 000455C1??mov?????????ebp,esp??
- 000455C3??sub?????????esp,0CCh??
- 000455C9??push????????ebx??
- 000455CA??push????????esi??
- 000455CB??push????????edi??
- 000455CC??lea?????????edi,[ebp-0CCh]??
- 000455D2??mov?????????ecx,33h??
- 000455D7??mov?????????eax,0CCCCCCCCh??
- 000455DC??rep stos????dword ptr es:[edi]??
-
- ????int temp = 4;
- 000455DE??mov?????????dword ptr [temp],4??
-
- ????return a + temp;
- 000455E5??mov?????????eax,dword ptr [a]??
- 000455E8??add?????????eax,dword ptr [temp]??
- }
- 000455EB??pop?????????edi??
- 000455EC??pop?????????esi??
- 000455ED??pop?????????ebx??
- 000455EE??mov?????????esp,ebp??
- 000455F0??pop?????????ebp??
- 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
再加上參數,返回地址,局部變數的入棧出棧,通過這樣一種統一的、並不複雜代碼產生模式和資料結構,可以應對任意複雜的函數調用情況,極其靈活。我一直覺得這是電腦科學中非常漂亮的一個創造,也是以簡馭繁的一個經曲例子。
C開發基礎--函數調用棧