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

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



前言

C语言的学习中,我们对函数的学习毋庸置疑是比较深刻的,而函数的调用会在我们的栈区分配出属于自己的空间 ,下面让我们来了解栈、寄存器及其常用指令、栈帧。

提示:以下是本篇文章正文内容,下面案例可供参考

一、栈是什么?

提示:以下内容来自InsCode AI助手
栈是一种数据结构,它是一种具有特定操作规则的线性表。栈的特点是只能在一端进行插入和删除操作,这一端通常被称为栈顶。栈的插入操作称为入栈(push),删除操作称为出栈(pop)。

栈的特点是先进后出(Last In First Out,LIFO)。也就是说,最后入栈的元素将第一个出栈。

栈的应用非常广泛,比如函数调用时的函数调用栈、计算机系统中的操作系统栈和进程栈、浏览器的历史记录、撤销操作等等。栈也可以用于解决某些算法问题,如递归、深度优先搜索等。
在这里插入图片描述


二、寄存器是什么?常用的寄存器有什么?

寄存器是计算机中一种用于暂时存储数据的硬件设备。

寄存器作用
EAX累加寄存器:执行算术和逻辑运算
EBX存储数据、指针和地址
ECX循环计数、存储字符串长度、传递参数和临时存储数据
EDX存储运算结果、偏移量、临时变量以及I/O端口地址
ESP指示当前栈顶
EBP指示当前栈底
ESI存储源字符串地址、数据传输、循环计数以及数据缓冲区
EDI存储目标字符串地址、数据传输、循环计数以及数据缓冲区
EIP实现程序的运行和流程控制
汇编指令作用
mov传送数据 如:mov a,b 将数据b移动到a
push进栈 如:push a 把a压入栈内
pop过程调用 如:call a 调用a
add加法运算 如:a+1 实现 a+1
sub减法 如:a-1 实现a-1
rep重复指令
ret从子程序中返回调用它的主程序
lea用于计算操作数的有效地址并将其加载到寄存器中,但不进行任何内存访问操作

三、栈帧是什么?

函数栈帧(Function Stack Frame),也称为活动记录(Activation Record)或者函数帧(Function Frame),是在程序执行过程中用来存储函数调用信息局部变量的一块内存区域

当一个函数被调用时,会创建一个对应的函数栈帧来保存函数的局部变量、参数、返回地址和其他相关的信息。函数栈帧通常包括以下几个重要的部分:

返回地址(Return Address):表示函数执行完后返回到调用该函数的地址。

参数(Arguments):保存函数调用时传递给函数的参数值。

局部变量(Local Variables):存储函数内部定义的局部变量。

临时变量(Temporary Variables):用于存储函数执行过程中临时生成的变量。

动态链(Dynamic Link):用于链接上层调用函数的栈帧。

函数栈帧在函数调用过程中会按照先进后出(Last-In-First-Out)的原则依次入栈和出栈。每次函数调用时,一个新的栈帧会被创建并入栈,函数执行完后,该栈帧会被弹出,恢复到上一层函数的执行。

下面让我们来举例理解栈帧
先看一段简单的代码:
在这里插入图片描述
代码实现了对简单加法函数的编写

在这里插入图片描述
main函数创建时的栈帧示意简图,ESP为栈顶指针,EBP为栈底指针


四、栈帧的创建过程

OK,下面我们仔细的研究一下,函数栈帧的创建与销毁
我们在VS中调试上面的代码
在这里插入图片描述
启用监视、调用堆栈、内存窗口,并转到反汇编
在这里插入图片描述
我们可以观察调用堆栈窗口
在这里插入图片描述

  • 点击显示外部代码

在这里插入图片描述
可以知道,main函数并非是直接调用的,而是由invoke_main()函数调用,我们可以找到此函数
在这里插入图片描述
在这里插入图片描述
main函数栈帧未创建时栈区如下:
在这里插入图片描述


下面我们截取main函数的汇编代码进行详细地讲解
在这里插入图片描述
获取第一个代码块
在这里插入图片描述
同时我们补充一个知识点:
ESP(或在64位模式下的RSP)是栈指针寄存器,它指向栈顶。在函数调用过程中,ESP(或RSP)用于管理栈空间,包括保存和恢复调用者的栈帧,以及传递函数参数。 EBP(或在64位模式下的RBP)是基指针寄存器,它通常用于指向当前函数的栈帧基地址。在函数调用过程中,EBP(或RBP)用于访问局部变量和函数参数。


OK,下面让我看第一条指令

00007FF7B12D18D0  push        rbp

在这里插入图片描述
下面是指令执行前后rsp的值,可以发现,rsp在rbp进行压栈后,减少了8个字节,相当于减少了一个指针的长度,也就是rsp进行了上移

在这里插入图片描述
在这里插入图片描述


同理,执行下一条指令:

00007FF7B12D18D2  push        rdi

在这里插入图片描述


执行下一条指令,进行栈顶指针压栈:

00007FF7B12D18D3  sub         rsp,148h

这时rsp - 148h,148h为16进制数字,转化为十进制为328,即rsp减去328个字节,即rsp往低地址移动,即在图中往上移动
在这里插入图片描述


执行下一条指令,调正ebp的位置

00007FF7B12D18DA  lea         rbp,[rsp+20h]

将rsp的值加上20h,然后赋值给rbp
在这里插入图片描述

这时main函数的栈帧基本创建完成


对于最后两条指令,我们就不画入图中啦,就简单介绍一下这两条指令的作用

