函數調用時的棧狀態

右側的紅色部分,寫出了引發棧結構變化的對應的指令
+| (棧底方向,高位地址) |
| ....................|
| ....................|
                        // call somefun(...)-->修改esp,棧向下增長,參數入棧,返回值入棧
| 參數3                |
| 參數2                |
| 參數1                |
| 返回地址             |
-| 上一層[EBP]         |              // push ebp   --->修改esp,棧向下增長
| 局部變量1            |                 // sub esp 局部變量佔用空間 -->修改esp,棧向下增長
| 局部變量2            |
|.....................|
 
   補充:棧一直隨着函數調用的深入,一直想棧頂方向壓下去。每次調用函數時候,先壓函數參數(從右往左順序壓),再壓入函數調用下條指令的地址(由call 完成)。接着進入調用函數體中先執行PUSH EBP; MOV EBP ESP;(一般已經由編譯器加入到函數頭中了),接着就是吧函數體中的局部變量壓入棧中。再遇到函數的調用的嵌套則依此類推。(added by smsong)

  “PUSH EBP”“MOV EBP ESP”這兩條指令實在大有深意:首先將EBP入棧,然後將棧頂指針ESP賦值給EBP。“MOV EBP ESP”這條指令表面上看是用ESP把EBP原來的值覆蓋了,其實不然——因爲給EBP賦值之前,原EBP值已被壓棧(位於棧頂),而新的EBP又恰恰指 向棧頂。
   此時EBP寄存器就已處於一個很重要的地位,該寄存器中存儲着棧中的一個地址(原EBP入棧後的棧頂),從該地址爲基準,向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取函數局部變量值,而該地址處又存儲着上一層函數調用時的EBP值!
   一般而言,ss:[ebp+4]處爲返回地址,ss:[ebp+8]處爲第一個參數值(最後一個入棧的參數值,此處假設其佔用4字節內存),ss:[ebp-4]處爲第一個局部變量,ss:[ebp]處爲上一層EBP值。
    由於EBP中的地址處總是“上一層函數調用時的EBP值”,而在每一層函數調用中,都能通過當時的EBP值“向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取函數局部變量值”。
如此形成遞歸,直至到達棧底。這就是函數調用棧。
編譯器對EBP的使用實在太精妙了。
從當前EBP出發,逐層向上找到任何的EBP是很容易的:
unsigned int _ebp;
__asm _ebp, ebp;
while (not stack bottom)
{
    //...
    _ebp = *(unsigned int*)_ebp;
}
假如要寫一個簡單的調試器的話,注意需在被調試進程(而非當前進程——調試器進程)中讀取內存數據。

首先要認識到這樣兩個事實:
   1、一個函數調用動作可分解爲:零到多個PUSH指令(用於參數入棧),一個CALL指令。CALL指令內部其實還暗含了一個將返回地址(即CALL指令下一條指令的地址)壓棧的動作。
   2、幾乎任何本地編譯器 都會在每個函數體之前插入類似如下指令:PUSH EBP; MOV EBP ESP;
即,在程序執行到一個函數的真正函數體時,已有以下數據順序入棧:參數,返回地址,EBP。
   由此得到類似如下的棧結構(參數入棧順序跟調用方式有關,這裏以C語言默認的CDECL爲例):

////////////////////////////
注意以下幾個事實
1) 棧的變化僅當esp的值被改變

2)esp的值得變化僅當
    1.1 發生在 push,pop的時候。
    1.2 sub esp ,add esp eg: 剛進入一個函數時,爲了給局部變量分配棧空間

3) push發生在
    1)代碼中顯式的push,pop
    2)call指令會讓參數入棧調用push,會讓返回值入棧,調用push       

4) ebp的值發生僅在 剛進入一個函數時 push ebp esp

發佈了15 篇原創文章 · 獲贊 5 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章