目录
1.先来看这样一段汇编代码,这里可以看到main函数调用的时候,首先进行push(压栈)操作:
【前言】首先,我们要清楚的认识函数栈帧的创建与销毁,为什么函数栈帧如此重要,以及它能给我们带来什么?认识到这个问题后,再来阅读此文,相信会给你们带来不一样的收获。那么本篇博文的重点在哪呢?主要是解决六个问题。
1.局部变量是怎么创建的?
2.为什么局部变量的值是随机值?
3.函数是怎么传参的?传参的顺序是怎样的?
4.形参和实参是什么关系?
5.函数调用是怎么做的?
6.函数调用结束后怎么返回的?
接下来步入正文:
从刚学习计算机的时候就已经听说过寄存器,而寄存器究竟是什么神秘人物,让我们揭开神秘人的面纱,一起来看一看他的正面目。
寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。其实寄存器就是一种常用的时序逻辑电路,但是这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的,因为一个锁存器或触发器能存储一位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址。(详细内容来自百度百科)
eax ebx ecx edx
了解函数栈帧需要了解的两个主要寄存器用于维护函数栈帧(ebp栈底指针 esp栈顶指针)
注意:每一次函数调用,都要在栈区开辟空间 。
- //函数栈帧的创建和销毁
- 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);
- }
函数栈帧的创建和销毁在不同编译器下略有差异(本篇文章的运行及调试均来自VS2013),所以我们应该尽可能的去分析实验结果,而不是纠结于为什么我的结果和别人的结果不一样?是我的编译器有问题吗?等等可能出现的问题。
压栈(push):给栈顶放一个元素。
出栈(pop):给栈顶删除一个元素。
在研究函数调用的实例时,先来观察调试状态下的调用堆栈,以及main函数是谁调用的?谁调用的?
观察调用堆栈,不难发现main函数也是被其他函数调用的,main函数是由__tmainCRTStartup调用的,而__tmainCRTStartup又是由mainCRTStartup调用完成的。栈区的使用习惯是先试用高地址,在使用低地址。函数是如何被调用的?先来看看main函数和Add函数是如何完成调用的:
在main函数调用之前,先调用__tmainCRTStartup,此时ebp和esp用来维护函数__tmainCRTStartup的栈帧,如图:
接着我们把代码转换为汇编代码,看一下main函数是怎么一步一步被调用的:
压栈操作是进栈的一种操作,在这里我们需要注意的是在压栈过程中,由于esp和ebp是用来维护函数栈帧的,而且esp是用来栈顶的寄存器,所以在压栈之后寄存器esp也会随之移动。
首先将esp的地址赋予ebp,进行复制操作后:
0E4h是一个数字,以十进制显示的话,0E4h是228,看着这个数字很小,其实这个数字的在内存中的空间还是很大的;
函数是在栈区创建的,而栈区的使用习惯是先试用高地址,在使用低地址,所以进行减法操作的时候其实就是在位main函数开辟空间,也就是说减去的这样一个十进制数字其实就是为main函数开辟的空间:
在main函数的栈帧创建之后,又进行了压栈操作,分别压入了三个值,分别是ebx、esi、edi 。
main函数初始化时所对应的汇编代码:
第一句汇编代码:lea edi,[ebp-0E4h]
意思是在edi和ebp-0E4h的空间中加载一段数据。
下面三句汇编代码:意思是将刚才加载的空间中全部初始化为CCCCCC.......
注意:由于在函数栈帧创建的过程中图片较长,所以在提到哪一部分的时候会截取所对应的片段,而不会全部截取。
初始化函数后,开始创建局部变量:
0Ah转化为十进制就是10,也就是a的值。把a的值放在了[ebp-8]这个地址所对应的空间,b(20)放在[ebp-14h]所对应的地址处;
c放在[ebp-20h]的地址处,借助于画图板能够帮助我们更好的理解:
借助于调试中的内存来观察一下:
从内存监视我们可以看出创建局部变量a之后,间隔两个整型的内存创建局部变量b,创建局部变量b后又间隔两个整型创建局部变量c,借助于画图板在刚才的图形中可以画出,更容易理解:
Add函数调用时的汇编代码:
把ebp-14h的值放在寄存器eax中,从上图中可以得出ebp-14h中存放的值正是b的值20,放入后进行压栈操作。
压栈完成后,再次进行move操作,把ebp-8(a的值10)放在寄存器ecx中,再次进行压栈操作。
那么为什么进行上面两个压栈操作呢?是不是在进行传参呢?
答案是肯定的。
call指令:调用函数
这里我们来看一个细节:
当进入call指令后内存空间的变化
这个地址是call指令后面的地址,这个内存空间是栈顶的变化,也就相当于压栈把这个地址压了进去。
进入Add函数:
Add函数栈帧的创建:
Add函数栈帧的创建和main函数的栈帧创建是相似的,以及对应的汇编指令也是相似的。这里直接画出图解:
执行Add函数:
执行到现在,还没有看到我们的x,y在哪 ,那么接着往下执行:
把ebp+8的值加到eax里面,ebp+8如图:
以及ebp+12也加到eax里面,而ebp+8和ebp+12 其实就是刚刚压栈进来的a和b,这里便于区分用a'和b'代替。
这就解释了形参是实参的一份临时拷贝的问题,在传参过程中,通过压栈将实参的值压到内存当中,进入函数需要用到形参时,通过地址找回压栈压进来的实参,改变实参不会影响实参。
结果算出来之后是怎么返回的?
函数结束之后函数内部的值是会销毁的,所以函数的值需要寄存器来保存,最后通过寄存器返回:
结果返回后空间的释放:
从上往下进行出栈pop,回收空间:
pop ebp实际上弹出栈顶的ebp,找回main函数的ebp
pop后通过call指令的下一个地址找到ma返回函数:
寄存器中保存的30在返回后move到c中去,这就是函数栈帧的创建和销毁的大概内容。
【结语】函数栈帧的创建和销毁是真正的内功,码字不易,喜欢的话就给个三连吧!