本期介绍
主要介绍:在C语言中函数调用时的整个过程。
一、前言
我想大家在学习C语言的过程中或多或少会产生如下几个问题吧:
1.局部变量在内存中是如何存放的?难道随便选一个没被占用的空间就可以吗?
2.为什么局部变量在没有初始化的时候会自动附上随机值呢?
3.函数是怎么传参的?传参的顺序又是何如?
4.都说形参是实参的一份临时拷贝,那为什么会这么说呢?
5.函数调用的整个过程又是怎样的呢?
在正式详细讲解之前我先笼统的说明一些知识点吧。首先我们应该知道,栈是高地址向低地址的延申,也就是说在栈区中一般会先使用高地址再使用低地址。其次,每一个函数的调用都会在栈区中开辟一块空间,而这块空间就被称为该函数的函数栈帧,这个栈帧中维持着各种所需的信息:函数的参数、局部变量、调用结束后跳转的地址等等。最后还要说到两个寄存器,分别为esp和ebp(其中esp存放的是当前栈帧的栈顶地址,ebp存放的是当前栈帧的栈底地址),函数的栈帧就是靠这两个寄存器来维护的。注意:esp和ebp一定维护的是当前正在被调用的函数的函数栈帧。
二、寄存器的种类与功能
寄存器 | 功能 |
---|---|
eax | 累加寄存器,相对于其他寄存器,在运算方面比较常用。 |
ebx | 基地址寄存器,在内存寻址时存放基地址。 |
ecx | 计数寄存器,用于循环操作,比如重复的字符存储操作或者数字统计。 |
edx | 作为eax的溢出寄存器,总是被用来存放整数除法产生的余数。 |
esi | 源变址寄存器,主要用于存放存储单元在段内的偏移量,通常在内存操作指令中作为“源地址指针”使用 |
edi | 目的变址寄存器,主要用于存放存储单元在段内的偏移量。 |
eip | 控制寄存器,存储CPU下次所执行的指令地址(存放指令偏移地址)。 |
esp | 栈顶指针,该指针永远指向栈帧的栈顶。 |
ebp | 栈底指针,该指针永远指向最上面一个栈帧的底部。 |
三、常用的汇编指令
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指令
:用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。
四、函数栈帧创建于销毁的整个过程
首先,我来介绍一下接下来要演示的代码:
#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);
}
我们知道函数需要被调用时才会开始执行函数中的代码,那有没有想过main()函数是被谁调用的呢?想要证实,就按下F10(逐步调试) 打开调用堆栈,直至调试到return 0后一步。如下图可知,main()
函数是被__tmainCRTStartup()
函数调用的,而若继续逐步调试下去我们还会发现__tmainCRTStartup()
函数是被mainCRTStartup()
函数调用的。
1.main()函数开辟栈帧
首先想要观察到函数在被调用时执行的每一步,我们就需要在调式执行后进行转到反汇编操作,从而就可以按步执行C语言的汇编代码了。如下图所示:
函数栈帧所开辟的空间将会全部初始化为随机值0cccccccch,这也是为什么在未初始化情况下,局部变量内会放置随机值。
2.在main()函数中创建局部变量
从这我们就能够看出,局部变量并不是随意在栈区中存放的,而是对应其所正在被维护的函数的函数栈帧中开辟空间存放的。此外,若局部变量不初始化时对应的汇编代码如下图所示。所以,这也就解释了为什么在不初始化的情况下,局部变量中存放的值是随机值0cccccccch。
3.调用Add()函数前准备
可以看出函数在正式被调用前会进行一系列的压栈操作,会将需要被传的参数和call指令下一条指令的地址压入栈中,为接下来函数的操作而准备。
4.为Add()函数开辟栈帧
这一步就和之前开辟main()函数的函数栈帧基本一摸一样没什么好说的。
5.Add()函数的定义说明部分与执行语句部分
从这就可以看出函数传参时,并不会另外再去为形参开辟一块空间,而是通过访问之前压入栈中所传的参数来达到效果的。而且压入栈中的顺序是从右向左传的,就像这里的Add(a,b)是先传b再传a的,所以我们可以认为函数传参是从右向左传的。同样验证了之前所说的一句话:形参是实参的一份临时拷贝,因为形参就是在调用函数前压入或者说传入栈中的参数。所以改变形参压根就不关实参什么事,实参自然不会随之发生什么变化啦。
6.Add()函数栈帧的销毁
所以这就是为什么在调用的和过程中,需要将上一个函数栈帧的栈底地址压入栈中,为的就是在调用回返的途中可以使得栈底指针ebp准确指向该位置,从而在结束调用后能够立马维护上一个被调用的函数。
7.Add()函数调用后的返回
五、总结
转载:https://blog.csdn.net/m0_66769266/article/details/125444755