C語言函數的調用原理不理解?深入淺出通俗易懂,一文讀懂函數棧

前言

本文轉載,非原創,前面在評論區看到有同學想要學習下函數棧是什麼情況,近日出奇的有點懶,故在博客上偶爾看到一篇這樣的文章,轉載發出,希望對你們有幫助。

C函數調用過程原理及函數棧幀分析(轉)

在x86的計算機系統中,內存空間中的棧主要用於保存函數的參數,返回值,返回地址,本地變量等。一切的函數調用都要將不同的數據、地址壓入或者彈出棧。因此,為了更好地理解函數的調用,我們需要先來看看棧是怎麼工作的。

棧是什麼?

簡單來說,棧是一種LIFO形式的數據結構,所有的數據都是後進先出。這種形式的數據結構正好滿足我們調用函數的方式:父函數調用子函數,父函數在前,子函數在後;返回時,子函數先返回,父函數後返回。棧支持兩種基本操作,push和pop。push將數據壓入棧中,pop將棧中的數據彈出並存儲到指定寄存器或者內存中。

這裡是一個push操作的例子。假設我們有一個棧,其中黃色部分是已經寫入數據的區域,綠色部分是還未寫入數據的區域。現在我們將0x50壓入棧中:

<code>// 將

0x50

的壓入棧

push

$0x5

0

/<code>

C語言函數的調用原理不理解?深入淺出通俗易懂,一文讀懂函數棧

圖一:壓棧操作

我們再來看看pop操作的例子:

<code> 
pop/<code>


C語言函數的調用原理不理解?深入淺出通俗易懂,一文讀懂函數棧

圖二:出棧操作

這裡有兩點需要注意的,第一,上面例子中棧的生長方向是從高地址到低地址的,這是因為在下文講的棧幀中,棧就是向下生長的,因此這裡也用這種形式的棧;第二,pop操作後,棧中的數據並沒有被清空,只是該數據我們無法直接訪問。有了這些棧的基本知識,我們現在可以來看看在x86-32bit系統下,C語言函數是如何調用的了。

棧幀是什麼?

棧幀,也就是stack frame,其本質就是一種棧,只是這種棧專門用於保存函數調用過程中的各種信息(參數,返回地址,本地變量等)。棧幀有棧頂和棧底之分,其中棧頂的地址最低,棧底的地址最高,SP(棧指針)就是一直指向棧頂的。在x86-32bit中,我們用 %ebp 指向棧底,也就是基址指針;用 %esp 指向棧頂,也就是棧指針。下面是一個棧幀的示意圖:

C語言函數的調用原理不理解?深入淺出通俗易懂,一文讀懂函數棧

圖三:棧幀示意圖

一般來說,我們將 %ebp 到 %esp 之間區域當做棧幀(也有人認為該從函數參數開始,不過這不影響分析)。並不是整個棧空間只有一個棧幀,每調用一個函數,就會生成一個新的棧幀。在函數調用過程中,我們將調用函數的函數稱為“調用者(caller)”,將被調用的函數稱為“被調用者(callee)”。在這個過程中,1)“調用者”需要知道在哪裡獲取“被調用者”返回的值;2)“被調用者”需要知道傳入的參數在哪裡,3)返回的地址在哪裡。同時,我們需要保證在“被調用者”返回後,%ebp, %esp 等寄存器的值應該和調用前一致。因此,我們需要使用棧來保存這些數據。

函數調用實例

函數的調用

我們直接通過實例來看函數是如何調用的。這是一個有參數但沒有調用任何函數的簡單函數,我們假設它被其他函數調用。

<code>

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

); ... } /<code>

對於這個函數,當調用時,MyFunction() 的彙編代碼大致如下:

<code>_MyFunction:
    

push

%ebp ;

//

保存%ebp的值 movl %esp, $ebp ;

//

將%esp的值賦給%ebp,使新的%ebp指向棧頂 movl -

12

(%esp), %esp ;

//

分配額外空間給本地變量 movl $1

0

, -

4

(%ebp) ; movl $5, -

8

(%ebp) ; movl $2, -

12

(%ebp) ; /<code>

光看代碼可能還是不太明白,我們先來看看此時的棧是什麼樣的:

C語言函數的調用原理不理解?深入淺出通俗易懂,一文讀懂函數棧

圖四:被調用者棧幀的生成

此時調用者做了兩件事情:第一,將被調用函數的參數按照從右到左的順序壓入棧中。第二,將返回地址壓入棧中。這兩件事都是調用者負責的,因此壓入的棧應該屬於調用者的棧幀。我們再來看看被調用者,它也做了兩件事情:第一,將老的(調用者的) %ebp 壓入棧,此時 %esp 指向它。第二,將 %esp 的值賦給 %ebp, %ebp 就有了新的值,它也指向存放老 %ebp 的棧空間。這時,它成了是函數 MyFunction() 棧幀的棧底。這樣,我們就保存了“調用者”函數的 %ebp,並且建立了一個新的棧幀。

只要這步弄明白了,下面的操作就好理解了。在 %ebp 更新後,我們先分配一塊0x12字節的空間用於存放本地變量,這步一般都是用 sub 或者 mov 指令實現。在這裡使用的是 movl。通過使用 mov 配合 -4(%ebp), -8(%ebp) 和 -12(%ebp) 我們便可以給 a, b 和 c 賦值了。

C語言函數的調用原理不理解?深入淺出通俗易懂,一文讀懂函數棧

圖五:本地變量賦值後的棧幀

函數的返回

上面講的都是函數的調用過程,我們現在來看看函數是如何返回的。從下面這個例子我們可以看出,和調用函數時正好相反。當函數完成自己的任務後,它會將 %esp 移到 %ebp 處,然後再彈出舊的 %ebp 的值到 %ebp。這樣,%ebp 就恢復到了函數調用前的狀態了。

<code>

int

MyFunction

(

int

x,

int

y,

int

z )

{

int

a,

int

b,

int

c; ...

return

; } /<code>

其彙編大致如下:

<code>_MyFunction:
    

push

%ebp movl %esp, %ebp movl -

12

(%esp), %esp ... mov %ebp, %esp

pop

%ebp ret /<code>

我們注意到最後有一個 ret 指令,這個指令相當於 pop + jum。它首先將數據(返回地址)彈出棧並保存到 %eip 中,然後處理器根據這個地址無條件地跳到相應位置獲取新的指令。

C語言函數的調用原理不理解?深入淺出通俗易懂,一文讀懂函數棧

圖六:被調用者返回後的棧幀

到這裡,C函數的調用過程就基本講完了。函數的調用其實不難,只要搞懂了如何保存以及還原 %ebp 和 %esp,就能明白函數是如何通過棧幀進行調用和返回的了。

尾言

文章都是手打原創,每天最淺顯的介紹C語言、C++,windows知識,喜歡我的文章就關注一波吧,每天帶你學習C/C++不同的知識,也可以看到最新更新和之前發表的文章哦。如果足下基礎比較差,不妨關注下人人都可以學習的視頻教程

《C語言51課視頻教程合集》

《C++45課視頻教程》

通俗易懂,深入淺出,一個視頻只講一個知識點。視頻不深奧,不需要鑽研,在公交、在地鐵、在廁所都可以觀看,隨時隨地漲姿勢

專欄

C語言十大新手練手項目實戰

作者:C語言基礎

100幣

9人已購

查看

專欄

隨到隨學全套C語言入門精品文檔

作者:C語言基礎

30幣

29人已購

查看

專欄

C語言數據結構那點事兒

作者:C語言基礎

100幣

13人已購

查看


分享到:


相關文章: