1. 程式人生 > >X86架構上函式呼叫過程的堆疊

X86架構上函式呼叫過程的堆疊

   理解呼叫棧最重要的兩點是:棧的結構,EBP暫存器的作用。
首先要認識到這樣兩個事實:
  1、一個函式呼叫動作可分解為:零到多個PUSH指令(用於引數入棧),一個CALL指令。CALL指令內部其實還暗含了一個將返回地址(即CALL指令下一條指令的地址)壓棧的動作。
  2、幾乎所有本地編譯器都會在每個函式體之前插入類似如下指令:PUSH EBP; MOV EBP ESP;即,在程式執行到一個函式的真正函式體時,已經有以下資料順序入棧:引數,返回地址,EBP。由此得到類似如下的棧結構(引數入棧順序跟呼叫方式有關,這裡以C語言預設的CDECL為例):


+| (棧底方向,高位地址) |
  | .................... |
  | .................... |
  | 引數3             |
  | 引數2             |
  | 引數1             |
  | 返回地址         |
-| 上一層[EBP]    |  <-------- [EBP](棧幀)


  “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;
}