形参变量、实参变量和系统堆栈的关系

用一个小例子来说明函数形参变量、实参变量和系统堆栈的关系

#include <stdio.h>

void fun1(double x) {
    printf("%lf\n", x);
}
int main() {
    int one = 1;
    int two = 2;
    int three = 3;
    
    fun1(3.14);
    printf("%d%d%d", one, two, three);
    
    return 0;
}

在这个程序中我们定义了两个简单操作的函数,首先将main()函数的汇编代码展示如下:

_one$ = -8
_two$ = -12
_three$ = -4        //这里定义了指针的偏移量
_main	PROC NEAR

	push	ebp        //首先让ebp入栈,esp的值会自动减4
			   //而调用主函数的“函数”的ebp被保护起来。
	mov	ebp, esp        //让esp 和ebp 相等,形成一个 空栈
	sub	esp, 12					; 0000000cH       
	//因为要定义三个int 类型的 变量并赋值,所以让esp减12,预留出局部变量的空间。
	mov	DWORD PTR _one$[ebp], 1        
	//间接寄存器寻址:操作数的有效地址存放在寄存器中,运算符PTR将存储器操作数类型定义为双字。
        //_one$是程序开头已经定义好的偏移量,其寻址地址是DS*10H + [ebp + (-8)]

	mov	DWORD PTR _two$[ebp], 2

	mov	DWORD PTR _three$[ebp], 3
	//将3这个立即数赋值给以 ebp-4 为首地址的int空间,说明在子函数实参和主函数局部变量之间还隔着4B的空间。
 
	push	1074339512				; 40091eb8H	//预先将实参3.14 这个双精度值 对应的浮点二进制数入栈
	push	1374389535				; 51eb851fH
	call	_fun1	//此处调用 fun1函数,这里直接跳转到fun1函数的汇编代码区(下边)
	                //call指令内部会执行push eip操作(esp会-4),然后执行mov eip,fun1 以便返回主函数之后能继续执行 add esp,8 操作.
	add	esp, 8		//esp 指针下降8

	mov	eax, DWORD PTR _one$[ebp]
	add	eax, DWORD PTR _two$[ebp]
	mov	DWORD PTR _one$[ebp], eax

	mov	ecx, DWORD PTR _three$[ebp]
	push	ecx
	mov	edx, DWORD PTR _two$[ebp]
	push	edx
	mov	eax, DWORD PTR _one$[ebp]
	push	eax
	call	_fun2
	add	esp, 12					; 0000000cH
	push	eax
	push	OFFSET FLAT:$SG361
	call	_printf
	add	esp, 8

	mov	esp, ebp
	pop	ebp
	ret	0
_main	ENDP
_TEXT	ENDS
END
第一个函数的汇编代码分析:
push	ebp		//调用新函数时又将ebp入栈,此时的ebp值,是执行main函数时的栈底,此时esp的值会减4,保护 ebp 的值不被修改。
mov	ebp, esp	//esp的值赋值给 ebp, 这里应该知道ebp是一个寄存器,执行此操作时 esp 的值并不会发生变化。

mov	eax, DWORD PTR _x$[ebp+4]	//_x$ 的值为8
push	eax
mov	ecx, DWORD PTR _x$[ebp]		
push	ecx				//eax 不够放双精度浮点数,所以ecx作为扩展寄存器
push	OFFSET FLAT:$SG355		//分两次将3.14的浮点二进制入栈

call	_printf				//调用 printf 函数
add	esp, 12					; 0000000cH		
//给esp + 12,即 栈顶指针下降。
pop	ebp		//弹出ebp的值,程序又回到主函数执行的位置
ret	0		//ret具体执行 mov eip。栈顶指针回落,回到主函数被中断处

总结:1、实参表达式的值入栈所占用的空间,就是对应的形参变量的空间

          2、局部变量的空间,是在这个函数的空栈的基础上,向上“长”出的空间。

          3、函数在调用子函数时,首先将eip的值入栈 ,再将ebp的值入栈,再形成空栈,这样就可以在子函数调用结束后,直接回到主函数执行的下一步开始执行。              

猜你喜欢

转载自blog.csdn.net/weixin_41249411/article/details/80157246