用一个小例子来说明函数形参变量、实参变量和系统堆栈的关系
#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的值入栈,再形成空栈,这样就可以在子函数调用结束后,直接回到主函数执行的下一步开始执行。