函数栈帧的创建与销毁

在这里插入图片描述

本期介绍🍖
主要介绍:在C语言中函数到底是如何实现调用函数的,以及调用的整个过程,局部变量是如何以及在哪创建的,为何局部类变量未初始化就会赋随机值,函数传参的顺序以及存放空间👀。



1. 前言

  大家在学习C语言的过程中,或多或少会产生下面几个疑惑:

  1. 局部变量在内存中是如何存放的?
  2. 为什么局部变量在没有初始化的时候会自动附上随机值?
  3. 函数是怎么传参的?传参的顺序又是何如?
  4. 都说形参是实参的一份临时拷贝,那为什么会这么说呢?
  5. 函数调用的整个过程又是怎样的呢?

  在正式详细讲解之前,需要补充一些知识点。首先,栈是高地址向低地址的延申,也就是说在栈区中一般会先使用高地址再使用低地址。其次,每一个函数的调用都会在栈区中开辟一块空间,而这块空间就被称为该函数的函数栈帧,这个栈帧中维持着各种所需的信息:函数的参数、局部变量、调用结束后跳转的地址等等。最后,需要了解两个寄存器,分别为esp和ebp(其中esp存放的是当前栈帧的栈顶地址,ebp存放的是当前栈帧的栈底地址),函数的栈帧就是靠这两个寄存器来维护的。注意:esp和ebp一定维护的是当前正在被调用的函数的函数栈帧,如下图所示:
在这里插入图片描述


2. 寄存器的种类与功能

寄存器功能
eax

累加寄存器,相对于其他寄存器,在运算方面比较常用。

ebx

基地址寄存器,在内存寻址时存放基地址。

ecx

计数寄存器,用于循环操作,比如重复的字符存储操作或者数字统计。

edx

作为eax的溢出寄存器,总是被用来存放整数除法产生的余数。

esi

源变址寄存器,主要用于存放存储单元在段内的偏移量,通常在内存操作指令中作为“源地址指针”使用

edi

目的变址寄存器,主要用于存放存储单元在段内的偏移量。

eip

控制寄存器,存储CPU下次所执行的指令地址(存放指令偏移地址)。

esp

栈顶指针,该指针永远指向栈帧的栈顶。

ebp

栈底指针,该指针永远指向最上面一个栈帧的底部。


3. 常用的汇编指令

  • push指令:压栈操作。它首先减少esp的值,再将源操作数复制到栈地址,在32位平台上,esp每次减少4字节。
  • pop指令:出栈操作。它首先把esp指向的栈元素内容复制到操作数中,再增加esp的值。在32位平台上,esp每次增加4字节。
  • mov指令:用于将一个数据从源地址传送到目标地址,源操作地址的内容不变。
  • sub指令:减操作指令,从寄存器中减去<shifter_operand>表示的数值,并将结果保存到目标寄存器中。
  • lea指令:是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数。
  • rep指令:重复前缀指令,英文缩写 repeat。能够引发其后字符串指令被重复。
  • stos指令: 串存储指令,英文缩写 store string。
  • rep指令重复其上面的指令,ecx的值是重复的次数,每执行一次,ecx 减 1,直到 ecx 减至0。
  • stos指令将 eax中的值拷贝到es:[edi]指向的地址。

在这里插入图片描述
dword双字 就是四个字节。
ptrpointer缩写 即指针
[ ]里的数据是一个地址值,这个地址指向一个双字型数据
一次拷贝双字(4个字节)的数据到目的地址。
es:[edi]指向目的串
解释:合起来的意思就是,将栈上从 ebp-0E4h开始的位置,向高地址方向的内存赋值 0CCCCCCCCh,重复 39h 次,每次赋值双字(四字节的空间)。

  • call指令:将程序下一条指令的位置的IP压入堆栈中,并转移到调用的子程序。
  • jmp指令:无条件跳转指令。
  • add指令:用于将两个运算子相加,并将结果写入第一个运算子。
  • ret指令:用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。

4. 函数栈帧创建于销毁的整个过程

  演示的代码:

#include<stdio.h>
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);
}

  当按下F10(逐步调试),并打开调用堆栈,运行至主程序末尾return 0;后。如图下所示,可知main函数也是被其他函数调用的。调用main函数的函数叫__tmainCRTStartup(),而若继续逐步调试下去我们还会发现__tmainCRTStartup()函数是被mainCRTStartup()函数调用的。

在这里插入图片描述


4.1 main()函数开辟栈帧

  想要观察到函数在被调用时执行的每一步,我们就需要在调式执行后进行转到反汇编操作。操作后就能得到C语言代码翻译后的汇编代码,从而可以按步执行并分析C语言的汇编代码。如下图所示:

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

  函数栈帧所开辟的空间将会全部初始化为随机值0cccccccch,这也是为什么在未初始化情况下,局部变量内会放置随机值。


4.2 在main()函数中创建局部变量

在这里插入图片描述
请添加图片描述

  从这就能看出,局部变量并不是随意在栈区中存放的,而是对应正在被维护的函数的函数栈帧中开辟空间存放的。若局部变量不初始化时对应的汇编代码如下图所示。所以,这也就解释了为什么在不初始化的情况下,局部变量中存放的值是随机值0cccccccch。

在这里插入图片描述


4.3 调用Add()函数前准备

在这里插入图片描述
请添加图片描述

  函数在正式被调用前会进行一系列的压栈操作,会将需要被传的参数call指令下一条指令的地址压入栈中,为接下来函数的操作而准备。


4.4 为Add()函数开辟栈帧

在这里插入图片描述
请添加图片描述


4.5 Add()函数的定义说明部分与执行语句部分

在这里插入图片描述请添加图片描述

  函数传参时,并不会另外再去为形参开辟一块空间,而是通过访问之前压入栈中所传的参数来达到效果的。而且压入栈中的顺序是先右再左传的,就像这里的Add(a,b)是先传b再传a的,所以我们可以认为函数传参是从右向左传的。同样验证了之前所说的一句话:形参是实参的一份临时拷贝,因为形参就是在调用函数前压入或者说传入栈中的参数。所以改变形参压根就不关实参什么事,实参自然不会随之发生什么变化啦。


4.6 Add()函数栈帧的销毁

在这里插入图片描述

请添加图片描述

  这就是解释了在调用的和过程中,为什么需要将上一个函数栈帧的栈底地址压入栈中。目的就是在销毁栈帧的过程中,可以使得栈底指针ebp准确指向上一个函数栈帧的栈底位置,从而在结束调用后能够立马维护上一个被调用的函数。


4.7 Add()函数调用后的返回

在这里插入图片描述

5. 总结

在这里插入图片描述


在这里插入图片描述

这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值