函數調用過程棧幀變化詳解



函數調用另一個詞語表示叫作 過程。一個過程調用包括將 數據(以過程參數和返回值的形式)控制從代碼的一部分傳遞到另一部分。另外,它還必須在進入時為過程的局部變量分配空間,並在退出時釋放這些空間。
 
大多數機器,包括IA32,只提供轉移控制到過程和從過程中轉移出控制這種簡單的指令。 數據傳遞、局部變量的分配和釋放通過操縱程序棧來實現。
 
在了解本文章之前,您需要先對程序的進程空間有所了解,即對進程如何使用內存?如果你知道這些,下面的內 容將是很easy的事情了。為了您的回顧還是將簡單的分布圖貼出來,便於您的回顧。

我們先來了解一個概念,棧幀(stack frame),機器用棧來 傳遞過程參數,存儲返回信息,保存寄存器用於以后恢復,以及本地存儲為單個過程(函數調用)分配的那部分棧稱為棧幀。 棧幀其實 是兩個指針寄存器,寄存器%ebp為幀指針(指向該棧幀的最底部),而寄存器%esp為棧指針(指向該棧幀的最頂部),當程序運行時,棧指針可以移動(大多數的信息的訪問都是通過幀指針的,換句話說,就是如果該棧存在,%ebp幀指針是不移動的,訪問棧里面的元素可以用-4(%ebp)或者8(%ebp)訪問%ebp指針下面或者上面的元素)。總之簡單 一句話,棧幀的主要作用是用來控制和保存一個過程的所有信息的。棧幀結構如下所示:

此處注意:這里面有一個錯誤,即:“保存的寄存器、局部變量和臨時值”處應該是ebp-4。
棧是從高地址向低地址存儲。所以越是低的地址,越是靠后入棧。

如果你已經對這個圖已經非常了解了,那么就沒有必要再看下去了。因為下面的內容都是對這幅圖的講解。

  假設過程P(調用者)調用過程Q(被調用者),則Q的參數放在P的棧幀中。另外,當P調用Q時,P中的返回地址被壓入棧中,形成P的棧幀的末尾 (返回地址就是當程序從Q返回時應該繼續執行的地方)。Q的棧幀從保存的幀指針的值開始,后面到新的棧指針之間就是該過程的部分了。

  過程實例講解:

下面以這個程序為例進行簡要說明函數調用的基本過程。

int swap_add(int* xp,int* yp) {
    int x = *xp;
    int y = *yp;
    *xp = y;
    *yp = x;
    return x+y;
}
int caller(){
    int arg1 = 534;
    int arg2 = 1057;
    int sum = swap_add(&arg1,&arg2);
    int diff = arg1 - arg2;
    
    return sum * diff;
} 
 
經過匯編之后caller部分的代碼如下:
 
caller:
    pushl %ebp   //保存%ebp 
    movl %esp,%ebp    //設置新的幀指針為舊的棧指針
    subl $24,%esp  //分配24子節的棧空間
    movl $534,-4(%ebp) //設置arg1=534
    movl $1057,-8(%ebp) //設置arg2=1057
    leal -8(%ebp),%eax //計算&arg2
    movl %eax,4(%esp) //將&arg2存入棧中
    leal -4(%ebp),%eax //計算&arg1
    movl %eax,(%esp) //將&arg1存入棧中
    call swap_add //調用swap_add-------------------》過程調用

這段代碼先保存了%ebp的一個副本,將新的過程(該函數的ebp)的ebp設置為棧幀的開始位置。然后將棧指針減去24,從而在棧上分配了24字 節的空間(你應該思考一下為什么是24字節),然后是初始化兩個局部變量,計算兩個局部變量的地址並存入棧中,形成了函數swap_add的參數。將這些 參數存儲到相對於棧指針偏移量為0和+4的地方,留待稍后的swap_add調用訪問。然后調用swap_add.

接下的代碼是swap_add的函數部分:

swap_add:
    pushl %ebp //save old %ebp
    movl %esp,%ebp  //set %ebp as frame pointer
    pushl %ebx     //save %ebx
     
    movl 8(%ebp),%edx   //Get xp
    movl 12(%ebp),%ecx   //Get yp
    movl (%edx),%ebx   //Get x
    movl (%ecx),%eax    //Get u
    movl %eax,(%edx)    //Store y as xp
    movl %ebx,(%ecx)      //Sotre x as yp
    addl %ebx,%eax         //return value = x + y
     
    popl %ebx        //restore  %ebx
    popl %ebp        //restore %ebp
    ret        //從過程調用中返回, 將控制轉移回caller
代碼分為3部分 建立部分: 初始化棧幀 ;主體部分: 執行過程的實體計算 ;結束部分: 回復棧幀的狀態,以及過程返回 。這一部分的代碼比較簡單,就不在一一介紹,根據以上的3 部分,划分的已經很清晰了。(說明一點程序在執行到swap_add的代碼之前,也就是在執行call語句已經把返回地址壓入棧中)值得注意的是最后一部 分的popl %ebx   popl %ebp。它的作用是恢復了之前存儲的棧幀指針的值,也就是調用程序的原始棧幀指針。從而程序就可以得到返回(有些細心的人會問那返回值咋么辦?呵呵, 返 回值是存入了%eax中 ,在接下來的調用程序caller中直接訪問該寄存器就可以了)。 
 
正如前面所講的那樣,棧向低地址方向增長,而棧指針%esp指向棧頂元素,可以利用pushl將數據存入棧中並利用popl指令從棧中取出。將棧指針的值減小適當的值可以分配沒有指定初始值的數據的空間,例如:subl $24,%esp 。類似的,通過增加棧指針來釋放空間。

下面就是返回之后繼續執行的部分代碼了:

movl -4(%ebp),%edx
subl -8(%ebp),%edx
imull %edx,%eax   //為了計算diff, 
leave          //為返回准備棧,GCC 產生的代碼有時候會使用leave指令來釋放棧幀,
         //而有時會使用一個或者兩個popl指令。兩個方法都可行。
ret //從過程調用中返回

 為了計算diff,從棧中取出arg1,和arg2的值,並將寄存器%eax當做swap_add的返回值。

整個過程的棧變化如下所示:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM