【C语言】函数栈帧的创建与销毁

人生之路不会是一帆风顺的,我们会遇上顺境,也会遇上逆境,在所有成功路上折磨你的,背后都隐藏着激励你奋发向上的动机,人生没有如果,只有后果与结果,成熟,就是用微笑来面对一切小事。

导读:本篇文章讲解 【C语言】函数栈帧的创建与销毁,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

大家在学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函数在栈区上开辟的空间、

这块空间就被称为栈帧

【C语言】函数栈帧的创建与销毁

那么这块空间是怎么被维护的呢?

【C语言】函数栈帧的创建与销毁

 就是由ebp和esp维护起来的

什么意思呢?

就是说在我当前去调用这个main函数的时候,这时候就会为main函数分配空间,而这块空间就由这个esp和ebp来维护,正在调用哪个函数,哪个函数就由esp和sbp来维护

如上代码

如果调用Add函数,ebp和esp就会去维护他的栈帧

esp叫栈顶指针(存放被维护空间的栈帧顶部地址)

ebp是栈底指针

栈区是由高地址向低地址处使用空间

这是要明白的基本前提~

如果像在编译器上看到现象的话,可以去打开调用堆栈

(这里作者用的VS2022)

【C语言】函数栈帧的创建与销毁

 这里其实可以看到main函数被调用了,可困惑的是,他被谁调用了呢?

【C语言】函数栈帧的创建与销毁

 继续往下走可以看到是mainCRTStartup等函数逐个调用

在主函数里的return 0可以看看出,他的返回值交给了__scrt_common_main——seh这个函数(转到反汇编里依然可以看到)

也就是说,其实main也是被其他函数调用的,并且将返回值交给调用main函数的函数

那么在main函数被调用之前,也会为调用main函数的函数在栈区上开辟一块空间

如下图红色框框:

【C语言】函数栈帧的创建与销毁

如果在进入Add函数的时候,栈上也会开辟一块空间给他,这就是Add函数的栈帧

如下图蓝色框:

【C语言】函数栈帧的创建与销毁

 这就是大致逻辑,具体是什么样呢?

咱们深入剖析(这里由于新版本编译器 如VS2022 不能更好的反应其内部情况,这里用VS2010)

这回进入汇编代码,来一探究竟

【C语言】函数栈帧的创建与销毁

为了更好的观察内存布局(地址),可以去掉显示符号名

刚刚我们看到在进入main函数之前也会调用到其他函数,那么此时,esp和edp维护的空间就是他

【C语言】函数栈帧的创建与销毁

 这副图的前提是,下面是高地址,上面是低地址,因为栈区内存的使用原则是高地址向低地址,所以一会向上使用空间

接下来看会汇编代码

【C语言】函数栈帧的创建与销毁

这里有个 push  ebp ,push的意思就是压栈,也就是说,把ebp的地址压入到栈区中

如下图蓝色框

 【C语言】函数栈帧的创建与销毁

 因为esp是维护栈顶的所以此时esp的位置也会发生变化

【C语言】函数栈帧的创建与销毁

上面是低地址,所以此时esp的地址会变小

下来看mov

【C语言】函数栈帧的创建与销毁

 这条指令的意思是把esp的值赋给ebp

此时,ebp将不再指向原来的位置,而是和esp指向同一位置,如下图

 【C语言】函数栈帧的创建与销毁

下来看sub

 【C语言】函数栈帧的创建与销毁

 sub是减的意思,就是给esp减上一个0E4h的值,这个值是一个8进制数字

那么,esp的地址将会减少,也就是说,他会指向上方和原来位置相差0E4h距离的位置

【C语言】函数栈帧的创建与销毁

其实这个时候,esp,ebp已经不再维护原来调用main函数的函数的空间,他有了新的维护空间,这块空间实际上就是main函数的栈帧

 【C语言】函数栈帧的创建与销毁

 接下来又遇到了3个push 

【C语言】函数栈帧的创建与销毁

也就是压栈压了三个值ebx,esi,edi,同时,esp作为栈顶维护空间,也会变化,移向栈顶

【C语言】函数栈帧的创建与销毁