00007FF7B12D18DF  lea         rcx,[__ECBA76DF_test@c (07FF7B12E1009h)]  
00007FF7B12D18E6  call        __CheckForDebuggerJustMyCode (07FF7B12D1370h)

00007FF7C7D918DF lea rcx,[__ECBA76DF_test@c (07FF7C7DA1008h)]
将一个字符串的地址加载到rcx寄存器中。这个字符串可能是函数的一个参数,或者用于调试信息。

00007FF7C7D918E6 call __CheckForDebuggerJustMyCode (07FF7C7D91370h)
调用__CheckForDebuggerJustMyCode函数,这个函数的地址是0x07FF7C7D91370。这个函数名暗示它可能用于检查是否有调试器附加到当前进程,以确保代码在调试模式下运行。
以上解释来自kimi AI


	int a = 10;
00007FF7B12D18EB  mov         dword ptr [a],0Ah  
	int b = 20;
00007FF7B12D18F2  mov         dword ptr [b],14h  
	int c = 0;
00007FF7B12D18F9  mov         dword ptr [c],0  

OK,我们往后介绍这三条指令,首先,我们可以通过以下操作让这三条指令更可观,取消显示符号名
在这里插入图片描述
我们得到以下新代码:

	int a = 10;
00007FF7B12D18EB  mov         dword ptr [rbp+4],0Ah  
	int b = 20;
00007FF7B12D18F2  mov         dword ptr [rbp+24h],14h  
	int c = 0;
00007FF7B12D18F9  mov         dword ptr [rbp+44h],0

观察得知,这三条指令相似,那我们就解释第一条就可以了

  1. dword ptr 指双字数据,大小为4个字节
  2. [rbp+4],0Ah 将0Ah(十进制的10)移动到rbp+4地址处

我们可以通过内存窗口查看执行后的指令
在这里插入图片描述
同理,当我执行完三条指令后,我们可以看到:
在这里插入图片描述
这是a,b,c的值就已经写入内存


接下来我们看下一段代码:
在这里插入图片描述
先看我们的一二两行代码

00007FF7B12D1900  mov         edx,dword ptr [rbp+24h]  
00007FF7B12D1903  mov         ecx,dword ptr [rbp+4]

如第一行指令,意思是,将[rbp+24]这个地方的值,储存进edx寄存器中,第二行同理,所以,我们可以看出main函数栈帧的变化,如图:
在这里插入图片描述


我们看最后一行代码:

00007FF7B12D1906  call        00007FF7B12D1348

此行的代码的意思是:执行函数调用,跳转到地址 00007FF7B12D1348 处的代码执行。这个地址是 Add 函数的入口点。
同时栈区回压入下一条指令的地址00007FF7B12D190B
在这里插入图片描述


接下来,我们会跳入Add函数里
在这里插入图片描述
经过上面的分析,下面我们画图下面代码段进行解释:

00007FF7B12D17B0  mov         dword ptr [rsp+10h],edx  
00007FF7B12D17B4  mov         dword ptr [rsp+8],ecx  
00007FF7B12D17B8  push        rbp  
00007FF7B12D17B9  push        rdi  
00007FF7B12D17BA  sub         rsp,0E8h  
00007FF7B12D17C1  lea         rbp,[rsp+20h]  
00007FF7B12D17C6  lea         rcx,[00007FF7B12E1009h]  
00007FF7B12D17CD  call        00007FF7B12D1370

前两行代码如下:
在这里插入图片描述
当指令完全执行完,栈区是这样的:
在这里插入图片描述


接下来我们讨论下一代码块

00007FF7B12D17D2  mov         eax,dword ptr [rbp+00000000000000E8h]  
00007FF7B12D17D8  mov         ecx,dword ptr [rbp+00000000000000E0h]  
00007FF7B12D17DE  add         ecx,eax  
00007FF7B12D17E0  mov         eax,ecx 

在这里插入图片描述


五.函数栈帧的销毁

接着往下看

00007FF7B12D17E2  lea         rsp,[rbp+00000000000000C8h]  
00007FF7B12D17E9  pop         rdi  
00007FF7B12D17EA  pop         rbp  
00007FF7B12D17EB  ret 

在这里插入图片描述
所以前面的内存被销毁,Add函数的栈帧也销毁了,并带回来了a+b储存在eax寄存器


下面的printf函数我们就不解释啦
在这里插入图片描述


最后,我们解释一下最后的代码:

00007FF7B12D191A  xor         eax,eax

这条指令将 eax 寄存器的值与自身进行异或操作,结果为0。这实际上是将 eax 寄存器清零。

00007FF7B12D191C  lea         rsp,[rbp+0000000000000128h]  
00007FF7B12D1923  pop         rdi  
00007FF7B12D1924  pop         rbp  
00007FF7B12D1925  ret  

第一条指令将栈指针(rsp)设置为基指针(rbp)加上0x128(即296字节)的偏移量。这通常是为了在函数返回之前释放为局部变量预留的栈空间,并恢复 rsp 到函数开始时的位置。
最后将rdi、rdp弹出,并执行ret,main函数的栈帧也随之销毁。


其他疑惑解答:

1.就算是函数中的形参储存的是实参的指针,在函数出栈结束后,形参照样会销毁
2.函数开辟的空间会不会不够?不会,因为编译器会自己计算所需空间大小
3.编译器为每个函数开辟的空间是一样的吗?不是,编译器会更具需求为函数开辟空间,是不确定的


End

感谢您的浏览,因为图片有点多,篇幅就长了一些,如果有什么错的地方,欢迎您在评论区留言!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值