1. 程式人生 > 其它 >c語言函式的棧幀

c語言函式的棧幀

非靜態區域性變數如何在棧上分配?c語言中的函式是如何傳引數?如何呼叫?如何返回的?

(1)、sum01.c生成32位彙編程式,進行靜態分析;

(2)、將sum01.c編譯連線成32位的可執行檔案sum01.exe,然後拖入OD軟體,在main函式入口出設定斷點,進行單步跟蹤,動態分析。

引數傳遞:通過堆疊傳遞(c語言傳參是從右向左傳);

函式呼叫:call 函式名;

函式返回:ret指令(返回值一般在EAX暫存器中);

c語言中,函式名本質就是一個地址(該函式的第一條指令在記憶體中儲存的偏移地址)。

函式名對應的地址:printf("add of sum is :%p,add of main :%p\n",sum,main);

c語言中每個函式呼叫:

(1)、傳引數:從右到左,存放到堆疊棧頂;

(2)、發出call指令:call 被呼叫函式名;(將call指令的下一條指令的地址推入堆疊棧頂,然後將被呼叫的函式的第一條指令的地址自動賦值給EIP暫存器-------段內呼叫;

如果是段間呼叫,則會自動將call指令的下一條指令的地址(段地址CS:偏移地址EIP),推入堆疊棧頂,然後將被呼叫的函式的第一條指令的地址(段地址CS:偏移地址EIP)自動賦值給相應的CS和EIP暫存器)
以下面程式碼為例子:

int sum(int x, int y)
{
	int z;

	z = x + y;

	return z;
}

int main(void)
{
	int a = 10;
	int b = 20;

	printf("sum=%d\n", sum(a, b));
	
	return 0;
}

/*彙編程式碼*/
_sum:
	push	ebp
	mov	ebp, esp
	sub	esp, 4
	mov	eax, DWORD PTR [ebp+12]
	add	eax, DWORD PTR [ebp+8]
	mov	DWORD PTR [ebp-4], eax
	mov	eax, DWORD PTR [ebp-4]
	leave
	ret
	.def	___main;	.scl	2;	.type	32;	.endef
LC0:
	.ascii "sum=%d\12\0"
.globl _main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	push	ebp
	mov	ebp, esp
	sub	esp, 24
	and	esp, -16
	mov	eax, 0
	mov	DWORD PTR [ebp-12], eax
	mov	eax, DWORD PTR [ebp-12]
	call	__alloca
	call	___main
	mov	DWORD PTR [ebp-4], 10
	mov	DWORD PTR [ebp-8], 20
	mov	eax, DWORD PTR [ebp-8]
	mov	DWORD PTR [esp+4], eax
	mov	eax, DWORD PTR [ebp-4]
	mov	DWORD PTR [esp], eax
	call	_sum
	mov	DWORD PTR [esp+4], eax
	mov	DWORD PTR [esp], OFFSET FLAT:LC0
	call	_printf
	mov	eax, 0
	leave
	ret
	.def	_printf;	.scl	2;	.type	32;	.endef

被呼叫函式內部:

(1)建立自己的棧幀底部:

push ebp				;儲存上一個棧幀的基地址(棧底地址)

mov ebp,esp		  ;讓ebp暫存器執行當前的棧幀的棧底 

(2)為函式內部定義的非靜態區域性變數分配儲存空間:

and esp ,-16   	;將棧頂指標esp進行對齊,保證能被16整除

sub esp ,32		;為函式內部的非靜態區域性變數分配儲存空間,分配的空間一般多餘區域性變數所需的,防止溢位。

(3)函式內部要呼叫別的函式:

A. 傳引數(從右向左傳,存放到堆疊棧頂) B. 發出call指令

(4)如何訪問自己的區域性變數呢?

   mov DWORD PTR [esp+28], 10  (esp+28---->某個區域性變數)
   mov DWORD PTR [esp+24], 20  (esp+24---->某個區域性變數)
   或者通過ebp來訪問傳遞給自己的引數:ebp-4--->第一個引數,ebp-8--->第二個引數

一個函式內部完整的棧幀結構:(ebp指向當前函式棧幀的底部,而esp指向當前函式棧幀的頂部)

