C语言绝命七连问,你能回答出几个?
想要对上面的这六个问题做出准确深入的回答,我们需要学习函数栈帧的创建和销毁相关知识,在正式进入函数栈帧之前,我们需要了解一些相关的寄存器和汇编指令。
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
同时,每一次函数调用,编译器都会为该函数分配一块空间,而这块空间就被称为这个函数的函数栈帧;并且,这块空间是由两个寄存器来维护的:esp寄存器(记录栈顶的地址)和ebp寄存器(记录栈底的地址)。
函数调用堆栈是反馈函数调用逻辑的。我们以main函数的调用为例:
我们可以看到,mainCRTStartup调用__scrt_common_main,__scrt_common_main调用__scrt_common_main_seh,__scrt_common_main_seh调用_SCRT_STARTUP_MAIN,_SCRT_STARTUP_MAINmain,main调用Add。
我们以一段程序为例讲解函数栈帧:(注意: 函数栈帧的创建和销毁过程,在不同的编译器上实现的方法和细节会有所差异,一般来说,越新的编译器对函数栈帧的封装就越严密,本次演示以VS2019为例。) 演示代码
#include<stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 3;
int b = 5;
int ret = 0;
ret = Add(a, b);
printf("%d", ret);
return 0;
}
F11进入Add函数内部,观察Add函数的反汇编代码
代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。 在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。
1. 将main函数的 ebp 压栈。 2. 计算新的 ebp 和 esp。 3. 将 ebx , esi , edi 寄存器的值保存。 4. 计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问 到了函数调用前压栈进去的参数,这就是形参访问。 5. 将求出的和放在 eax 寄存器中准备带回。
当函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁,具体销毁过程如下:
调用完Add函数,回到main函数的时候,继续往下执行,可以看到:
add esp,8: esp直接+8,相当于pop了main函数之前压栈的a’和b’。
mov dword ptr [ebp-20h] eax:将eax中的值存放到ebp-0x20的地址处,其实就是存储到main函数中ret变量中,而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的。
当我们完整的了解了函数栈帧创建和销毁的过程后,我们就可以回答开篇提到的问题了: