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
观察得知,这三条指令相似,那我们就解释第一条就可以了
- dword ptr 指双字数据,大小为4个字节
- [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
感谢您的浏览,因为图片有点多,篇幅就长了一些,如果有什么错的地方,欢迎您在评论区留言!