一、相关的几个名词解释。
- ebp:栈底指针。
- esp:栈顶指针。(函数再次进行使用是在此上面加入使用的)
- call:用于保存当前指令的下一条指令并跳转到目标函数。
- push:入栈。(压栈)
- pop:出栈。
- mov:类似于赋值操作。
- add:加法操作。
- sub:减法操作。
- ecx : 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
- eax:是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
- esi/edi:分别叫做"源/目标索引寄存器"(source/destination index)。
- ret:使得出栈一次,并将出栈的内容当作地址。将程序执行跳转到该地址处。
寄存器:
eax ebx ecx edx ebp esp
ebp esp 这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。
二、函数调用实例
这里我们使用的环境是vs2013(在不同编译器下,函数调用过程中函数栈帧的创建是略有差异的)
接下来用以下代码来分析:
#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;
}
1、 从main函数的地方开始,要展开main函数的调用就得为main函数创建栈帧,那我们先来看main函数栈帧的创建。
【思考】
那么创建之后怎么维护的呢?
是由ebp和esp寄存器维护的。ebp和esp之间的空间就是调用mian函数时为其分配的空间(哪个正在被调用就去维护哪个)
【思考】
main函数是不是也被其他函数调用了呢?
我们接下来F10进入调试,可以在调用堆栈里面看到main函数被调用了:
那么被谁调用了呢?
继续按F10往下,我们发现在这调用了main函数:
往上找,我们发现这个函数名是:所以main函数是被_tmainCRTStartup函数调用的,再网上看,而_tmainCRTStartup函数是被mainCRTStartup()函数调用的。
所以,在main()函数调用之前,也为_tmainCRTStartup函数和mainCRTStartup()函数分配了空间。
2、接下来在反汇编里面看具体创建过程:(查看时把显示符号名去掉,这样可以直接看到地址)
我们知道,在mian()也为_tmainCRTStartup函数和mainCRTStartup()函数分配了空间,
(压栈:给栈顶放一个元素 出栈:从栈顶分离一个元素)
(1)首先先push进行压栈,那么此时esp指向的是压栈之后的顶端位置(此时esp的地址要减少,因为向上是低地址)
(2)接着mov,将esp给到ebp,这时候ebp的指向不再是原位置,而是指向了esp所在位置
(3)接着sub减码,给esp减去0E4h(16进制数字),那么esp指向的位置发生改变,此时esp和ebp之间的空间是为mian函数开辟的空间。
(4)接着三个push进行压栈,esp指向的是压栈之后的顶端位置
(5)接着lea加载,把后面的有效地址加载到edi里面(地址不好之间观察我们可以右击显示符号名,可以看到是ebp-0E4h),意思就是找到了sub减码之后的esp指向的地址。
(6)接着三步,mov把39h放到ecx里面去,接着下一步mov把0CCCCCCCCh放到eax里面
(7)再接下来一步,rep stos意思是从edi开始向下的39h这么多个dword的数据改为0CCCCCCCCh(1个word 2个字节,dword 四个字节)我们在内存中可以看到,全部都被初始化为了cc cc cc。
到此,为main函数栈的开辟以及完成,现在就要走正式的代码了。
(8)接下来将0Ah(10)放到ebp-8的位置,将14h 放到ebp-14h的位置,将0放到ebp-20h的位置。
(9)接下来调用Add函数:
c=Add()调用函数:
首先,将ebp-14h位置的元素放到eax里面,压栈,接着把ebp-8位置元素放到ecx里面,压栈,那么esp向上指向ecx位置。
接着,call调用函数,(按F11进入函数)
会发现内存中放的是007B1450,是这条指令的下一条指令,我们可以看到进入call函数之后调用了Add函数,之后调用完会退出找到下一条指令,这样call执行完之后会找到这个地址,再接着执行。
进入Add函数里面,
接着,再顶上压一个ebp(来自于main函数的),此时esp向上挪,再将esp给ebp,再sub给esp减去0CCh,此时esp指向减去之后的最顶端,这是ebp和esp之间的空间就是为Add函数分配空间。
接着,ebx,esi,edi压栈。
接下来,加载(过程和为创建main函数相似)
再接着创建变量z:将0放到ebp-8的位置上去,
接着,把ebp+8的值放到eax中,此时eax为10,再把ebp+0Ch加到eax中,此时eax为30。
接着,再把eax放到ebp-8中去,z即为30.
接着,往回返,把ebp-8的值放到eax寄存器中(不会销毁),即把30返回。
接着这一部分:
pop是弹出,将edi,esi,ebx弹出,这时esp指向
接着把ebp赋给esp,这时esp指向以下箭头处(Add函数栈帧销毁)
接着把ebp pop出去,即ebp找到(指向)main函数处的ebp,此时的esp位于:
即回到main函数里面。
接着ret指令就是从栈顶弹出(POP)call指令的下一条地址,来到call指令的下一条地址。
之后:
接着esp+8,则形参x,y的空间还给操作系统:
接着把eax的值放到ebp-20h:
所以将结果赋值给c。
以上就是函数栈帧的创建与销毁过程。