棧指針&& 幀指針詳解


一、基礎知識

幀指針使得訪問函數的參數很容易。所以任何函數調用進來的第一件事都是保護調用者的幀指針,以使得返回時可以恢復調用者的幀指針,

即pushl %ebp

   movl %esp  %ebp

有了上面這兩個命令,函數就可返回了,返回時只要

leave  或  movl %ebp &esp

               popl  %ebp

----------------------------------------------------------------

函數調用的故事

1.調用者的各種參數入棧

2.調用者的返回地址入棧,這個是用  前指令地址++  作返回地址的

3.調用者的幀指針入棧保護(本次調用的基址指針就指向這里)

4.調用者的寄存器入棧保護

5.被調用的函數的局部變量的分配

依cpu不同會有細小的差別

 

函數調用時的內存布局:

|-----參數----------|

|---返回地址--------|

|----舊的ebp--------| 新的ebp就會指向這里

|---保存的寄存器狀態|

|--本地變量---------|

所以當前ebp-4就是返回地址,當前ebp-8就是調用傳來的參數。

----------------------------

堆是可以建立很多個的,但是每個堆是有最大尺寸限制的。

內存有5種,堆,棧,靜態,常量,代碼

全局變量和靜態變量都在靜態內存里。

常量字符串都在常量區里。

 

二、棧幀結構和控制轉移權方式

大多數CPU上的程序實現使用棧來支持函數調用操作。棧被用來傳遞函數參數、存儲返回信息、臨時保存寄存器原有值以備恢復以及用來存儲局部數據。單個函數調用操作所使用的棧部分被稱為棧幀(stack frame)結構,其一般結構如下圖所示。棧幀結構的兩端由兩個指針來指定。寄存器%ebp通常用做幀指針(frame pointer),而esp則用作棧指針(stack pointer)。在函數執行過程中,棧指針esp會隨着數據的入棧和出棧而移動,因此函數中對大部分數據的訪問都基於幀指針%ebp進行。

對於函數A調用函數B的情況,傳遞給B的參數包含在A的棧幀中。當A調用B時,函數A的返回地址(調用返回后繼續執行的指令地址)被壓入棧中,棧中該位置也明確指明了A棧幀的結束處。而B的棧幀則從隨后的棧部分開始,即圖中保存幀指針(ebp)的地方開始。再隨后則用於存放任何保存的寄存器值以及函數的臨時值。

B函數同樣也使用棧來保存不能放在寄存器中的局部變量值。例如由於通常CPU的寄存器數量有限而不能夠存放函數的所有局部數據,或者有些局部變量是數組或結構,因此必須使用數組或結構引用來訪問。另外,C語言的地址操作符"&"被應用到一個局部變量上時,我們就需要為該變量生成一個地址,即為變量的地址指針分配一空間。最后,B函數會使用棧來保存調用任何其他函數的參數。

棧是往低(小)地址方向擴展的,而esp指向當前棧頂處的元素。通過使用push和pop指令我們可以把數據壓入棧中或從棧中彈出。對於沒有指定初始值的數據所需要的存儲空間,我們可以通過把棧指針遞減適當的值來做到。類似地,通過增加棧指針值我們可以回收棧中已分配的空間。

指令CALL和RET用於處理函數調用和返回操作。調用指令CALL的作用是把返回地址壓入棧中並且跳轉到被調用函數開始處執行。返回地址是程序中緊隨調用指令CALL后面一條指令的地址。因此當被調函數返回時就會從該位置繼續執行。返回指令RET用於彈出棧頂處的地址並跳轉到該地址處。在使用該指令之前,應該先正確處理棧中內容,使得當前棧指針所指位置內容正是先前CALL指令保存的返回地址。另外,若返回值是一個整數或一個指針,那么寄存器eax將被默認用來傳遞返回值。

盡管某一時刻只有一個函數在執行,但我們還是需要確定在一個函數(調用者)調用其他函數(被調用者)時,被調用者不會修改或覆蓋調用者今后要用到的寄存器內容。因此Intel CPU 采用了所有函數必須遵守的寄存器用法統一慣例。該慣例指明,寄存器eax、edx和ecx的內容必須由調用者自己負責保存。當函數B被A調用時,函數B可以在不用保存這些寄存器內容的情況下任意使用它們而不會毀壞函數A所需要的任何數據。另外,寄存器ebx、esi和edi的內容則必須由被調用者B來保護。當被調用者需要使用這些寄存器中的任意一個時,必須首先在棧中保存其內容,並在退出時恢復這些寄存器的內容。因為調用者A(或者一些更高層的函數)並不負責保存這些寄存器內容,但可能在以后的操作中還需要用到原先的值。還有寄存器ebp和esp也必須遵守第二個慣例用法。

三、函數調用舉例

作為一個例子,我們來觀察下面C程序exch.c中函數調用的處理過程。該程序交換兩個變量中的值,並返回它們的差值。

 1 void swap(int * a, int *b)
 2 {
 3   int c;
 4   c = *a; *a = *b; *b = c;
 5 }
 6 
 7 int main()
 8 {
 9    int a, b;
10    a = 16; b = 32;
11    swap(&a, &b);
12    return (a - b);
13 }
14  

