文章來源:https://blog.seclibs.com/函數調用堆棧圖-c語言/函數
咱們就使用一個簡單的c語言程序來對描述一下在函數調用的時候都發生了什麼。調試
中間的一小段沒有意義的彙編語言是爲了方便設置斷點,爲後面的調試作好鋪墊,由於有時會碰到找不到斷點位置的狀況,使用這個方法,能夠在找不到斷點的時候向後執行一次,而不破壞咱們想調試的程序當前的堆棧狀態,這裏對main函數和sum函數的效果是相似的,這裏直接跟着斷點來執行分析sum函數的堆棧操做。blog
咱們先假設初始狀態下的堆棧圖以下,esp與ebp的真實距離咱們省略。get
接下來咱們來看一下後面的操做。編譯器
在程序的執行當中,咱們通常都是按照從右向左的方式去處理的,這裏也不例外,咱們能夠發現當咱們調用sum函數對數字1和數字2進行處理的時候,將數字2和1依次壓入棧中,這個時候堆棧的狀況是這個樣子的,esp的值已經減8。博客
接下來調用了call,這時進行了兩步操做,先將call後面的地址push進堆棧,而後再jmp到call所調用的地址。編譯
由於jmp是不會影響堆棧的,因此如今的堆棧狀況是這樣的基礎
而後由於編譯器的緣由在call的時候還會有一個jmp來中轉到後面的處理函數,由於jmp不影響堆棧,咱們能夠忽略掉它,這裏是跳轉到了sum函數的處理位置。變量
此時的堆棧是沒有發生變化的,如今開始到了函數調用的關鍵階段了。cli
首先先將ebp的值push到堆棧中,由於用到了ebp尋址的方式,因此這裏用這種方式來保存ebp中本來的值,而後將esp的值賦給ebp,用ebp尋址來代替esp尋址,由於esp的值一直在不斷的發生變化,使用esp尋址會帶來很大的計算負擔,此時esp與ebp都指向了同一塊地址,其中的內容是原來的ebp的值。
而後讓esp減去了0c0h位,開始提高堆棧了,爲程序的運行開闢一個存儲空間,這個區域也就是平時所說的緩衝區,由於一個單元是四個字節,c0也就是往上提了48個格,因爲位置有限中間依舊省略,此時堆棧就變成了以下的樣子。
後面又進行了一系列的push操做,也是爲了方便在後續使用這些寄存器的時候保證它們初始的值不丟失,與前面保存ebp的值是同樣的方式。
而後接下來的四步操做只有一個目的,那就是將中間的48格所有值爲CC,CC在調試的時候至關於斷點,也就是若是你程序跑過的話,就會觸發斷點不會再繼續執行了。
lea是交換地址中的值,給eax和ecx賦值是爲rep的執行作準備的,stos是將eax中的值賦給edi,rep是執行後面的指令ecx次。
接下來的兩步指令咱們忽略,它們是vs編譯器添加的調試指令
由於eax通常是來用做返回值的,因此這裏的計算都是跟eax進行計算的,由於咱們這裏直接使用的是return返回,沒有涉及到臨時變量,因此不會用到緩衝區來存儲。
接下來的三步pop,是將以前存儲在棧中的元素都恢復到它原來的位置。
此時的堆棧狀況就變成了,上面的值仍是沒有清除的,它們如今已是垃圾數據的,下一次填充的時候會把它們覆蓋掉,這也就形成了能夠在其中獲取到某些程序不想讓人知道的臨時變量值。
接下來讓esp增長0c0,也就恢復到了提高堆棧以前的位置,此時esp與ebp到了一個位置。
接下來的三步操做依舊能夠忽略,它們是vs編譯器生成的,用來檢測堆棧是否平衡,若是不平衡的話在這裏就會產生報錯。
最後就是使用pop,將ebp恢復到以前的位置。
最後使用ret回到堆棧中存儲的地址,也就是call調用的下一個地址。
可是此時還有個問題,esp並無回到調用前的位置,因此堆棧仍是沒有平衡的,若是堆棧不平衡,那在不斷的執行的過程當中,就會發生堆棧溢出,這裏編譯器是使用外平棧的方式來使堆棧恢復平衡的,它在esp的基礎上增長了8。
此時堆棧也就恢復到了平衡狀態
還有另外一種方式是使用內平棧的方式,即在函數內部就將堆棧恢復平衡,使用ret 8的方式。
再日後面的操做就是main函數的堆棧平衡的處理了,與上面的函數調用相似,就不提了。
文章首發公衆號和我的博客
公衆號:無意的夢囈(wuxinmengyi)