1. 程式人生 > >棧指標&& 幀指標詳解

棧指標&& 幀指標詳解

Hugh

棧指標&& 幀指標詳解

一、基礎知識

幀指標使得訪問函式的引數很容易。所以任何函式呼叫進來的第一件事都是保護呼叫者的幀指標,以使得返回時可以恢復呼叫者的幀指標,

即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值,設定當前函式的幀指標。(執行完此指令後,棧指標esp會向下移動,指向儲存原ebp的空間的地方)
 4   movl %esp,%ebp  (把資料(原ebp的值)壓入棧後,棧指標esp會向下移動4個位元組指向放原ebp的值的空間,執行此句指令的意義就
                      是把esp指向的地址賦值給%ebp,%ebp作為新棧:_swap棧的棧底指標或者說是新棧的幀指標ebp使用)
 5   subl $4,%esp  # 為區域性變數c在棧內分配空間。
 6   movl 8(%ebp),%eax   # 取函式第1個引數,該引數是一個整數型別值的指標。(%eax作為傳遞引數的中間暫存器)
 7   movl (%eax),%ecx # 取該指標所指位置的內容,並儲存到區域性變數c中。
 8   movl %ecx,-4(%ebp)  //完成這一步:c = a* 9   movl 8(%ebp),%eax # 再次取第1個引數&a,然後取第2個引數&b。
10   movl 12(%ebp),%edx
11   movl (%edx),%ecx  # 把第2個引數所指內容b放到第1個引數所指的位置。
12   movl %ecx,(%eax)  //&a作為地址指向的空間a中 = b 即:(a* = b*)
13   movl 12(%ebp),%eax   # 再次取第2個引數&b。
14   movl -4(%ebp),%ecx   # 然後把區域性變數c中的內容放到這個指標所指位置處。
15   movl %ecx,(%eax)  //&b作為地址指向的空間b中 = c 即:(b* = c)
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在棧中分配空間。(因為變數a首先出現,所以先給a分配空間,再給b 分配空間,也就是
                                                       //執行int a; int b;這兩條語句)
22   movl $16,-4(%ebp) # 為區域性變數賦初值(a=16,b=32)。//先給a的空間賦值
23   movl $32,-8(%ebp)  //再給b的空間賦值
24   leal -8(%ebp),%eax # 為呼叫swap()函式作準備,取區域性變數b的地址,(把記憶體單元-8(%ebp)的地址傳送給暫存器%eax,此時
                                                                      幀指標esp指向了-12%ebp處)
25   pushl %eax # 作為呼叫的引數並壓入棧中。即先壓入第2個引數。(執行此指令後,幀指標esp的指向由原來的-8%ebp指向
                                                                //了-12%ebp處,儲存了第2個引數&b)
26   leal -4(%ebp),%eax   # 再取區域性變數a的地址,作為第1個引數入棧。
27   pushl %eax  (執行此指令後,幀指標esp的指向由原來的-8%ebp指向了-16%ebp處,儲存了第1個引數&a,接下來就要呼叫
                   //函式swap()了)28   call _swap # 呼叫函式swap()。
29   movl -4(%ebp),%eax # 取第1個區域性變數a的值,減去第2個變數b的值。
30   subl -8(%ebp),%eax  (subl S,D 即:D = D – S)
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語言是傳值類語言,沒有直接的方法可用來在被呼叫函式中修改呼叫者變數的值。因此為了達到修改的目的就需要向函式傳遞變數的指標(即變數的地址)。