本期介绍🍖
主要介绍:在C语言中函数到底是如何实现调用函数的,以及调用的整个过程,局部变量是如何以及在哪创建的,为何局部类变量未初始化就会赋随机值,函数传参的顺序以及存放空间👀。
文章目录
1. 前言
大家在学习C语言的过程中,或多或少会产生下面几个疑惑:
- 局部变量在内存中是如何存放的?
- 为什么局部变量在没有初始化的时候会自动附上随机值?
- 函数是怎么传参的?传参的顺序又是何如?
- 都说形参是实参的一份临时拷贝,那为什么会这么说呢?
- 函数调用的整个过程又是怎样的呢?
在正式详细讲解之前,需要补充一些知识点。首先,栈是高地址向低地址的延申,也就是说在栈区中一般会先使用高地址再使用低地址。其次,每一个函数的调用都会在栈区中开辟一块空间,而这块空间就被称为该函数的函数栈帧,这个栈帧中维持着各种所需的信息:函数的参数、局部变量、调用结束后跳转的地址等等。最后,需要了解两个寄存器,分别为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. 总结
这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。