继续往下分析

【C语言】函数栈帧的创建与销毁

 这里有个lea,实际上他是这几个单词的缩写load effecitive address(加载有效地址)

显示符号名可以看到

【C语言】函数栈帧的创建与销毁

 这里ebp – 0E4h,相当于给ebp减去了0E4h这么多的地址,就和之前的sub一样

然后再将地址赋给edi

此时,ebp 就指向了紫色箭头的位置

【C语言】函数栈帧的创建与销毁

接下里看这三步

【C语言】函数栈帧的创建与销毁

 两个mov ,第一个是把39h这个值赋给ecx,第二个是把0CCCCCCCCh这个值赋给eax

但其实真正产生效果的是下面这个地方

【C语言】函数栈帧的创建与销毁

 他是干甚的呢?

【C语言】函数栈帧的创建与销毁

 他其实是把从edi开始向下39h(ecx)的空间全部赋值成eax,也就是赋值成0CCCCCCCCh这个值

word表示2个字节,dword表示双字节,也就是四个字节,每次向下将修改4个字节的内容

【C语言】函数栈帧的创建与销毁

向下执行可以看见数据已被修改

也就是这个样子

【C语言】函数栈帧的创建与销毁

 这时,其实才完成了main函数栈帧的开辟

接下来才是真正实现有效代码

【C语言】函数栈帧的创建与销毁

 int a = 10;转化成汇编代码就是他下面这段代码

 意思就是将0Ah这个值放到ebp-8的这个位置的地址处去

【C语言】函数栈帧的创建与销毁

 以上就是a 的实际位置

【C语言】函数栈帧的创建与销毁

 不知道大家以前有没有用printf这个函数打印出“ 烫烫烫 ”实际上就是打印的cccccccc也就是随机值。

小端这里显示放入a变量如下图

【C语言】函数栈帧的创建与销毁

接下来就是把b放入内存中了

【C语言】函数栈帧的创建与销毁

 这里就是把14h这个值放入到ebp – 14h这个地址处

【C语言】函数栈帧的创建与销毁

至于他为什么存放在这个位置,这都取决于编译器

接着存放c变量 

 【C语言】函数栈帧的创建与销毁

就是把0这个值放在ebp – 20h这个地址处

【C语言】函数栈帧的创建与销毁

这时候abc这三个变量是如何创建想必心里因该有谱了 

所以局部变量是如何创建的呢?

就是在栈上为函数开辟一块空间,也就是这个函数的栈帧,然后继续在这个函数的栈帧内部找空间开辟,来存放局部变量

当main函数创建好了以后,就因该调用Add函数了

【C语言】函数栈帧的创建与销毁

 第一段代码,mov作用是把ebp-14h地址(这里的[   ]表示地址)指向的值放到eax里面去,还记得eax是什么吗?

【C语言】函数栈帧的创建与销毁

eax实际上就是b啊,显然这里在实现传参

【C语言】函数栈帧的创建与销毁

 然后push,这里就是压栈,将eax压入栈顶

【C语言】函数栈帧的创建与销毁

 同时这里esp也会发生变化

【C语言】函数栈帧的创建与销毁

下一步mov是把ebp-8这个位置处的地址指向的内容放到ecx里面去 ,ebp-8实际上指向的就是a,

那么现在ecx里面的值就是10

下一步

【C语言】函数栈帧的创建与销毁

 继续push,把ecx压入栈顶

【C语言】函数栈帧的创建与销毁

这些个过程实际上就是函数传参的过程

下一步call指令实际上就是在调用

【C语言】函数栈帧的创建与销毁

 看一下红色框框起来的call指令的地址

并且可以看到a值地址的上方

【C语言】函数栈帧的创建与销毁

值变成了00C21450,实际上就是call指令的下一条指令的地址,将他压栈

【C语言】函数栈帧的创建与销毁

 【C语言】函数栈帧的创建与销毁

 为什么要这么做呢?

其实走到call指令的时候,程序的执行就已经进入到了Add函数里面,可Add函数执行完了因该怎么回到main函数继续执行呢,这里的00c21450实际上就记住了Add执行完下一步的地址,帮助找回main函数下一步执行的程序

