函数栈帧
目录
- 函数开辟内存空间
- 函数变量的创建
- 函数传参
- 函数调用
- 函数退换内存空间
1,函数开辟空间
我们都知道程序是先从main开始执行的,所以先位main函数开辟内存,其实在main函数前也需要调用main函数,在这里就不过多的阐述。
开辟内存空间,需要俩个寄存器来维护,分别是esp和ebp,它俩分别在main函数内存空间的开头和结尾。
写一个小程序,帮助大家理解
#include<stdio.h>
int add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = add(a, b);
printf("%d\n", c);
return 0;
这段反汇编代码就是为内存开辟空间,我们逐步分析。
首先,未调用main函数前如图:
执行 push ebp
push 就是在栈顶上压栈,压栈也意味着指向栈顶的esp也往上走了一位,如图:
执行 mov ebp,esp
mov 就是将 逗号后的值赋给前面,所以就是ebp=esp,所以现在ebp和esp指向了同一个位置。如图:
执行 sub esp,0E4h
sub 就是前面的值,变成 减去逗号后的值 的值,所以esp会向上走0E4h个单位,地址是从高处向低处用,这样就开辟了main的内存空间。如图:
接下来是三个push在栈顶压了ebx,esi,edi。
执行 lea edi,[ebp-0E4h]
lea指令和mov有些类似,大家注意区别,mov是将内容赋给前面,lea是将有效地址,赋给前面。
然后是 两个mov 分别给ecx和eax存了内容,ecx记录循环的次数,eax装的是初始化的内容。
接下来注意是执行 rep stos dword ptr es:[edi]
这个操作就是初始化内存空间的内容,将edi到ebp的内容全部初始化成0cccccch。效果如图:
这个就是mian函数内存的开辟。
函数变量的创建
这段汇编代码就是函数变量的创建。
dword ptr [a]意思就是访问指针,[]里放的地址。所以用mov 指令就将变量的值赋好了。可以看看变量的地址在哪。
可以清楚的看到,在ebp-8的位置创建了变量a,需要注意的是,并不是所有的函数都是在ebp-8的位置创建第一个变量,这取决于编译器。
函数传参
这段反汇编代码就是将a,b的值分别存在了寄存器ecx和eax,然后再压栈到栈顶。细心的老铁可以发现,ebp-14h就是c的地址,ebp-8是b的地址。
接下来是call 001211EA ,就是将这个地址放到栈区。
现在其实已经传参了,就是两个参数存到了寄存器里,它的地址就是ebp-8,和ebp-14h。
函数调用
在本程序里,是调用add函数,汇编代码如下:
可以很容易的发现前面的一大部分就是为函数add开辟内存空间。然后是创建了变量 z。然后就是加法的实现,可以看到是利用eax来计算,然后讲eax的值赋给z,因为ebp的值变为esp所以参数的地址的表现形式和之前不同。然后返回z的值。
可以看到又将z的值存进了寄存器eax中。
函数退还内存空间
这是add函数的销毁,pop和push相反,它是将栈顶的元素弹出,并且esp向下指一个单位。弹出栈顶的edi,esi,ebx,将ebp的值赋给esp所以esp和ebp都指向了add函数的底部,再pop ebp,执行ret返回了。
esp+8就是那两个形参的销毁,从这里可以看出形参是实参的临时拷贝。这样就是完全的add函数退还内存。
小结
大家可以写个小程序,自己调试的看看,可以帮助理解函数压栈。再补充点,ret就是返回下一个单元的地址,在main函数的call里我们压进去一个地址,那个地址就是ret要返回的地址。所以这逻辑是很严谨的。还有在add函数一上来就push了一个ebp这是保存main函数的ebp,这就是为啥,pop ebp,它就会回到main函数底部的原因。