函式呼叫過程原理及函式棧幀分析

2021-08-05 23:46:02 字數 3114 閱讀 7074

在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,bc賦值了。

上面講的都是函式的呼叫過程,我們現在來看看函式是如何返回的。從下面這個例子我們可以看出,和呼叫函式時正好相反。當函式完成自己的任務後,它會將%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 函式的...