【前言】前期学习的时候,我们可能有很多的困惑
比如:
·局部变量是怎么创建的呢?
·为什么局部变量的值是随机值?
·函数是怎么传参的?传参的顺序是怎么样的?
·形参和实参是什么关系?
·函数调用是怎么做的?
·函数调用是结束后怎么返回的?
知道了函数栈帧的创建和销毁这些问题就都可以解决了,学习这个知识其实就是修炼自己的内功,也能搞懂后期的更多知识
【进入正题】:
本部分讲解使用的环境是vs2013,因为越高级的编译器,越不容易学习和观察。
同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现
一、寄存器
电脑中的任何指令都是在CPU上运行的,但是CPU本身只负责运算不负责存储,数据一般都存储在内存和寄存器(存储最常见的数据)
寄存器有eax,ebx,ecx,edx,还有本篇的重点ebp,esp
ebp(栈底指针),esp(栈顶指针)这2个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的
二、栈区
每一个函数调用,都要在栈区创建一个空间
那么什么是栈区呢?
内存四区模型:
栈区stack:由编译器自动分配和释放,存放函数的参数值(形参和返回值),局部变量的值
栈区的使用习惯是先使用高地址,再使用低地址,空间不断向上消耗,如果再使用空间,就需要在顶部放数据(栈顶),当函数调用时,esp维护的就是栈顶,ebp维护的就是栈底
【温馨提示】:这里只是以main函数开辟的空间为例,esp,ebp不只是维护main函数,还有其他的函数
【问】:那么什么又是栈帧呢?
【栈帧】:利用寄存器访问局部变量,函数的参数值等的手段,表示程序的函数调用记录
【补充】:压栈和出栈是什么?
push:压栈:从栈顶上放入元素
pop:出栈:从栈顶上删除元素
三、函数栈帧的创建
代码:为了更清晰地演示此过程,所以代码写的足够的细节
#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; }
按下F10进行逐过程调试,按F11下一步,然后打开调试-窗口-调用堆栈,随着我们一步一步地按F11逐语句调试,最后发现从return 0跳转到另一个界面,其实就是main函数的调用函数
其实main()函数是被_tmainCRTStartup函数调用,而_tmainCRTStartup函数又是被mainCRTStartup函数调用的
接下来分步骤演示函数栈帧的创建和销毁的过程
1.为main函数开辟栈帧
<0>在调用main函数之前:esp和ebp分别在栈顶和栈底维护_tmainCRTStartup函数
<1>:将ebp的值压入栈中,esp的值向低地址减小,指向esp的上端
<2>:将esp的值赋给ebp,两者同时指向esp
<3>:将esp减去0E4h并保存在esp中,即esp向低地址移动0E4h个字节(就是开始为main函数开辟空间)
<4>:将ebx,esi,edi的值分别压入栈中,注意每次压栈后,esp都会向低地址移动,最后sep就到达了edi的上方
<5>:将ebp减去0E4h的地址放到到edi中(lea:load effective address)
将39h放到ecx中去
将0CCCCCCCCh放到eax中去
<6>:将从edi(ebp-0E4h)开始的位置,重复39h次地向下(高地址方向)的内存赋值0CCCCCCCCh,每次赋值双字符(dword:double word)即四字节的空间(就是为main函数开辟的空间全部初始化为0CCCCCCCCh)
为main()函数开辟栈帧
2.在main函数中创建变量
将初始化的值赋值给内存地址为【 】中的双字符空间
eg:int a=10:将0Ah(也就是初始化的值10)放到ebp-8为内存地址的空间
结果我们可以看到三个位置分别放入了变量并初始化了,从这里我们就可以知道为什么变量不初始化就是随机值了,因为初始化,变量里面就是0CCCCCCCCh,这个值是不确定的
3.调用Add函数前的准备
<1>传参b:
(1)把地址为ebp-14h(b的地址)里面的数据(也就是b的值)赋给eax
(2)将eax的值(里面放的就是b的值20)压入栈中,esp向上(低地址)移动
<2>传参a:
(1)把地址为ebp-8(a的地址)里面的数据(也就是a的值)赋给ecx
(2)将ecx的值(里面放的就是a的值10)压入栈中,esp向上(低地址)移动
【注意】传参是从右往左传的
<3>准备调用Add函数:
(1)把call指令的下一条指令的地址压入栈中(这一步是为了调用完自定义函数后根据这个地址回到main函数去,并从这个地址向下执行),esp向上(低地址)移动
【注意】这个时候main函数的栈帧范围:栈顶esp已经到了这个地址,而栈底ebp还在下面
(2)程序进入到Add函数中去
4.为Add函数开辟栈帧
这一步和为main函数开辟栈帧一样
<1>:将ebp的值压入栈中,esp向上(低地址)移动
<2>:将esp的值赋给ebp,两者同时指向esp
<3>:将esp减去0CCh并保存在esp中,即esp向低地址移动0CCh个字节(就是开始为Add函数开辟空间)
<4>:将ebx,esi,edi的值分别压入栈中,每次压栈后,esp都会向上(低地址)移动,最后sep就到达了edi的上方
<5>:将ebp减去0CCh的地址放到到edi中
将33h放到ecx中去
将0CCCCCCCCh放到eax中去
<6>:将从edi(ebp-0CCh)开始的位置,重复33h次地向下(高地址方向)的内存赋值0CCCCCCCCh,每次赋值双字符(四字节)的空间(就是为Add函数开辟的空间全部初始化为0CCCCCCCCh)
5.在Add函数中创建变量并运算
<1>创建变量z:将0(z初始化的值)放到ebp-8为内存地址的空间
<2>进行运算:
(1)将地址为ebp+8里面的数据(也就是a)放入eax
(2)将地址为ebp+12里面的数据(也就是b)加到eax(上次的10+这次的20=30)
(3)将eax放入地址为ebp-8里面(也就是z,让z=30)
【注意】形参并不是在自定义函数里面创建的,而是回去找到调用函数之前压栈压进去值的地址
<3>返回z:将地址为ebp-8里面的数据(也就是30)放入eax(让eax读取z的值,防止函数销毁以后拿不回z的值)
四、函数栈帧的销毁
6.Add函数栈帧的销毁
<1>出栈:edi,esi,ebx分别弹出栈区,每一次出栈esp都向下(高地址)移动
<2>Add函数回收:将ebp的值赋给esp,add函数的栈就没了
<3>将栈顶main函数的ebp弹出,ebp直接回到了main函数的ebp,同时esp向下(高地址)移动4字节
<4>返回ret,程序自动返回刚才call指令的下一行,读完地址,esp又向下(高地址)移动4个字节
7.返回main函数栈帧
<1>形参x,y空间销毁:esp+8即esp向下(高地址)移动8字节,将形参x,y空间也还给操作系统
<2>把计算结果给c:把eax(Add求和的结果)的值放到以ebp-20h(c的地址)为地址的空间中
main函数的销毁和Add函数差不多,下面就不做过多的阐述
【总结】
以上就是函数栈帧的创建和销毁,可以看到这个过程非常的严谨,非常的奇妙
看到这里相信大家对前面提出的问题都已经有了答案,有些地方还是挺难理解的,有问题欢迎评论区或者私信交流,觉得笔者写的还可以,或者自己有些许收获的,麻烦铁汁们动动小手,给俺来个一键三连,万分感谢