这篇博客主要是对局部变量以及函数的一些问题的解答
- 局部变量是如何创建的?
- 局部变量在没有初始化时是随机值,背后原理是什么?
- 为什么说形参是实参的临时拷贝?
- 函数是如何调用参数的?
- 局部变量出函数就会销毁,那么函数返回的值是如何传递到main函数中的?
上述问题在函数的栈帧创建到销毁的过程中可以找到答案,这个过程涉及到汇编语言,我尽量简洁的说明每一条指令在做什么
注:不同的编译器此过程会有差异
以下列代码为例,来展示函数的栈帧从创建到销毁的全部过程
int Add(int x,int y)
{
int z=x+y;
return z;
}
int main()
{
int a=20;
int b=10;
int c=Add(a,b);
printf("%d\n",c);
}
目录
1.寄存器
在展示之前需要了解什么是寄存器,寄存器是CPU的组成部分,是有限存贮容量的高速存贮部件,可用来暂存指令、数据和地址等。这里主要介绍后面内容会使用到的寄存器:
- ebp:基址指针寄存器,常用它作为基址来访问堆栈
- esp:堆栈指针寄存器,堆栈的栈顶指针
这两个寄存器存放的都是地址,二者的地址用于维护函数栈桢
- ebx:基址寄存器,常以它为基址访问内存
- esi:来源索引寄存器,常用做内存数据指针和源字符串指针
- edi:目的索引寄存器,常做内存数据指针和目的字符串指针
- eax:累加寄存器,常用于加法、乘法和函数的返回值
2.main函数的创建
在写好代码后我们进入调试,来看看程序在创建main函数之前在干什么
首先,main函数是由其它的函数调用的,如图:
mainCRTStarturp调用__tmainCRTStartup,然后__tmainCRTStartup调用main函数,所以在main函数之前在栈帧上就已经为两个函数开辟出了空间
调用main函数后来看一下汇编代码
在main函数和第一个变量之间相隔多条汇编代码,我们逐个来解读
完成这三步后,就在栈区为main函数开辟出了一块空间
操作完成后从ebp到edi之间的空间全部被初始化为了CCCCCCCC
到这里第二个问题就有了答案,因为main函数空间内在创建变量之前就已经初始化了,不同编译器的初始化的值不同,变量在未初始化时是随机值
3.创建变量
main函数和变量都创建完毕后,接下来就是调用函数了
4.调用函数
4.1传参
还是逐个来解读,先看前4行
也就是说,形参的空间并不是在函数内部开辟的
传参后就是调用函数
在调用函数之后,便进入了函数Add内部,call下方的两条指令会在函数返回的时候执行
4.2开辟函数空间
同main函数一样,进入Add函数后首先要给函数开辟空间,可能空间的大小以及初始化有所不同,但操作一样
4.3函数的计算
ebp+8和ebp+12就是传参时的地址
在计算完毕后,接下来就是返回main函数了
5.返回main函数
5.1Add函数的返回值
所以为什么即使变量销毁,但是值依旧能够传递给main函数?
是因为变量的在函数销毁前存放在了寄存器中,寄存器不会被销毁,除非你物理销毁CPU
(〃 ̄ω ̄〃)
5.2函数销毁
结合上图来看,ebp指向的地址中存放的是main函数的地址,所以在弹出ebp地址后,ebp返回main函数,并指向栈底
此时esp指向的地址中存放着一个地址,下一条指令
返回,那么esp就返回00C21450这个地址
00C21450是由指令call记录的,是指令call的下一条指令,故esp返回的是call的下一条指令
5.3返回值的传递
call下一条指令esp+8,那么eax和ecx的空间就弹出了
eax存储的是Add销毁前z的值,ebp-20处存储的是变量c,所以到这返回值传给了c
那么函数栈帧的创建到销毁的过程便介绍完了,来回答一下开头提出的问题
1.局部变量是如何创建的?
在开辟出main函数的空间后,将main函数的空间分配给变量
2.局部变量在没有初始化时是随机值,背后原理是什么?
main变量在进入main函数之前,main函数内部的空间就已经填充了值,不同的编译器的值不同
3.为什么说形参是实参的临时拷贝?
因为函数调用参数的时候并不是在main函数内部调用,而是提前在main函数外将参数的值放入寄存器进行压栈,然后调用,二者虽然值相同,但地址不同
4.函数是如何调用参数的?
在开辟函数空间之前函数的参数就已经放在寄存器中进行压栈了,在函数需要使用的时候直接去寄存器的地址将值取出
5.局部变量出函数就会销毁,那么函数返回的值是如何传递到main函数中的?
返回值提前保存在寄存器中,返回时虽然变量销毁,但寄存器不会被销毁,返回main函数后需要使用返回值时再从寄存器中取出
如果上述过程有遗漏或错误的地方,请指出
完