在x86的計算機系統中,記憶體空間中的棧主要用於儲存函式的引數,返回值,返回位址,本地變數等。一切的函式呼叫都要將不同的資料、位址壓入或者彈出棧。因此,為了更好地理解函式的呼叫,我們需要先來看看棧是怎麼工作的。
簡單來說,棧是一種lifo形式的資料結構,所有的資料都是後進先出。這種形式的資料結構正好滿足我們呼叫函式的方式: 父函式呼叫子函式,父函式在前,子函式在後;返回時,子函式先返回,父函式後返回。棧支援兩種基本操作,push和pop。push將資料壓入棧中,pop將棧中的資料彈出並儲存到指定暫存器或者記憶體中。
這裡是乙個push操作的例子。假設我們有乙個棧,其中黃色部分是已經寫入資料的區域,綠色部分是還未寫入資料的區域。現在我們將0x50壓入棧中:
// 將0x50的壓入棧
push $0x50
我們再來看看pop操作的例子:
// 將0x50彈出棧
pop
這裡有兩點需要注意的,第一,上面例子中棧的生長方向是從高位址到低位址的,這是因為在下文講的棧幀中,棧就是向下生長的,因此這裡也用這種形式的棧;第二,pop操作後,棧中的資料並沒有被清空,只是該資料我們無法直接訪問。有了這些棧的基本知識,我們現在可以來看看在x86-32bit系統下,c語言函式是如何呼叫的了。
棧幀,也就是stack frame,其本質就是一種棧,只是這種棧專門用於儲存函式呼叫過程中的各種資訊(引數,返回位址,本地變數等)。棧幀有棧頂和棧底之分,其中棧頂的位址最低,棧底的位址最高,sp(棧指標)就是一直指向棧頂的。在x86-32bit中,我們用%ebp
指向棧底,也就是基址指標;用%esp
指向棧頂,也就是棧指標。下面是乙個棧幀的示意圖:
一般來說,我們將%ebp
到%esp
之間區域當做棧幀(也有人認為該從函式引數開始,不過這不影響分析)。並不是整個棧空間只有乙個棧幀,每呼叫乙個函式,就會生成乙個新的棧幀。在函式呼叫過程中,我們將呼叫函式的函式稱為「呼叫者(caller)」,將被呼叫的函式稱為「被呼叫者(callee)」。在這個過程中,1)「呼叫者」需要知道在**獲取「被呼叫者」返回的值;2)「被呼叫者」需要知道傳入的引數在**,3)返回的位址在**。同時,我們需要保證在「被呼叫者」返回後,%ebp
,%esp
等暫存器的值應該和呼叫前一致。因此,我們需要使用棧來儲存這些資料。
我們直接通過例項來看函式是如何呼叫的。這是乙個有引數但沒有呼叫任何函式的簡單函式,我們假設它被其他函式呼叫。
int
myfunction
(int x, int y, int z)
inttestfunction
()
對於這個函式,當呼叫時,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) ;
光看**可能還是不太明白,我們先來看看此時的棧是什麼樣的:
此時呼叫者做了兩件事情:第一,將被呼叫函式的引數按照從右到左的順序壓入棧中。第二,將返回位址壓入棧中。這兩件事都是呼叫者負責的,因此壓入的棧應該屬於呼叫者的棧幀。我們再來看看被呼叫者,它也做了兩件事情:第一,將老的(呼叫者的)%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 )
其彙編大致如下:
_myfunction:
push %ebp
movl %esp, %ebp
movl -12(%esp), %esp
...mov %ebp, %esp
pop %ebp
ret
我們注意到最後有乙個ret
指令,這個指令相當於pop + jum
。它首先將資料(返回位址)彈出棧並儲存到%eip
中,然後處理器根據這個位址無條件地跳到相應位置獲取新的指令。
到這裡,c函式的呼叫過程就基本講完了。函式的呼叫其實不難,只要搞懂了如何儲存以及還原%ebp
和%esp
,就能明白函式是如何通過棧幀進行呼叫和返回的了。希望這篇文章對你有幫助!
函式呼叫過程及棧幀分析
linux核心程式boot head.s執行完基本初始化操作之後,就會跳轉去執行init main.c程式。那麼head.s程式是如何把執行控制轉交給init main.c程式的呢?即匯程式設計序是如何呼叫執行c語言程式的?這裡我們首先描述一下c函式的呼叫機制 控制權傳遞方式,然後說明head.s程...
函式呼叫過程(棧幀)
眾所周知,程式每呼叫乙個函式,系統都會為其開闢一塊空間,當它返回時,才收回這塊空間。程式崩潰有一部分原因就是因為無限制的呼叫函式,卻沒有及時返回,導致記憶體空間不夠。為了更好的維護這一塊空間 通常稱為棧空間 我們需要了解兩個暫存器,乙個為 esp 指向棧頂的指標 乙個為 ebp 指向棧底的指標 棧空...
函式棧幀(呼叫過程)
函式棧幀就是在呼叫函式是為其在棧空間上開闢了一段空間,指向過程呼叫,乙個過程呼叫包括將資料 以過程引數和返回值的形式 和控制從 的一部分傳遞到另一部分。我們以以下 為例講解整個函式呼叫過程 int my add int x,int y int main 一 呼叫main 函式 我們從main 函式的...