其中函數swap()用於交換兩個變量的值。C程序中的主程序main()也是一個函數(將在下面說明),它在調用了swap()之后返回交換后的結果。這兩個函數的棧幀結構如圖3-5所示。可以看出,函數swap()從調用者main()的棧幀中獲取其參數。圖中的位置信息相對於寄存器ebp中的幀指針。棧幀左邊的數字指出了相對於幀指針的地址偏移值。在像gdb這樣的調試器中,這些數值都用2的補碼表示。例如,-4被表示成0xFFFFFFFC,-12會被表示成0xFFFFFFF4。

調用者main()的棧幀結構中包括局部變量a和b的存儲空間,相對於幀指針位於-4和-8偏移處。由於我們需要為這兩個局部變量生成地址,因此它們必須保存在棧中而非簡單地存放在寄存器中。

使用命令"gcc -Wall -S -o exch.s exch.c"可以生成該C語言程序的匯編程序exch.s代碼,如下所示(刪除了幾行與討論無關的偽指令)。

 1  .text
 2  _swap:
 3   pushl %ebp  # 保存原ebp值,設置當前函數的幀指針。
 4   movl %esp,%ebp
 5   subl $4,%esp  # 為局部變量c在棧內分配空間。
 6   movl 8(%ebp),%eax   # 取函數第1個參數,該參數是一個整數類型值的指針。
 7   movl (%eax),%ecx # 取該指針所指位置的內容,並保存到局部變量c中。
 8   movl %ecx,-4(%ebp)
 9   movl 8(%ebp),%eax # 再次取第1個參數,然后取第2個參數。
10   movl 12(%ebp),%edx
11   movl (%edx),%ecx  # 把第2個參數所指內容放到第1個參數所指的位置。
12   movl %ecx,(%eax)
13   movl 12(%ebp),%eax   # 再次取第2個參數。
14   movl -4(%ebp),%ecx   # 然后把局部變量c中的內容放到這個指針所指位置處。
15   movl %ecx,(%eax)
16   leave   # 恢復原ebp、esp值(即movl %ebp,%esp; popl %ebp;)。
17   ret
18  _main:
19   pushl %ebp    # 保存原ebp值,設置當前函數的幀指針。
20   movl %esp,%ebp
21   subl $8,%esp # 為整型局部變量a和b在棧中分配空間。
22   movl $16,-4(%ebp) # 為局部變量賦初值(a=16,b=32)。
23   movl $32,-8(%ebp)
24   leal -8(%ebp),%eax # 為調用swap()函數作准備,取局部變量b的地址,
25   pushl %eax # 作為調用的參數並壓入棧中。即先壓入第2個參數。
26   leal -4(%ebp),%eax   # 再取局部變量a的地址,作為第1個參數入棧。
27   pushl %eax
28   call _swap # 調用函數swap()。
29   movl -4(%ebp),%eax # 取第1個局部變量a的值,減去第2個變量b的值。
30   subl -8(%ebp),%eax
31   leave # 恢復原ebp、esp值(即movl %ebp,%esp; popl %ebp;)。
32   ret
33  

這兩個函數均可以划分成三個部分:"設置",初始化棧幀結構;"主體",執行函數的實際計算操作;"結束",恢復棧狀態並從函數中返回。對於swap()函數,其設置部分代碼是3~5行。前兩行用來設置保存調用者的幀指針和設置本函數的棧幀指針,第5行通過把棧指針esp下移4字節為局部變量c分配空間。6~15行是swap函數的主體部分。第6~8行用於取調用者的第1個參數&a,並以該參數作為地址取所存內容到ecx寄存器中,然后保存到為局部變量分配的空間中(-4(%ebp))。第9~12行用於取第2個參數&b,並以該參數值作為地址取其內容放到第1個參數指定的地址處。第13~15行把保存在臨時局部變量c中的值存放到第2個參數指定的地址處。第16~17行是函數結束部分。leave指令用於處理棧內容以准備返回,它的作用等價於下面兩個指令:

movl %ebp,%esp  # 恢復原esp的值(指向棧幀開始處)。
popl %ebp  # 恢復原ebp的值(通常是調用者的幀指針)。

這部分代碼恢復了在進入swap()函數時寄存器esp和ebp的原有值,並執行返回指令ret。

第19~21行是main()函數的設置部分,在保存和重新設置幀指針之后,main()為局部變量a和b在棧中分配了空間。第22~23行為這兩個局部變量賦值。從第24~28行可以看出,main()中是如何調用swap()函數的。其中首先使用leal指令(取有效地址)獲得變量b和a的地址並分別壓入棧中,然后調用swap()函數。變量地址壓入棧中的順序正好與函數申明的參數順序相反。即函數最后一個參數首先壓入棧中,而函數的第1個參數則是最后一個在調用函數指令call之前壓入棧中的。第29~30行將兩個已經交換過的數字相減,並放在eax寄存器中作為返回值。

從以上分析可知,C語言在調用函數時是在堆棧上臨時存放被調函數參數的值,即C語言是傳值類語言,沒有直接的方法可用來在被調用函數中修改調用者變量的值。因此為了達到修改的目的就需要向函數傳遞變量的指針(即變量的地址)。

 

 


免責聲明!

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



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