在x86的計算機系統中,內存空間中的棧主要用於保存函數的參數,返回值,返回地址,本地變量等。一切的函數調用都要將不一樣的數據、地址壓入或者彈出棧。所以,爲了更好地理解函數的調用,咱們須要先來看看棧是怎麼工做的。html
簡單來講,棧是一種LIFO形式的數據結構,全部的數據都是後進先出。這種形式的數據結構正好知足咱們調用函數的方式: 父函數調用子函數,父函數在前,子函數在後;返回時,子函數先返回,父函數後返回。棧支持兩種基本操做,push和pop。push將數據壓入棧中,pop將棧中的數據彈出並存儲到指定寄存器或者內存中。sass
這裏是一個push操做的例子。假設咱們有一個棧,其中黃色部分是已經寫入數據的區域,綠色部分是還未寫入數據的區域。如今咱們將0x50壓入棧中:數據結構
// 將0x50的壓入棧 push $0x50
咱們再來看看pop操做的例子:ide
// 將0x50彈出棧 pop
這裏有兩點須要注意的,第一,上面例子中棧的生長方向是從高地址到低地址的,這是由於在下文講的棧幀中,棧就是向下生長的,所以這裏也用這種形式的棧;第二,pop操做後,棧中的數據並無被清空,只是該數據咱們沒法直接訪問。有了這些棧的基本知識,咱們如今能夠來看看在x86-32bit系統下,C語言函數是如何調用的了。函數
棧幀,也就是stack frame,其本質就是一種棧,只是這種棧專門用於保存函數調用過程當中的各類信息(參數,返回地址,本地變量等)。棧幀有棧頂和棧底之分,其中棧頂的地址最低,棧底的地址最高,SP(棧指針)就是一直指向棧頂的。在x86-32bit中,咱們用 %ebp
指向棧底,也就是基址指針;用 %esp
指向棧頂,也就是棧指針。下面是一個棧幀的示意圖:學習
通常來講,咱們將 %ebp
到 %esp
之間區域當作棧幀(也有人認爲該從函數參數開始,不過這不影響分析)。並非整個棧空間只有一個棧幀,每調用一個函數,就會生成一個新的棧幀。在函數調用過程當中,咱們將調用函數的函數稱爲「調用者(caller)」,將被調用的函數稱爲「被調用者(callee)」。在這個過程當中,1)「調用者」須要知道在哪裏獲取「被調用者」返回的值;2)「被調用者」須要知道傳入的參數在哪裏,3)返回的地址在哪裏。同時,咱們須要保證在「被調用者」返回後,%ebp
, %esp
等寄存器的值應該和調用前一致。所以,咱們須要使用棧來保存這些數據。ui
咱們直接經過實例來看函數是如何調用的。這是一個有參數但沒有調用任何函數的簡單函數,咱們假設它被其餘函數調用。spa
int MyFunction(int x, int y, int z) { int a, b, c; a = 10; b = 5; c = 2; ... } int TestFunction() { int x = 1, y = 2, z = 3; MyFunction1(1, 2, 3); ... }
對於這個函數,當調用時,MyFunction()
的彙編代碼大體以下:指針
_MyFunction: push %ebp ; //保存%ebp的值 movl %esp, $ebp ; //將%esp的值賦給%ebp,使新的%ebp指向棧頂 movl -12(%esp), %esp ; //分配額外空間給本地變量 movl $10, -4(%ebp) ; movl $5, -8(%ebp) ; movl $2, -12(%ebp) ;
光看代碼可能仍是不太明白,咱們先來看看此時的棧是什麼樣的:code
此時調用者作了兩件事情:第一,將被調用函數的參數按照從右到左的順序壓入棧中。第二,將返回地址壓入棧中。這兩件事都是調用者負責的,所以壓入的棧應該屬於調用者的棧幀。咱們再來看看被調用者,它也作了兩件事情:第一,將老的(調用者的) %ebp
壓入棧,此時 %esp
指向它。第二,將 %esp
的值賦給 %ebp
, %ebp
就有了新的值,它也指向存放老 %ebp
的棧空間。這時,它成了是函數 MyFunction()
棧幀的棧底。這樣,咱們就保存了「調用者」函數的 %ebp
,而且創建了一個新的棧幀。
只要這步弄明白了,下面的操做就好理解了。在 %ebp
更新後,咱們先分配一塊0x12字節的空間用於存放本地變量,這步通常都是用 sub
或者 mov
指令實現。在這裏使用的是 movl
。經過使用 mov
配合 -4(%ebp)
, -8(%ebp)
和 -12(%ebp)
咱們即可以給 a
, b
和 c
賦值了。
上面講的都是函數的調用過程,咱們如今來看看函數是如何返回的。從下面這個例子咱們能夠看出,和調用函數時正好相反。當函數完成本身的任務後,它會將 %esp
移到 %ebp
處,而後再彈出舊的 %ebp
的值到 %ebp
。這樣,%ebp
就恢復到了函數調用前的狀態了。
int MyFunction( int x, int y, int z ) { int a, int b, int c; ... return; }
其彙編大體以下:
_MyFunction: push %ebp movl %esp, %ebp movl -12(%esp), %esp ... mov %ebp, %esp pop %ebp ret
咱們注意到最後有一個 ret
指令,這個指令至關於 pop + jum
。它首先將數據(返回地址)彈出棧並保存到 %eip
中,而後處理器根據這個地址無條件地跳到相應位置獲取新的指令。
到這裏,C函數的調用過程就基本講完了。函數的調用其實不難,只要搞懂了如何保存以及還原 %ebp
和 %esp
,就能明白函數是如何經過棧幀進行調用和返回的了。但願這篇文章對你有幫助!
在我學習棧幀以及寫這篇文章的過程當中,參考了下面這些文章,在這我感謝他們對我提供的大力的幫助。若是你對這些文章感興趣,請訪問如下連接:
1. x86 Instruction Set Reference
2. x86 Disassembly/Functions and Stack Frames
3. x86 Assembly Guide