大家在学C的时候是否有过这些疑问?
1.局部变量是如何创建的?
2.为什么局部变量不初始化内容是随机的?
3.函数调用时参数时如何传递的?
4.传参的顺序是怎样的?
5.函数的形参和实参分别是怎样实例化的?
6.函数的返回值是如何带会的?
想要了解函数栈帧,就要先了解这些问题,想要了解这些问题就要了解寄存器...
是不是感觉一环套一环啊?
直接上干货~
先要了解这两个寄存器
1.ebp 2.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);
return 0;
}
一下就是main函数在栈区上开辟的空间、
这块空间就被称为栈帧
那么这块空间是怎么被维护的呢?
就是由ebp和esp维护起来的
什么意思呢?
就是说在我当前去调用这个main函数的时候,这时候就会为main函数分配空间,而这块空间就由这个esp和ebp来维护,正在调用哪个函数,哪个函数就由esp和sbp来维护
如上代码
如果调用Add函数,ebp和esp就会去维护他的栈帧
esp叫栈顶指针(存放被维护空间的栈帧顶部地址)
ebp是栈底指针
栈区是由高地址向低地址处使用空间
这是要明白的基本前提~
如果像在编译器上看到现象的话,可以去打开调用堆栈
(这里作者用的VS2022)
这里其实可以看到main函数被调用了,可困惑的是,他被谁调用了呢?
继续往下走可以看到是mainCRTStartup等函数逐个调用
在主函数里的return 0可以看看出,他的返回值交给了__scrt_common_main——seh这个函数(转到反汇编里依然可以看到)
也就是说,其实main也是被其他函数调用的,并且将返回值交给调用main函数的函数
那么在main函数被调用之前,也会为调用main函数的函数在栈区上开辟一块空间
如下图红色框框:
如果在进入Add函数的时候,栈上也会开辟一块空间给他,这就是Add函数的栈帧
如下图蓝色框:
这就是大致逻辑,具体是什么样呢?
咱们深入剖析(这里由于新版本编译器 如VS2022 不能更好的反应其内部情况,这里用VS2010)
这回进入汇编代码,来一探究竟
为了更好的观察内存布局(地址),可以去掉显示符号名
刚刚我们看到在进入main函数之前也会调用到其他函数,那么此时,esp和edp维护的空间就是他
这副图的前提是,下面是高地址,上面是低地址,因为栈区内存的使用原则是高地址向低地址,所以一会向上使用空间
接下来看会汇编代码
这里有个 push ebp ,push的意思就是压栈,也就是说,把ebp的地址压入到栈区中
如下图蓝色框
因为esp是维护栈顶的所以此时esp的位置也会发生变化
上面是低地址,所以此时esp的地址会变小
下来看mov
这条指令的意思是把esp的值赋给ebp
此时,ebp将不再指向原来的位置,而是和esp指向同一位置,如下图
下来看sub
sub是减的意思,就是给esp减上一个0E4h的值,这个值是一个8进制数字
那么,esp的地址将会减少,也就是说,他会指向上方和原来位置相差0E4h距离的位置
其实这个时候,esp,ebp已经不再维护原来调用main函数的函数的空间,他有了新的维护空间,这块空间实际上就是main函数的栈帧
接下来又遇到了3个push
也就是压栈压了三个值ebx,esi,edi,同时,esp作为栈顶维护空间,也会变化,移向栈顶
继续往下分析
这里有个lea,实际上他是这几个单词的缩写load effecitive address(加载有效地址)
显示符号名可以看到
这里ebp - 0E4h,相当于给ebp减去了0E4h这么多的地址,就和之前的sub一样
然后再将地址赋给edi
此时,ebp 就指向了紫色箭头的位置
接下里看这三步
两个mov ,第一个是把39h这个值赋给ecx,第二个是把0CCCCCCCCh这个值赋给eax
但其实真正产生效果的是下面这个地方
他是干甚的呢?
他其实是把从edi开始向下39h(ecx)的空间全部赋值成eax,也就是赋值成0CCCCCCCCh这个值
word表示2个字节,dword表示双字节,也就是四个字节,每次向下将修改4个字节的内容
向下执行可以看见数据已被修改
也就是这个样子
这时,其实才完成了main函数栈帧的开辟
接下来才是真正实现有效代码
int a = 10;转化成汇编代码就是他下面这段代码
意思就是将0Ah这个值放到ebp-8的这个位置的地址处去
以上就是a 的实际位置
不知道大家以前有没有用printf这个函数打印出“ 烫烫烫 ”实际上就是打印的cccccccc也就是随机值。
小端这里显示放入a变量如下图
接下来就是把b放入内存中了
这里就是把14h这个值放入到ebp - 14h这个地址处
至于他为什么存放在这个位置,这都取决于编译器
接着存放c变量
就是把0这个值放在ebp - 20h这个地址处
这时候abc这三个变量是如何创建想必心里因该有谱了
所以局部变量是如何创建的呢?
就是在栈上为函数开辟一块空间,也就是这个函数的栈帧,然后继续在这个函数的栈帧内部找空间开辟,来存放局部变量
当main函数创建好了以后,就因该调用Add函数了
第一段代码,mov作用是把ebp-14h地址(这里的[ ]表示地址)指向的值放到eax里面去,还记得eax是什么吗?
eax实际上就是b啊,显然这里在实现传参
然后push,这里就是压栈,将eax压入栈顶
同时这里esp也会发生变化
下一步mov是把ebp-8这个位置处的地址指向的内容放到ecx里面去 ,ebp-8实际上指向的就是a,
那么现在ecx里面的值就是10
下一步
继续push,把ecx压入栈顶
这些个过程实际上就是函数传参的过程
下一步call指令实际上就是在调用
看一下红色框框起来的call指令的地址
并且可以看到a值地址的上方
值变成了00C21450,实际上就是call指令的下一条指令的地址,将他压栈
为什么要这么做呢?
其实走到call指令的时候,程序的执行就已经进入到了Add函数里面,可Add函数执行完了因该怎么回到main函数继续执行呢,这里的00c21450实际上就记住了Add执行完下一步的地址,帮助找回main函数下一步执行的程序
此时,来到Add函数的内部
有没有发现,int z = 0;以上的内容和main函数初始化的内容是一样的,实际上这里就是为Add函数创建栈帧
接下来创建z
将0这个值放到ebp-8指向的那块空间
下来执行计算:
这里有人可能就疑惑了,这x,y在哪创建的啊,这怎么执行计算啊?
别急,继续往下看
mov,将ebp+8指向的值放到eax里面去
ebp+8实际上就是指向a 的值:10
add,给eax处的值加上ebp+0Ch指向的值,ebp+0Ch指向的就是b的值:20
mov,将eax的值放入到ebp-8指向的空间,ebp-8就是z啊
仔细回想一下刚刚的过程,我们完成这一系列的过程,在进入Add函数以后,真的有去创建x ,y吗?
并没有,再执行z = x + y;这条指令的时候,是怎么找到x,y的值的?就是通过ebp+上一个值,找到在还没有调用Add函数之前就已经压栈压进去的ecx和eax,并且不难发现Add(a,b)传参的时候是从右向左依此压栈,先传b,再传a,归根结底这里就是对原来main函数里面的实参a,b进行的一份临时拷贝并压入栈中
想象一下,这里你改变形参会影响实参a,b吗?并不会,因为这时形参已经有了自己独立的空间,寄存器ecx和寄存器eax
#形参是实参的一份临时拷贝
目前只是调用并计算了这些值,还没有返回,怎么返回呢?而且出了Add函数这些值不就销毁了吗?
我们看下一步
return z; 意思就是把ebp-8 (刚刚计算得到的z的值) 指向的值放到寄存器eax里面去,为什么要放在eax里面去呢,因为一会出了函数z就销毁了,而存进eax里就不会,为什么呢?这里可是寄存器,并不会因为程序退出而销毁
下一步
return z;完了以后出现了三个pop ,pop的意思是 弹出(出栈),也就是说,这里将edi,esi,ebx的值全部弹出栈栈区,此时esp也就会发生变化
接下来Add函数已经完成了它的使命,那么这块空间就会被回收,怎么回收呢,看下一条指令
这里mov,就是把ebp赋给esp,那么esp就不会指向原来的空间,而是和ebp指向同一空间,
下一步
pop这里将ebp弹出空间,这里的ebp存的是main函数的地址
什么意思呢,也就是之前的存的ebp就被释放掉了,同时这个指向ebp的指针也会从这个main函数的地址,回到原来他指向的位置
此时就顺顺利利的回到了原来main函数的栈帧
下一条指令ret
意思就是返回,那么程序的下部该返回到哪里去呢?
大家是否还记得
main函数的栈顶上出了有刚刚的main函数的ebp被弹出,下一条就是之前call指令 ,而这条指令完成的任务就是将call指令的下一条指令的地址存起来,怎么回去呢,此时这里正好存放这这条地址,而ret就是要返回到call指令所指向的下一条指令的地址处去
现在因该就可以明白了当时为什么要存这条地址,不仅要走出去,还要回得来
这就是函数栈帧创建与销毁的一个严谨的过程,
找回到原来的地址,esp也将指向下一个位置
此时ecx和eax的值还有用吗?看下一条汇编指令
这里将esp的地址加8,esp将向下挪动8个字节,那么此时ecx,eax也将还给操作系统
现在形参怎么销毁,什么时候销毁,想必心里也因该清楚了
下一条指令
mov将eax的值放在了ebp-20h指向的空间位置处,c变量不就是在ebp-20h地址处吗,
所以此时,Add函数处理的值也就回到了c变量处
以上就是Add函数的创建和销毁的全过程,那么想必因该也能明白了main函数的创建与销毁过程.
开头的问题应该也就清楚了.