寄存器
在讲函数栈帧之前先来补充一下知识~
eax、ebx、ecx、edx
ebp、esp 这两个寄存器中放的是地址,这两个地址是用来维护函数栈帧的。
函数调用前的准备
在VS2013中,main函数也是被其他韩数调用的:mainCRTStartup->_tmainCRTStartup->main。
mainCRTStartup先调用_tmainCRTStartup,再通过_tmainCRTStartup调用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;
}
栈创建的实现
先来解释一下几个代码意思:
push:压栈,给栈顶放一个元素。
pop:出栈,从栈顶删除一个元素。
sub:相减运算。
mov:传值。
lea:取偏移地址。
首先进行了压栈,将栈底指针ebp压在_tmainCRTStartup开辟空间的上面。
紧接着mov后,esp指向的位置和ebp的位置相同。
sub后,esp减去一个地址后得到了新的地址,会重新指向新的地址,这个地址并且比原来变小了
push:接着压栈,将ebx、esi、edi放入栈顶。
接下来的三行的作用:
001C18BF mov ecx,9
001C18C4 mov eax,0CCCCCCCCh
001C18C9 rep stos dword ptr es:[edi]
将ebx到ebp也就是main函数内部的空间每一个地址用“cccccccc”填充。
main函数栈帧内部实现
紧接着在ebp-8的地址所指向的空间赋值a=10、ebp-16的地址所指向的空间赋值b=20、ebp-24的地址所指向的空间赋值c=0。
注意:发现每次开辟好的栈空间里的赋值都要间隔2个地址,这是在VS环境下的特点,不同编译器有不同的特点,并且为什么要这么做呢?有什么意义呢?具体详情可以看我相关的博客~
经过上述实现后:
通过调试我们发现,main结束完之后并没有直接进入到Add函数的调用。
而是进行接下来的步骤:
mov eax,dword ptr[ebp-14h]
push eax
mov ecx,dword ptr [ebp-8]
push ecx
mov:将ebp-14h地址对应的内容放到eax寄存器中
push:压栈
mov:将ebp-8h地址对应的内容放到ecx寄存器中
push:压栈
经过这样的操作后,相当于把main函数里面的b和a的值传到了新开辟的一块空间。在这里我们就可以清晰的明白,函数传值调用后,新开辟的空间用形参接收传过来的参数。原来的a和b属于实参,形参和实参属于两块不同的空间,形参的改变不会影响实参的改变。
用图来来表示这过程:
注意:从上面我们可以看出,传参比压栈先行。Add函数还没有在栈区上开辟空间,参数就已经传传给相关的寄存器指针,也就是说形参不在Add函数栈空间的内部。
再紧接着F11调试
调用call指令,call指令里面存了下一条指令的地址。这样可以保存mian函数栈帧的状态,也方便调用完Add之后返回main函数的栈帧。可以认为:现场保护和现场恢复。
接下来是Add函数内部的实现。在ebp-8的位置上对int z=0;进行开辟空间。
mov dword ptr [ebp-8],0
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0ch]
mov dword ptr [ebp-8],eax
mov:创建z
mov:取a放到eax中
add:把b加a后放到eax里面(原来eax放过b的值了)
mov:把z里面的值放到eax寄存器中,通过函数返回值eax带回。
具体图示如下:
栈帧的销毁
Add函数调用完之后开辟的空间就会销毁。
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
c
pop:edi出栈
pop:esi出栈
pop:ebx出栈
mov:ebp赋值给esp,esp指向ebp指向的位置,Add函数的栈帧就销毁了。
pop:ebp出栈
ret:ret指令会使得出栈一次,并将出栈的内容当作地址,将程序执行跳转到该地址。
ret执行完之后,汇编代码跳转到刚才call指令的下一条指令的地址。
然后打印c的值,main函数结束之后销毁main函数的栈帧.
在return 0之前的命令:add esp 8->相当于将esp向下移动两个地址。从而此时的位置是:
main函数栈帧的销毁
00C31910 pop edi
00C31911 pop esi
00C31912 pop ebx
00C31913 add esp,0E4h
00C31919 cmp ebp,esp
00C3191B call 00C31244
00C31920 mov esp,ebp
00C31922 pop ebp
类比于Add函数栈帧的销毁,对main函数栈帧空间进行销毁。
如图:
最后执行ret指令,汇编代码回到_mainCRTStartup栈帧处,函数调用全部结束。
总体的草图大致如下: