x86匯編之棧與子程序調用


什么是棧

棧與普通數據結構所說的棧的概念是相似的,遵循后進先出原則。不同的是匯編中所說的棧是一個在內存中連續的保存數據的區域,也即是實際存在的內存區域,進棧和出棧遵循后進先出原則。

在x86架構中,棧是向下生長的,即棧頂指針小於棧底指針。

ESP

ESP是x86架構中用於保存當前棧頂位置的寄存器。更多詳細內容請參閱參考資料[1]

下面的兩對代碼是相互等價的
入棧操作:

push eax
;修改棧頂指針
sub esp, 4  ; 由於是向下生長,所以esp - 4, 減去4是因為eax占4個字節
mov DWORD PTR SS:[ESP], eax ;放入esp指定的內存區域

出棧操作

pop eax
mov eax, dword ptr ss:[esp]
add esp, 4  ;理解同入棧,注意這兩行代碼順序與入棧不同

清除棧頂數據

假如我們要清除棧頂的四個雙字的數據,只需要修改ESP即可

add esp, 4 * 4  ; 一個雙字占4個字節,共4個雙字

EBP

棧的一個典型應用就是函數調用時的參數傳遞。ESP保存的是當前棧的棧頂指針,EBP保存的是當前stack frame的基址[2].

[3]所述,在可執行環境中函數經常以stack frame的形式來進行參數傳遞和函數局部變量的訪問。stack frame的概念使得每一個子程序(在匯編中函數通常稱為子程序)都能夠擁有獨立的棧空間。當函數被調用時,以當前esp所在位置為基址創建了stack frame,當前的esp就是stack frame的棧幀基址,在執行其他命令之前需要把棧基址保存在ebp當中。

值得注意的是棧幀的概念是邏輯上的概念,實際上並不存在。一個進程仍然只是擁有一個棧,只是為了方便子程序內部的使用而引入了棧幀的概念。

standard entry sequence

有關更多在子程序調用中如何使用棧幀概念進行子程序調用請參閱[3:1].

一般而言,在子程序中首先要執行下面一段代碼:

push ebp    ;保存主調函數的棧幀基址
mov ebp, esp    ;當前函數的棧幀基址
sub esp, X  ;X表示函數中要用到的變量大小,用於分配空間

例如一個C程序的函數:

void MyFunction()
{
    int a, b, c;
    ...
}

則對應匯編程序的進入代碼為:

_MyFunction:
    push ebp
    mov ebp, esp
    sub esp, 12 ;4 * 3, int 類型是dword

若對上面的代碼有:

a = 10;
b = 5;
c = 2;

則對應的匯編為:

mov [ebp - 4], 10
mov [ebp - 8], 5
mov [ebp - 12],2

為什么保存ebp

為了更好的理解ebp,我們考慮下面帶有參數的函數

vod MyFunction2(int x, int y, int z)
{
    ...
}

匯編代碼如下:

_MyFunction2:
    push ebp
    mov ebp, esp
    sub esp, 0; no local variables, most compilers will omit this line

當調用函數時MyFunction2(10,5, 2),在匯編中調用格式如下:

;通過棧進行參數傳遞
; 參數從右向左壓入棧,這樣第一個pop出來的數據即是第一個參數
push 2
push 5
push 10
call _MyFunction2

其中,call _MyFunction2等價於下列指令:

push eip + 2 ;return address is current address + size of two instructions
jmp _MyFunction2

進入到子程序之后就要執行entry sequence代碼:

push ebp
mov ebp, esp
sub esp, X; X為局部變量需要的字節數目

此時在棧中的內容如下:

:    : 
|  2 | [ebp + 16] (3rd function argument)
|  5 | [ebp + 12] (2nd argument)
| 10 | [ebp + 8]  (1st argument)
| RA | [ebp + 4]  (return address)
| FP | [ebp]      (old ebp value)
|    | [ebp - 4]  (1st local variable)
:    :
:    :
|    | [ebp - X]  (esp - the current stack pointer. The use of push / pop is valid now)

就目前看來似乎並沒有必要使用ebp,因為單單使用esp也能夠解決問題,但是利用esp訪問變量是不可靠的,因此需要ebp去訪問變量,因此需要保存舊的ebp的值。

Standard Exit Sequence

standard exit sequence是用於撤銷standard entry sequence的。

void MyFunction3(int x, int y, int z)
{
  int a, b, c;
  ...
  return;
}
_MyFunction3:
  push ebp
  mov ebp, esp
  sub esp, 12 ; sizeof(a) + sizeof(b) + sizeof(c)
  ;x = [ebp + 8]
  ;y = [ebp + 12]
  ;z = [ebp + 16]
  ;a = [ebp - 4] = [esp + 8]
  ;b = [ebp - 8] = [esp + 4]
  ;c = [ebp - 12] = [esp]
  mov esp, ebp ; 這一步是直接把棧頂指針指向保存返回地址的地方
               ; 直接消除了局部變量的影響
  pop ebp
  ret 12 ; sizeof(x) + sizeof(y) + sizeof(z)

參考資料


  1. x86 Disassembly/The Stack ↩︎

  2. What is between ESP and EBP? ↩︎

  3. x86 Disassembly/Functions and Stack Frames ↩︎ ↩︎


免責聲明!

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



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