C语言的函数栈帧(动画展示详细过程)

本文通过一段简单的C语言代码,详细解析了函数调用在内存中的实现过程,包括栈帧的建立与销毁、参数传递及返回值处理等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:本篇内容通过一段简单的C语言代码带大家认识函数调用在内存中的过程。无论你是学习Java,C,C#,C++还是其他的语言,相信本篇文章都会对大家对函数调用这一过程有新的认知。

一段简单的函数调用代码

#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 10;
	int b = 20;

	int c = Add(a, b);
	return 0;
}

以上的代码就是一个简单的调用Add函数的过程,我们都知道函数的调用是在内存中的栈区上开辟空间的,那么我们通过动画的方式来为大家简单的演示一下这个过程
首先为mian函数开辟空间
创建a变量
创建b变量
为Add函数开辟空间
创建x变量
创建y变量
返回x+y的结果并将函数的返回值赋值给c
在这里插入图片描述
Add函数在调用完成之后空间就会被销毁
但是函数调用的过程其实远不如那么简单。

通过反汇编来观察

想用观察函数调用的细节,我们就不得不通过反汇编代码来进一步了解其中的过程了(建议使用低版本的VS进行观察,版本越高细节可能就会越少,不利与观察)
1.F10
2.鼠标右击,转到反汇编(记得把显示符号名关闭,利于之后的观察)
在这里插入图片描述
在这里插入图片描述
关于什么是反汇编,我们在这里不做过多的挖掘,本章的重点是通过反汇编来观察函数栈帧的过程。
我们可以看到,反汇编代码相比于我们平常看到的代码,多了很多指令,列如图中的push,mov,lea,,等等。大家不用担心,接下来我们会逐语句的进行解释。
我们先来看main函数于a之间的这一段指令动作。

在这里插入图片描述
在正式介绍之前,我们先来了解一下图中的基本含义
在这里插入图片描述
左边被红色圈住的是指令的动作,比如push是压栈的意思,sub是减的意思,,,
右边被蓝色圈住的是各个指令寄存器的名称。

这一段指令的目的就是为我们的main函数开辟空间的。讲到这里我们就先来介绍一下两个重要的指令寄存器ebp, esp
ebp和esp是维护我们函数空间的两个重要的指令,这个两个指令里存放的是地址,ebp又被称为栈底指针,esp又被称为栈顶指针(栈顶发生改变时,esp就会发生移动),也就是说我们每一次在栈区中为函数开辟空间的时候,就需要ebp和esp。如下图:
在这里插入图片描述
那么我们现在来看第一条指令 push ebp
我们首先要明白一点,main函数也是被其他函数调用的,那么在我们还未调用main函数的时候,这两个指针是在维护其他的函数的,当我们写下main函数之后,第一步就是将ebp的值push在调用main函数的函数的栈顶
然后mov ebp,esp,就是把esp的值赋值给ebp,那么ebp就指向了原本esp的位置,esp指向新的栈顶位置。如下图:
在这里插入图片描述
随后sub esp,0E4h,意思就是esp减去0E4h(这是个八进制值,由系统分配),这一步就是为main函数开辟的空间,esp减去一个值之后,就会向上发生移动(我们是由高地址向低地址)移动的大小就是0E4h。
再之后就三步相同的操作
push ebx
push esi
push edi

就是在main函数的栈顶位置,压上ebx,esi,edi这三个值。
之后的lea edi,[ebp-24h]意思就是将[ebp-24h]这个值加载到edi中去,那么[ebp-24h]是什么呢?还记得我们刚才的sub esp,0E4h吗,24h显然是小于0E4h的,那么我们就可以知道lea edi,[ebp-24h]就是将main函数中的一小段空间的地址放到了edi中去,大小就是24h。下图解释:
在这里插入图片描述

其中红色的区域就是main的空间
接下来的三段指令我放在一起解释,更有利于大家理解一些

mov ecx,9
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]

