原文地址:https://www.cnblogs.com/still-smile/p/14900573.html
Debug调试
前面我们只是讲解了一个函数的活动记录是什么样子的,相信大家对函数的详细调用过程的认识还不是太清晰,这节我们就以 VS2010 Debug 模式为例来深入分析一下。
请看下面的代码:
void func(int a, int b){
int p =12, q = 345;
}
int main(){
func(90, 26);
return 0;
}
函数使用默认的调用惯例 cdecl,即参数从右到左入栈,由调用方负责将参数出栈。函数的进栈出栈过程如下图所示:
函数进栈分析
步骤 ① 到 ⑥ 是函数进栈过程:
main()
是主函数,也需要进栈,如步骤 ① 所示。- 在步骤 ② 中,执行语句
func(90, 26);
,先将实参 90、26 压入栈中,再将返回地址压入栈中,这些工作都由main()
函数(调用方)完成。这个时候 ebp 的值并没有变,仅仅是改变 esp 的指向。 - 到了步骤 ③,就开始执行
func()
的函数体了。首先将原来 ebp 寄存器的值压入栈中(也即图中的 old ebp),并将 esp 的值赋给 ebp,这样 ebp 就从main()
函数的栈底指向了func()
函数的栈底,完成了函数栈的切换。由于此时 esp 和ebp 的值相等,所以它们也就指向了同一个位置。 - 为局部变量、返回值等预留足够的内存,如步骤 ④ 所示。由于栈内存在函数调用之前就已经分配好了,所以这里并不是真的分配内存,而是将 esp 的值减去一个整数,例如 esp - 0XC0,就是预留 0XC0 字节的内存。
- 将 ebp、esi、edi 寄存器的值依次压入栈中。
- 将局部变量的值放入预留好的内存中。注意,第一个变量和 old ebp 之间有4个字节的空白,变量之间也有若干字节的空白。
为什么要留这么多的空白
为什么要留出这么多的空白,岂不是浪费内存吗?这是因为我们使用 Debug 模式生成程序,留出多余的内存,方便加入调试信息;以 Release 模式生成程序时,内存将会变得更加紧凑,空白也被消除。
至此,func()
函数的活动记录就构造完成了。可以发现,在函数的实际调用过程中,形参是不存在的,不会占用内存空间,内存中只有实参,而且是在执行函数体代码之前、由调用方压入栈中的。
未初始化变量的值为什么是垃圾值
为局部变量分配内存时,仅仅是将 esp 的值减去一个整数,预留出足够的空白内存,不同的编译器在不同的模式下会对这片空白内存进行不同的处理,可能会初始化为一个固定的值,也可能不进行初始化。
例如在 VS2010 Debug 模式下,会将预留出来的内存初始化为 0XCCCCCCCC,如果不对局部变量赋值,它们的内存就不会改变,输出时的结果就是 0XCCCCCCCC,请看下面的代码:
#include <stdio.h>
#include <stdlib.h>
int main(){
int m, n;
printf("%#X, %#X\n", m, n);
system("pause");
return 0;
}
运行结果:
0XCCCCCCCC, 0XCCCCCCCC
虽然编译器对空白内存进行了初始化,但这个值对我们来说一般没有意义,所以我们可以认为它是垃圾值、是随机的。
函数出栈分析
步骤 ⑦ 到 ⑨ 是函数 func()
出栈过程:
- 函数
func()
执行完成后开始出栈,首先将 edi、esi、ebx 寄存器的值出栈。 - 将局部变量、返回值等数据出栈时,直接将 ebp 的值赋给 esp,这样 ebp 和 esp 就指向了同一个位置。
- 接下来将 old ebp 出栈,并赋值给现在的 ebp,此时 ebp 就指向了
func()
调用之前的位置,即main()
活动记录的 old ebp 位置,如步骤 ⑨ 所示。
这一步很关键,保证了还原到函数调用之前的情况,这也是每次调用函数时都必须将 old ebp 压入栈中的原因。
最后根据返回地址找到下一条指令的位置,并将返回地址和实参都出栈,此时 esp 就指向了 main()
活动记录的栈顶, 这意味着 func()
完全出栈了,栈被还原到了 func()
被调用之前的情况。
遗留的错误认知
经过上面的分析可以发现,函数出栈只是在增加 esp 寄存器的值,使它指向上一个数据,并没有销毁之前的数据。前面我们讲局部变量在函数运行结束后立即被销毁其实是错误的,这只是为了让大家更容易理解,对局部变量的作用范围有一个清晰的认识。
栈上的数据只有在后续函数继续入栈时才能被覆盖掉,这就意味着,只要时机合适,在函数外部依然能够取得局部变量的值。请看下面的代码:
#include <stdio.h>
int *p;
void func(int m, int n){
int a = 18, b = 100;
p = &a;
}
int main(){
int n;
func(10, 20);
n = *p;
printf("n = %d\n", n);
return 0;
}
运行结果:
n = 18
在 func()
中,将局部变量 a 的地址赋给 p,在 main()
函数中调用 func()
,函数刚刚调用结束,还没有其他函数入栈,局部变量 a 所在的内存没有被覆盖掉,所以通过语句 n = *p;
能够取得它的值。