什麼是棧
棧與普通資料結構所說的棧的概念是相似的,遵循後進先出原則。不同的是彙編中所說的棧是一個在記憶體中連續的儲存資料的區域,也即是實際存在的記憶體區域,進棧和出棧遵循後進先出原則。
在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)