学习C语言的过程中,我们经常会进行函数调用,我们对于函数调用表面现象倒是一清二楚,但是对于其具体在内存中是如何让运作,却不怎么了解,今天我们就来好好探究里面的奥妙。
想要了解函数栈帧的创建和销毁,首先必须了解以下知识点:
知识介绍:
我们电脑CPU中会有一个小型存储区域,用来暂时存放参与运算的数据和运算的结果,那就是寄存器。寄存器具有高速度,低空间的特点。寄存器有eax、ebx、ecx、edx、esi、edi、eip、esp、ebp,我们这里着重介绍esp、ebp。
esp(栈顶指针)、ebp(栈底指针)这两个寄存器中存放的是地址,而这两个地址是用来维护函数栈帧的,具体意思就是esp、和ebp之间的空间就是栈帧。
内存中有一个分区,名为栈区,函数的每一次调用,都需要在栈区上开辟空间,而这些开辟的空间就可以当作函数栈帧,而且函数栈帧随着函数的执行一般会有所变化。
栈区的中的函数和数据等是从高地址向低地址创建的,压栈就是把数据放入栈顶,出栈就是把数据取出栈顶,所以会有先入后出的特点。
int add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int sum = 0;
sum = add(a, b);
return 0;
}
我们在windows10的visual studio 2013编译器(debug、32位,以下简称vs2013)环境下进行测试。为展示效果,代码(如上图)就写的尽可能详细。
在vs2013中通过对main函数定义的查看可以了解,main函数是被一个名叫mainCRTStartup的函数调用,而mainCRTStartup又被一个名叫__tmainCRTStartup的函数调用,因此main函数也会在栈区上开辟属于自己的空间。
用一幅图来总结上面的知识。
演示函数栈帧的创建和销毁:
我们对代码进行反汇编可以看见函数栈帧创建和销毁的具体过程(如下图所示)。接下来我就把整个过程详细的讲解一番。(反汇编中的地址和栈区中的地址不一样)
1.为main()函数开辟栈帧
push压栈,esp的值减少4个字节,然后将ebp的值压入栈中。
move移动,将esp的值赋给ebp,这里并不是将esp所指向的空间赋给ebp,只是单纯的地址赋值。
sub减去,将esp的值减去0E4h字节保存在esp中,即esp向低地址移动0E4h字节。
将esp的值减少4字节,然后将ebx的值压入栈中。
将esp的值减少4字节,然后将esi的值压入栈中。
将esp的值减少4字节,然后将edi的值压入栈中。
lea将一个内存地址的值直接赋给操作数,rep能够引发其后字符串指令被重复,stos将eax的值中的值拷贝到es:[edi]指向的空间,dword双字(四个字节),ptr指针,es:[edi]指向目的串。
在栈上从edi-0E4h开始的位置,向高地址方向内存赋值0CCCCCCCCh,重复39次,每次赋值双字(4个字节)的空间。
2.main()函数创建局部变量
将0Ah赋给a所在的内存地址指向的空间
将14h赋给b所在内存地址所指向的空间
将0赋给sum所在内存地址所指向的空间
3.形参的传递
esp的值减少4个字节,将14h的值压入栈中(b的形参,即y)
esp的值减少4个字节,将0Ah的值压入栈中(a的形参,即x)
可以看出,虽然我们写的是int add(int x, int y),但是传参顺序确实从右往左,即先创建y,再创建x
,也知道形参和实参是两块不同空间储存,改变形参不会改变实参,即形参是实参的临时拷贝。
call将下一条指令的地址压入栈中(当子程序执行完之后很根据这个地址返回),并移动到调用的子程序中(跳转到后面的地址)
jump将无条件跳转到后面的地址。
4.为add()函数开辟栈帧
这里和mian()函数的函数栈帧开辟如出一辙。总的概括就是:
esp减少4字节,将ebp的值压入栈中
将esp的值赋给ebp,这里并不是将esp所指向空间的值赋给ebp
将esp的值减去0CCh字节保存在esp中,即esp向低地址移动0CCh字节。
将esp的值减少4字节,然后将ebx的值压入栈中。
将esp的值减少4字节,然后将esi的值压入栈中。
将esp的值减少4字节,然后将edi的值压入栈中。
在栈上从edi-0CCh开始的位置,向高地址方向内存赋值0CCCCCCCCh,重复33次,每次赋值双字(4个字节)的空间。
将0赋值给z所指向的双字节空间
将x所指向的双字节空间的值赋给eax
add是加法运算,将y所指向的双字节空间的值加给eax
将eax的的值赋给z所指向的双字节空间
将z所指向双字节空间的值赋给eax
5.为add()函数销毁栈帧
取出edi在栈中的值,esp的值增加4字节
取出esi在栈中的值,esp的值增加4字节
取出ebx在栈中的值,esp的值增加4字节
将ebp的值赋给esp,这里并不是将ebp所指向的内存空间的值赋给esp
取出ebp在栈中的值,esp的值增加4字节
ret会取出栈中存放的call下一条指令的地址,用于返回main()函数,esp的值增加4字节
给esp的值加上8个字节
将eax的值赋给sum所指向的双字节空间
6.为main()函数销毁栈帧
可以看出,之后就是main()函数的栈帧销毁了,这里就不详细展开讲了,留给有兴趣的老铁研究研究。
总结:
以上就是函数栈帧创建和销毁的大概流程了,看到这里,想必以前学习中的许多困惑已经有了答案了吧。
比如:
1.局部变量是怎么创建的?
2.为什么局部变量的值是随机值?
3.函数是怎么传参的?
4.传参的顺序是怎么样的?
5.形参和实参什么关系?
6.函数调用以后怎么返回的?
……