發現有一些問題幾乎是全部的新人都會遇到,並且也常由於缺少一些基本的知識而無從下手。函數調用棧的內容就是其中之一。因而花點時間把之前寫的內容整理出來。數據結構
程序在運行期間,內存中有一塊區域,用來實現程序的函數調用機制。這塊區域是一塊LIFO的數據結構區域,咱們能夠叫函數棧(調用棧)。每一個未退出的函數都會在函數棧中擁有一塊數據區,咱們叫函數的棧幀。函數的調用棧幀中,保存了相應的函數的一些重要信息:函數中使用的局部變量,函數的參數,另外還有一些維護函數棧所須要的數據,好比EBP指針,函數的返回地址。以下圖。咱們假設程序當前執行的函數是Z函數,那麼在函數調用棧中就會存在相似像這樣的結構(EBP所指向的實際上是「父函數」的調用棧幀,如何作到的後面會解釋):函數
編譯器把C/C++代碼編譯成彙編指令時,會生成一系列(頗有規則)的指令來支持函數調用的機制。當一個函數調用發生時(咱們假設是Z函數內調用了A函數):this
會執行零到多個PUSH指令(用於參數入棧),而後有執行一個CALL指令。CALL指令內部其實還暗含了一個將返回地址(即CALL指令下一條指令的地址)壓棧的動做。以後,IP(instruction point)指向要跳轉的指令的地址(CALL指令的目標地址,也就是下一個函數)spa
大部分的本地編譯器都會在每一個函數體以前插入相似以下指令:PUSH EBP; MOV EBP ESP;即,在程式執行到一個函數的真正函數體時,已有如下數據順序入棧:參數,返回地址,EBP。(注意,EBP是如何做到指向上個函數調用棧幀的)debug
將棧頂指針進行上移,「空」出一塊區域,用於臨時地存放函數的局部變量。3d
這幾步完成(也就是一個函數調用發生了),函數調用棧就會變成這個樣子,函數調用結束的時候,相應的作「反向的動做」就能夠了。指針
咱們用一段很是簡單的真實代碼看來:code
在main函數中調用 increase 函數。用VS單步斷點打開彙編模式,能夠看到以下的代碼blog
對照前面的說明,咱們能夠看到,調用函數前有 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
再加上參數,返回地址,局部變量的入棧出棧,經過這樣一種統一的、並不複雜代碼生成模式和數據結構,能夠應對任意複雜的函數調用狀況,極其靈活。我一直以爲這是計算機科學中很是漂亮的一個創造,也是以簡馭繁的一個經曲例子。