此时,来到Add函数的内部 

【C语言】函数栈帧的创建与销毁

有没有发现,int z = 0;以上的内容和main函数初始化的内容是一样的,实际上这里就是为Add函数创建栈帧

 【C语言】函数栈帧的创建与销毁

接下来创建z

 【C语言】函数栈帧的创建与销毁

 将0这个值放到ebp-8指向的那块空间

【C语言】函数栈帧的创建与销毁

 下来执行计算:

【C语言】函数栈帧的创建与销毁

 这里有人可能就疑惑了,这x,y在哪创建的啊,这怎么执行计算啊?

别急,继续往下看

mov,将ebp+8指向的值放到eax里面去

【C语言】函数栈帧的创建与销毁

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进行的一份临时拷贝并压入栈中

【C语言】函数栈帧的创建与销毁

 想象一下,这里你改变形参会影响实参a,b吗?并不会,因为这时形参已经有了自己独立的空间,寄存器ecx和寄存器eax

#形参是实参的一份临时拷贝

目前只是调用并计算了这些值,还没有返回,怎么返回呢?而且出了Add函数这些值不就销毁了吗?

我们看下一步

【C语言】函数栈帧的创建与销毁

 return z; 意思就是把ebp-8 (刚刚计算得到的z的值) 指向的值放到寄存器eax里面去,为什么要放在eax里面去呢,因为一会出了函数z就销毁了,而存进eax里就不会,为什么呢?这里可是寄存器,并不会因为程序退出而销毁

下一步

【C语言】函数栈帧的创建与销毁

return z;完了以后出现了三个pop ,pop的意思是 弹出(出栈),也就是说,这里将edi,esi,ebx的值全部弹出栈栈区,此时esp也就会发生变化

【C语言】函数栈帧的创建与销毁

接下来Add函数已经完成了它的使命,那么这块空间就会被回收,怎么回收呢,看下一条指令

【C语言】函数栈帧的创建与销毁

 这里mov,就是把ebp赋给esp,那么esp就不会指向原来的空间,而是和ebp指向同一空间,

下一步

 【C语言】函数栈帧的创建与销毁

 pop这里将ebp弹出空间,这里的ebp存的是main函数的地址

【C语言】函数栈帧的创建与销毁

什么意思呢,也就是之前的存的ebp就被释放掉了,同时这个指向ebp的指针也会从这个main函数的地址,回到原来他指向的位置

【C语言】函数栈帧的创建与销毁

 此时就顺顺利利的回到了原来main函数的栈帧

【C语言】函数栈帧的创建与销毁

 下一条指令ret

【C语言】函数栈帧的创建与销毁

 意思就是返回,那么程序的下部该返回到哪里去呢?

大家是否还记得

main函数的栈顶上出了有刚刚的main函数的ebp被弹出,下一条就是之前call指令 ,而这条指令完成的任务就是将call指令的下一条指令的地址存起来,怎么回去呢,此时这里正好存放这这条地址,而ret就是要返回到call指令所指向的下一条指令的地址处去

现在因该就可以明白了当时为什么要存这条地址,不仅要走出去,还要回得来

【C语言】函数栈帧的创建与销毁

这就是函数栈帧创建与销毁的一个严谨的过程,

找回到原来的地址,esp也将指向下一个位置

【C语言】函数栈帧的创建与销毁

 此时ecx和eax的值还有用吗?看下一条汇编指令

【C语言】函数栈帧的创建与销毁

这里将esp的地址加8,esp将向下挪动8个字节,那么此时ecx,eax也将还给操作系统

【C语言】函数栈帧的创建与销毁

 现在形参怎么销毁,什么时候销毁,想必心里也因该清楚了

下一条指令

【C语言】函数栈帧的创建与销毁

 mov将eax的值放在了ebp-20h指向的空间位置处,c变量不就是在ebp-20h地址处吗,

所以此时,Add函数处理的值也就回到了c变量处

 以上就是Add函数的创建和销毁的全过程,那么想必因该也能明白了main函数的创建与销毁过程.

开头的问题应该也就清楚了.

码字不易~

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/124395.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!