首先将9赋值给ecx,将0CCCCCCCCh 赋值给eax,然后重要的一步就是将ecx大小的空间初始化为0CCCCCCCCh 这个值,这就是上述第三条指令的操作,我们通过监视内存窗口来观察这一步骤。
在这里插入图片描述可以看到图中这九处的位置被改写成了0CCCCCCCCh!
到此我们就已经完成了对main函数空间的开辟。值得一提的是,我这里使用的是vs2019,如果使用更早版本的编译器的话(例如2013),main函数的所有空间都是会被初始化为0CCCCCCCCh的。大家感兴趣的话可以去尝试一下,但是不建议大家使用高版本的编译器,因为高版本的编译器对这一过程封装的更为严密,不利于大家观察很多的细节部分。
之后的cmp ebp,esp这条指令我们不必关心。

在这里插入图片描述
这个就要说一下了,call指令的意思是保存一个跳转指令的地址,这个跳转指令会带我们找到调用目前函数的地址。我们按F11即可观察。
在这里插入图片描述
那么如果我们在main函数当中调用其他函数时,call记录的地址就是当前main函数的地址,当被调用的函数结束调用时,就会用过这个跳转指令回到main函数。
在后面我们会在介绍这一过程,到时就会明白很多。

那么接下来就是正式开始解析我们的第一条C语言代码了。
在这里插入图片描述
这条指令就是在刚才开辟的main函数的空间内找到一个位置(即[ebp-8]),并将0Ah(十进制10)赋值给该位置,下面的b也是如此操作的。我们通过监视内存窗口来看一下这两个过程。
在这里插入图片描述
那么接下来就是调用Add函数了,调用Add函数第一步就是要进行函数的传参。
在这里插入图片描述
我们来逐个解释一下这几条指令的动作。
首先是将[ebp-14h]的值,也就是b的值(20)赋给eax,然后将eax压栈
再将[ebp-8]的值,也就是a的值(10)赋给ecx,并将ecx压栈。在这里插入图片描述

之后我们按F11进入到Add含糊的内部去看,发现前面的一部分和我们在开创main函数的时候是一样的,这里就不再解释了。
我们来着重的来看一下紫色区域的代码
在这里插入图片描述
第一条是指令是mov,意思就是,将【ebp+8】的值存放在eax寄存器中,第二条就是将【ebp+0Ch】的值加到eax寄存器中。那么我们来解释一下,为什么操作的是【ebp+8】和【ebp+0Ch】
我们看图,此时我们的Add函数已经开辟好一定的空间
在这里插入图片描述

我们在前面知道ecx中存放的是a的值(地址是【ebp-8】),那么当我们ebp+8的时候就会找到ecx,再进行减12(12的十六进制是0Ch)的时候就找到了eax,所以这俩步之后eax中存放的值就是30了。
那么接下来就是释放函数Add的空间了,大家看到这里我相信,这一过程已经对大家来说很容易了。
在这里插入图片描述
前面的pop指令就是释放掉对应得指令寄存器,没什么好说的,当栈顶发生改变时,我们的esp也会随之改变,这一点大家应该没用忘记吧!
然后的
add esp,0C0h
cmp ebp,esp

cmp的功能相当于减法指令,只是对操作数之间运算比较,不保存结果。cmp指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
这一部分我们不过多的进行挖掘,会牵扯到汇编指令的内容,在这里我们只要知道这一功能就ok了。
然后接下来的动作就很简单了,通过esp和ebp来释放掉Add的空间,并通过之前call记录的jmp指令跳转(ret的动作)到main的下一条c语句。
在这里插入图片描述

回到main函数之后就是将eax寄存器中的值放到c变量中去。
在这里插入图片描述
接下来main函数的释放(当程序结束时)也是如此的一个过程!!!!

总结

我们发现原来函数的调用是有那么多的细节的!是不是对C语言的开发者们感到敬佩!!
我们都是踩在“巨人肩膀上的”人。
所以我们在学习函数的时候,常常会说到一句话,形参是实参的一份临时拷贝,改变形参时不会改变实参的值(除非传地址)。函数在调用完成之后为什么空间释放掉了也会返回结果的值(要返回的值在exa寄存器)。
最后送大家一句话“低头赶路,敬事如仪”

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南山忆874

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值