發現有一些問題幾乎是所有的新人都會遇到,而且也常因為缺乏一些基本的知識而無從下手。函數調用棧的內容就是其中之一。於是花點時間把以前寫的內容整理出來。
程序在運行期間,內存中有一塊區域,用來實現程序的函數調用機制。這塊區域是一塊LIFO的數據結構區域,我們可以叫函數棧(調用棧)。每個未退出的函數都會在函數棧中擁有一塊數據區,我們叫函數的棧幀。函數的調用棧幀中,保存了相應的函數的一些重要信息:函數中使用的局部變量,函數的參數,另外還有一些維護函數棧所需要的數據,比如EBP指針,函數的返回地址。如下圖。我們假設程序當前執行的函數是Z函數,那麼在函數調用棧中就會存在類似像這樣的結構(EBP所指向的其實是“父函數”的調用棧幀,如何做到的後面會解釋):
在main函數中調用 increase 函數。用VS單步斷點打開匯編模式,可以看到如下的代碼
對照前面的說明,我們可以看到,調用函數前有 push 指令先把函數參數壓棧。之後才真正的call increase 。然後我們進入 increase 函數再看看函數體是什麼樣的。
進入函數前,做的動作主要是保存各寄存器,注意“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, esp013D55EE mov esp, ebp // 恢復到原來的調用幀
013D55F0 pop ebp
再加上參數,返回地址,局部變量的入棧出棧,通過這樣一種統一的、並不復雜代碼生成模式和數據結構,可以應對任意復雜的函數調用情況,極其靈活。我一直覺得這是計算機科學中非常漂亮的一個創造,也是以簡馭繁的一個經曲例子。