这部分,我们将解决如下问题:
1.局部变量是如何创建的?(在栈上是怎么创建的?)
2.为什么局部变量的值是随机值?
3.函数是怎么传参的,传参的顺序是怎样的?
4.形参和实参是什么关系?
5.函数调用是怎么做的,又是怎么返回的?
编译环境:VS2022(在不同的编译器下,栈帧的创建和销毁是略有差异的,但是大体原理相同)
目录
4.开始执行Add函数,将两个形参相加并将结果保存在寄存器中
5.返回,并销毁Add函数的栈帧,并使两个寄存器指针重新指向main函数
6.返回main函数内部,销毁形参变量,将返回值赋值给实参,销毁main函数栈帧,程序结束
准备知识:
1.常见的寄存器:如eax,ebx,ecx,edx,efx,ebp,esp(这两个寄存器中存放的是地址,而这两个地址就是用来维护函数栈帧的,esp是栈顶指针,edp是栈底指针。栈区是优先使用高地址的)寄存器是集成到cpu上的,不单独存在于某一个函数里;
2.栈是从高地址向低地址使用的,所以我们的地址越小,表示离栈顶越近
为了使下面的逻辑更加清晰,我们将一段代码拆分的足够细,方便我们可以观察他的每一步操作,
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);
return 0;
}
下面,我们就来通过调试,查看寄存器,内存里的相关变量和寄存器的工作情况,来更加深入的理解相关操作的原理和实现:
1.main函数栈帧的创建和初始化
我们通过调用堆发现main函数也是被其他函数所调用的,为了方便我们就将这一部分称之为mainCRTStartup 函数,mainCRTStartup 函数栈帧的开辟不做研究,
我们调用c语言所代表的汇编代码,来看他们的一步步的执行过程:
通过以上操作,我们可以开辟出main函数的栈帧空间:
接下来就是main函数栈帧的初始化
ps:有没有大佬知道这个地方为什么这个初始化不将全部的main函数栈帧初始化,而是只初始化一部分呢?
我们通过调试也可以确定:
当前就是这样的栈结构
2.将变量放入已经开辟好的内存中
3.开始调用外部函数Add,并为Add函数开辟栈帧和初始化
将eax赋值为20,将ecx赋值为10,并依次压入栈中,本质上是创建的形参变量(实参的拷贝)
执行call指令,跳出函数并将call指令的下一条指令的压入栈中,保证函数可以有“入口”回来,
开始进入Add函数,并为Add函数开辟栈帧和初始化,方法上和main函数类似,这里不再展开
4.开始执行Add函数,将两个形参相加并将结果保存在寄存器中
将要返回的值放在寄存器中进行保存,避免函数结束变量被销毁(寄存器是不会被销毁的),这就是为什么我们可以通过局部变量返回值,这是靠存储在相应的寄存器中实现的。
5.返回,并销毁Add函数的栈帧,并使两个寄存器指针重新指向main函数
6.返回main函数内部,销毁形参变量,将返回值赋值给实参,销毁main函数栈帧,程序结束
销毁和Add函数的销毁类似,这里我们不在展开。
这就是关于函数栈帧的创建和销毁的相关原理的介绍,多了解一点我们就多懂一点,说不定面试就考了呢,哈哈~~~
再回到刚开始我们提出的几个问题:
1.局部变量是如何创建的?(在栈上是怎么创建的?)
2.为什么局部变量的值是随机值?
3.函数是怎么传参的,传参的顺序是怎样的?
4.形参和实参是什么关系?
5.函数调用是怎么做的,又是怎么返回的?
1.局部变量的创建,首先是开辟好函数的栈帧空间,接着,初始化一块内存空间(如果不给初值也可能是随机值),然后给局部变量在初始化的内存里分配空间即可。
2.局部变量不初始化的时候是随机值是在函数还在开辟栈帧空间的时候就将其一部分栈帧初始化为随机值,如果我们不给局部变量赋初值,那么放入局部变量时得到的就是随机值。
3.函数的传参,是在我们的函数栈帧还未开辟之前,我们的实参变量就已经拷贝了一份压入了栈中,在我们开辟好函数栈帧之后,我们通过寄存器指针的偏移量来找到已经拷贝好的形参变量,完成了传参操作。
4.形参是实参的一份拷贝,只是在值上与实参相等,在空间上却是独立的,所以改变形参不会影响实参的值,还要注意,形参变量不在函数内部,他是在函数栈帧开辟之前就已经压入栈中的,像我们上面的x和y分别是两个绿色方块中的形参,只有在函数内部的局部变量才是在该函数的栈帧空间内创建并存储(像我们的z变量)。
5.我们在函数栈帧开辟之前就已经将调用该函数的函数的栈底指针(相当于上面的ebp-main),所以当返回时,随着当前函数的栈帧被销毁,我们可以找到调用该函数的栈底指针,并且在调用一个函数时,我们会将该函数在调用其他函数的那一步的下一步的地址(也就是我们上面讲的call指令的作用)压入栈中,在返回时我们就可以按地址寻找到当前函数的下一步指令。
这里还要注意,我们的栈帧是在栈上讨论的,所以不应该涉及static等静态区的变量类型。
这些东西比较抽象,不太好理解,但是多了解一些总是没有坏处的,万一在某个时候就正好考到了呢~~~
好了,这次的分享就到这里了,希望能给你们带来帮助,谢谢。