函式呼叫中棧的妙用

2021-08-22 12:56:33 字數 3658 閱讀 8044

理解呼叫棧最重要的兩點是:棧的結構,ebp暫存器的作用。

首先要認識到這樣兩個事實:

2、幾乎所有本地編譯器都會在每個函式體之前插入類似如下指令:push ebp; mov esp ebp;

+| (棧底方向,高位位址) |

| .................... |

| .................... |

| 引數3 |

| 引數2 |

| 引數1 |

| 返回位址 |

-| 上一層[ebp] | <-------- [ebp]

「push ebp」「mov ebp esp」這兩條指令實在大有深意:首先將ebp入棧,然後將棧頂指標esp賦值給ebp。「mov ebp esp」這條指令表面上看是用esp把ebp原來的值覆蓋了,其實不然——因為給ebp賦值之前,原ebp值已經被壓棧(位於棧頂),而新的ebp又恰恰指向棧頂。

此時ebp暫存器就已經處於乙個非常重要的地位,該暫存器中儲存著棧中的乙個位址(原ebp入棧後的棧頂),從該位址為基準,向上(棧底方向)能獲取返回位址、引數值,向下(棧頂方向)能獲取函式區域性變數值,而該位址處又儲存著上一層函式呼叫時的ebp值!

一般而言,ss:[ebp+4]處為返回位址,ss:[ebp+8]處為第乙個引數值(最後乙個入棧的引數值,此處假設其占用4位元組記憶體),ss:[ebp-4]處為第乙個區域性變數,ss:[ebp]處為上一層ebp值。

由於ebp中的位址處總是「上一層函式呼叫時的ebp值」,而在每一層函式呼叫中,都能通過當時的ebp值「向上(棧底方向)能獲取返回位址、引數值,向下(棧頂方向)能獲取函式區域性變數值」。

如此形成遞迴,直至到達棧底。這就是函式呼叫棧。

編譯器對ebp的使用實在太精妙了。

從當前ebp出發,逐層向上找到所有的ebp是非常容易的:

unsigned int _ebp;

__asm _ebp, ebp;

while (not stack bottom)

如果要寫乙個簡單的偵錯程式的話,注意需在被除錯程序(而非當前程序——偵錯程式程序)中讀取記憶體資料。

c**:

int fun(int para)

void main()

彙編**:

1:2:

3: int fun(int para)

4: 0040b831 pop edi

0040b832 pop esi

0040b833 pop ebx

0040b834 mov esp,ebp

0040b836 pop ebp

0040b837 ret

10:11: void main()

12:

0040b7b2 pop edi

0040b7b3 pop esi

0040b7b4 pop ebx

0040b7b5 add esp,40h

0040b7b8 cmp ebp,esp

0040b7ba call __chkesp (004010a0)

0040b7bf mov esp,ebp

0040b7c1 pop ebp

0040b7c2 ret

歸納呼叫fun的過程為:

(1)引數入棧

(2)呼叫fun,保護斷點,eip入棧(內部完成,無彙編**)

(3)保護暫存器ebp

(4)定位新的ebp,此後ebp一般不變

(5)保護ebx,esi,edi

(6)初始化40bytes間隔空間和fun內部變數

此過程,資料空間變化如下:

(1)引數入棧

|低位址|...|引數值|...|高位址| ...

^esp指向此 ^ebp指向某一位置

(2)呼叫fun,保護斷點,eip入棧(內部完成,無彙編**)

|低位址|...|fun返回函式位址|引數值|...|高位址| ...

^esp指向此 ^ebp指向某一位置

(3)保護暫存器ebp

|低位址|...|ebp入棧值|fun返回函式位址|引數值|...|高位址| ...

^esp指向此 ^ebp指向某一位置

(4)定位新的ebp,此後ebp一般不變

|低位址|...|ebp入棧值|fun返回函式位址|引數值|...|高位址| ...

^esp和ebp指向此

(5)保護ebx,esi,edi

|低位址|...|edi入棧值|esi入棧值|ebx入棧值|40bytes間隔空間|fun內部變數值|ebp入棧值|fun返回函式位址|引數值|...|高位址| ...

^esp指向此 ^ebp指向(ebp入棧值)

(6)初始化40bytes間隔空間和fun內部變數

曾看到一程式:

#include "stdio.h"

int fun()

int main(int argc, char* argv)

下面是其中兩段彙編**:

15: int i=1;

0040b558 mov dword ptr [ebp-4],1

16:17: fun();

0040b55fcall @ilt+10(fun) (0040100f)

18: i++;

0040b564mov eax,dword ptr [ebp-4]

0040b567add eax,1

0040b56a mov dword ptr [ebp-4],eax

5: int a=0;

0040b508 mov dword ptr [ebp-4],0

6: int* p=&a;

0040b50f lea eax,[ebp-4]

0040b512 mov dword ptr [ebp-8],eax

7: p=p+2;

0040b515 mov ecx,dword ptr [ebp-8]

0040b518 add ecx,8

0040b51b mov dword ptr [ebp-8],ecx

8: *p=*p+3;

0040b51e mov edx,dword ptr [ebp-8]

0040b521 mov eax,dword ptr [edx]

0040b523 add eax,3

0040b526 mov ecx,dword ptr [ebp-8]

0040b529 mov dword ptr [ecx],eax

9:10: return 0;

0040b52b xor eax,eax

11: }

其資料空間為:

|低位址|...|40bytes間隔空間|p的值|a的值|ebp入棧值|fun返回函式位址|引數值|...|高位址| ...

^ebp指向|ebp入棧值|

由於p=&a,p指向|a的值|,則執行p=p+2後,p指向|fun返回函式位址|

跟蹤程式|fun返回函式位址|=0040b564,執行的*p=*p+3,|fun返回函式位址|=0040b567

即fun返回後程式從位址0040b567執行,跳過0040b564處的**mov eax,dword ptr [ebp-4],使得結果i=1,而不是i=2。

函式呼叫棧

當程式進行函式呼叫的時候,系統會用到下面三種暫存器 3.ebp ebp暫存器裡儲存的是棧基址,是在函式呼叫之前,由esp賦值給ebp的。棧底方向,高位位址 call fun arg1,arg2,arg3 修改esp,棧向下增長,引數入棧,返回位址入棧 arg3 arg2 arg1 返回位址 上一層e...

棧 函式呼叫

編譯以下程式,分析此程式以得出棧的精髓 1 主函式被上層呼叫者呼叫後,執行push ebp,esp 4 因為ebp入棧 ebp值沒有改變,值得注意的是剛開始分配站的時候,第乙個入棧的是return,主函式的返回位址,然後是ebp 2 然後是mov ebp,esp,將esp的值賦給ebp,該語句未執行...

函式呼叫 函式棧

函式呼叫大家都不陌生,呼叫者向被呼叫者傳遞一些引數,然後執行被呼叫者的 最後被呼叫者向呼叫者返回結果,還有大家比較熟悉的一句話,就是函式呼叫是在棧上發生的,那麼在計算機內部到底是如何實現的呢?對於程式,編譯器會對其分配一段記憶體,在邏輯上可以分為 段,資料段,堆,棧 段 儲存程式文字,指令指標eip...