一、基本概念

想要徹底的理解函數調用過程,先要明白一下幾個概念。

1、棧

這裏說的棧不是數據結構中的棧,而是計算機內存中的一塊存儲區,它的訪問方式是“先進後出”。大多數情況下,棧是從高地址向低地址增長的。

棧有很多單元格,通常情況下每個單元格是8位的(即可以存8個0或1),稱爲數據寬度,是用來存放數據的。每個單元格都會對應一個地址,地址一般是無符號32位的整數,因此可以表示4294967295(32位無符號整數可以表示的最大值)個單元格。

關於棧的操作涉及到兩個寄存器,即ESP和EBP(什麼是寄存器這裏就不多說了),另外還有兩個指令,POP和PUSH。

首先說一下兩個寄存器,這兩個寄存器分別稱爲棧指針寄存器和基址指針寄存器,因此這兩個寄存器中存的是指針,即地址(這個地址就是前面說到的單元格對應的地址),更確切的說應該是棧幀頂部的地址(ESP)和棧幀底部的地址(EBP)(棧幀是什麼後面會講到)。

下面是一個棧的結構圖:

可以看到,棧是從高地址向低地址增長的,高地址對應棧底,低地址對應棧頂,每個存放數據的單元格都有一個對應的地址,每個單元格的寬度爲8位。

下面說一下POP和PUSH指令,這兩個指令都只能操作棧頂,PUSH是向棧中推入一個數放在棧頂,POP是將棧頂的數據彈出。無論是推入新的數據還是彈出數據,ESP始終指向棧頂,也就意味着ESP會不斷改變。

注意到上面我寫了個棧幀,下面解釋一下棧幀是什麼。

2、棧幀

棧幀是一塊連續的棧區(注意看上面的圖),每個函數都有自己的棧區,這個棧區就稱爲棧幀。注意,函數調用所佔用的棧區才稱爲棧幀,並且每個函數都會有自己的棧幀,包括main函數。當前棧幀的範圍在EBP和ESP指向的區域之間。

當然棧幀不是固定不變的,由於推入新的數據和彈出數據ESP都會改變,因此棧幀的大小也會隨之改變,但是EBP一般不會改變。

那麼什麼時候會改變EBP?

前面說到,每個函數都有自己的棧幀,也就是有自己的EBP和ESP,那麼當一個函數調用另外一個函數的時候,第二個函數也應該有自己的棧幀,也有自己的EBP和ESP,但是系統中只有一個相應的寄存器(即只有一個ESP寄存器和一個EBP寄存器)。因此這個時候就要改變EBP的值,在改變EBP的值之前把舊的EBP的值保存一下就可以了(這個後面還會講到)。

3、保存寄存器

爲什麼要保存寄存器?

首先,寄存器數量有限,因此寄存器是被所有的函數共享的。假設現在有A、B兩個函數,A函數調用了B函數。A在調用過程中往EAX中存放了數據x,B在調用過程中往EAX存放了新的數據y,那麼當B函數調用結束返回到A函數後,A繼續使用EAX中的值,但這個時候EAX中的值已經不是最初存的x了。

怎麼保護?

方法很簡單,在B函數使用EAX寄存器之前,在準備階段(剛進入B函數時)先將寄存器中的值保存到棧中,用完以後,在結束階段再從棧中將值重新寫入EAX中。

注:並不是所有通用寄存器中的值都由被調用函數保存,通常調用函數保存一部分,被調用函數保存一部分。IA-32規定,寄存器EAX、ECX、EDX是調用者保存寄存器,寄存器EBX、ESI、EDI是被調用者保存寄存器。

二、函數調用大體流程

假設有兩個函數A和B,A函數調用B函數,調用B函數需要傳入兩個參數x和y。

1:在A調用B函數之前,A先把需要傳入B函數中的參數x,y推入棧中。(注意x,y被放在了A的棧幀中)

注:上面一句步驟沒有說先把調用者保存寄存器推入棧中,事實上並不是每次調用函數都要把調用者保存寄存器推入棧中,只有在必要的時候纔會進行保護(編譯器自己決定)。

2:在執行函數調用的時候(即執行call指令),A把B函數的返回地址推入棧中(還是在A的棧幀中)。

3:在進入B函數後,推入舊的EBP的值,這時ESP指向的單元格中的內容是舊的EBP,然後令EBP等於當前的ESP,則這個EBP即爲B函數的棧幀的棧底,EBP指向的單元格中的數據是舊的EBP。這裏比較抽象,下面有一個圖可以幫助理解。

(上圖有個地方寫錯了,EBP應該是B函數棧幀的棧底)

4:開闢一塊相對合適的空間用來存放非靜態局部變量(存放非靜態局部變量前會先置成cc)。

5:將被調用者保存寄存器中的內容推入棧中進行保護。

6:執行函數中的內容。

7:函數調用完畢,return。返回的時候將EDI、ESI、EBX依次彈出,然後讓ESP指向EBP,將ESP指向的單元格中的內容(即舊的EBP)彈到EBP中,這樣EBP又重新變成了A的棧幀的棧底,執行ret指令,彈出B的返回地址,然後ESP根據參數的個數加上相應的數使ESP指向原來A的棧幀的棧頂(一個參數加4,兩個加8,以此類推)。這裏有一點需要注意,ESP的值雖然改變了,但是棧中B的參數還存在,但是無所謂,在推入新的數據的時候就會被覆蓋掉。

注意上圖ESP = ESP+8後ESP指向了原來A棧幀的棧頂,但是B的參數還存在,不過當有新數據進來的時候就會被覆蓋掉。

這裏有個問題,B函數的參數在A函數的棧幀中,B函數應該怎麼去讀取使用呢?

注意推入參數和函數返回地址後就改變了EBP,因此EBP和函數的參數緊挨着(也就是說參數在B能夠訪問到的地方),那麼怎麼去訪問呢?拿上面的例子講,參數x在EBP+8中,參數y在EBP+12中。

博主最近比較忙,最後三部分抽時間再更。

三、用VC++6.0查看反彙編詳解函數調用

四、用VC++6.0調試講解函數遞歸調用

五、微軟的內存保護機制

查看原文 >>
相關文章