呼叫當前函式的那個函式的棧底指標ebp(也是當前函式的棧底)。 <-----ebp(棧底)

函式內部的區域性變數。

當前函式呼叫別的函式傳的引數。

當前函式呼叫別的函式的返回地址。					<----esp(棧頂)

(5)函式結束,返回返回值(一般通過eax暫存器返回),進行平衡堆疊操作:拋棄當前的棧幀,恢復ebp和esp原來的值。

leave			(leave指令的功能:mov esp, ebp以及pop ebp,本質是拋棄當前函式棧幀,恢復上一個函式的棧幀)
ret    			(噹噹前棧頂的返回地址彈出送到eip中,如果是段間返回,則彈出棧頂到cs和eip中)


堆疊程式碼分析:

EBP=0022FFB0
ESP=0022FF84

push ebp

EBP=0022FFB0
ESP=0022FF80 --->(EBP)=0022FFB0

mov ebp, esp

EBP=0022FF80
ESP=0022FF80 --->(EBP)=0022FFB0

sub esp, 18

EBP=0022FF80
ESP=0022FF68 

and esp, fffffff0 (-16)  //地址對齊操作,讓32位地址的最右邊4位為0,也就是該地址能被16整除。

EBP=0022FF80
ESP=0022FF68 & fffffff0 = 0022ff60

mov eax, 0
MOV DWORD PTR SS:[EBP-C],EAX     ss:0022FF74--(00000000)

MOV DWORD PTR SS:[EBP-4],0A      EBP-4-->a
MOV DWORD PTR SS:[EBP-8],14      EBP-8-->b

MOV EAX,DWORD PTR SS:[EBP-8]
MOV DWORD PTR SS:[ESP+4],EAX     b--->棧頂ESP+4

MOV EAX,DWORD PTR SS:[EBP-4]     a--->棧頂ESP
MOV DWORD PTR SS:[ESP],EAX
                                 ESP = 0022ff60
CALL _sum

ESP=0022FF5C                     0022FF5C--->004012EA  函式呼叫的返回地址(call指令下一條指令的地址)
設定新的EIP為被呼叫的函式中第一條指令的地址:00401290。  00401290--->push ebp (sum函式中的第一條指令)

ESP=0022FF58    執行了PUSH EBP   EBP=0022FF80-->棧頂(儲存main函式裡的ebp,因為sum函式要建立自己的棧幀ebp)

棧幀:就是呼叫到某個函式時該函式使用的一段棧上的連續的儲存空間,ebp指向棧幀底部,esp指向棧幀頂部。

EBP=0022FF58   ESP=0022FF58  執行了MOV EBP,ESP

SUB ESP,4 實際上是為sum函式中的區域性變數z分配儲存空間。此時ESP=0022FF54 

MOV EAX,DWORD PTR SS:[EBP+C]   EBP+C--->引數y,存放的是b變數的值
ADD EAX,DWORD PTR SS:[EBP+8]   EBP+8--->引數x,存放的是a變數的值
MOV DWORD PTR SS:[EBP-4],EAX   和--->z中 0022FF54(z)<--- 30(1E)
MOV EAX,DWORD PTR SS:[EBP-4]   返回值(和30)----> EAX   (返回值一般都通過EAX暫存器返回)

LEAVE指令執行:恢復原來的棧幀(EBP、ESP):執行前EBP=0022FF58,ESP=0022FF54  執行後EBP=0022FF80,ESP=0022FF5C

此時的ESP棧頂裡存放的是返回地址004012EA(main函式中call _sum的下一條指令的地址)

執行sum函式的最後一條指令:RETN 返回指令(從棧頂彈出返回地址,送入EIP)。

MOV DWORD PTR SS:[ESP+4],EAX將返回值EAX(和30)放入棧頂(傳引數,準備執行printf函式)
MOV DWORD PTR SS:[ESP], 004012A4(字串常量"sum=%d\n"的偏移地址)放入棧頂
CALL _printf呼叫printf函式輸出計算的和。

MOV EAX,0   main函式返回給作業系統的值
LEAVE
RETN   main函式返回  return 0;