1. 程式人生 > >函式的呼叫過程(棧幀結構)—C語言版

函式的呼叫過程(棧幀結構)—C語言版

主要從三個方面來詳解棧幀

1. 在c語言中關於函式呼叫過程,用棧幀來分析

2. 可以通過棧幀來修改函式返回時的地址

3. 讓不能正常返回的函式可以返回

----------

測試環境 VS2008 win10

----------

1.函式的呼叫過程 棧幀分析(棧幀在C語言中是指每一個函式呼叫時棧區會自動地為其開闢一塊空間即為棧幀。棧幀中儲存了該函式的返回地址和區域性變數)

先來看一段程式碼:

<textarea readonly="readonly" name="code" class="C++">

#include <stdio.h>

int Add(int x,int y)
{
	int z = 0;
	z = x+y;
	return z;
}

int main()
{
	int a = 0xaaaaaaaa;
	int b = 0xbbbbbbbb;
	int ret = Add(a,b);
	printf("ret = %d\n", ret);
	return 0;
}
</textarea>

`然後一直按F10除錯到 int c =Add(a,b)這一步,同時可以在窗口裡開啟反彙編 函式中的變數都是存在於棧區中(當然了用stactic定義的變數不存於棧區),並且這些變數會一直存在於程式執行期間

----------

ebp為棧底指標,esp為棧頂指標,EIP用於存放當前指令的下一條指令的地址

----------

棧區是向下生長的(從高地址到低地址),先將ESP向下生長然後對新的變數進行壓棧。下面來看a和b的反彙編程式碼
<textarea readonly="readonly" name="code" class="彙編">
	int a = 0xaaaaaaaa;
00E9141E  mov         dword ptr [a],0AAAAAAAAh 
	int b = 0xbbbbbbbb;
00E91425  mov         dword ptr [b],0BBBBBBBBh 
	int ret = Add(a,b);
00E9142C  mov         eax,dword ptr [b] 
00E9142F  push        eax  
00E91430  mov         ecx,dword ptr [a] 
00E91433  push        ecx  
00E91434  call        @ILT+215(_Add) (0E910DCh) 
00E91439  add         esp,8 
00E9143C  mov         dword ptr [ret],eax 
</textarea>

可以看出即將呼叫Add函式時是將b先壓棧,a後壓棧,隨後呼叫call指令(call指令有兩個作用 1> 將call指令的下一條指令的地址存到棧中  2> 用目標函式的地址覆蓋EIP的值)

<img src="https://img-blog.csdn.net/2017094030746?watmark/2/text/aHR0cDovL2cuY3Nkbi5uZXQvV5bm1hbjIzMw

==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"width="40%">


然後接著用F11進入Add函式內部

<textarea readonly="readonly" name="code" class="彙編">
int Add(int x,int y)
{
00E913B0  push        ebp  
00E913B1  mov         ebp,esp 
00E913B3  sub         esp,0CCh 
00E913B9  push        ebx  
00E913BA  push        esi  
00E913BB  push        edi  
00E913BC  lea         edi,[ebp-0CCh] 
00E913C2  mov         ecx,33h 
00E913C7  mov         eax,0CCCCCCCCh 
00E913CC  rep stos    dword ptr es:[edi] 
	int z = 0;
00E913CE  mov         dword ptr [z],0 
	z = x+y;
00E913D5  mov         eax,dword ptr [x] 
00E913D8  add         eax,dword ptr [y] 
00E913DB  mov         dword ptr [z],eax
</textarea>

這段彙編是將EBP先壓棧,此時的EBP指向的是main函式的棧底,然後使ESP的內容移動到EBP中,此時相當於EBP指向了另一個棧幀的棧底,而使得ESP減去一個數,,這樣一來開闢了一塊新的棧幀,而這個新的棧幀即為Add函式的棧幀返回值z,將其先儲存到EAX暫存器裡,然後移動EBP,ESP,使得回到main函式的棧幀中。而呼叫ret時和call相反

2.可以通過棧幀來修改函式返回時的地址

<textarea readonly="readonly" name="code" class="C">
#include <stdio.h>
#include <windows.h>

void bug()
{
	printf("I am a bug");
	Sleep(3000);
}

int Add(int x, int y)
{
	int *p = &x;
	p--;
	*p = bug;
	printf("Add is a 函式\n");
}

int main()
{
	int a = 0;
	int b = 0;
	int ret = Add(a,b);

	printf("ret: %d\n", ret);
	return 0;
}
</textarea>

將call指令的下一條指令的地址用bug函式的地址覆蓋掉,達到了跳轉到bug函式執行

執行結果:

Add is a 函式

I am a bug請按任意鍵繼續

3. 讓不能正常返回的函式可以返回

在上面的 bug函式中,當bug函式呼叫完畢後不知道返回時的地址,所以我們可以在進入bug函式之前儲存一下call指令的下一條指令的地址

#include <stdio.h>
#include <windows.h>

void *ret = NULL;

void bug()
{
	int c = 0;
	int *p = &c;
	p+=2;
	*p = (int)ret;
	printf("I am a bug");
	Sleep(3000);
}

int Add(int x, int y)
{
	int *p = &x;
	p--;
	ret = *p;
	*p = bug;
	printf("Add is a 函式\n");
	return 0;
}

int main()
{
	int a = 0xaaaaaaaa;
	int b = 0xbbbbbbbb;
	int ret = Add(a,b);

    printf("main is running\n");
	return 0;
}