zoukankan      html  css  js  c++  java
  • (C语言内存十五)用一个实例来深入剖析函数进栈出栈的过程

    debug

    前面我们只是讲解了一个函数的活动记录是什么样子的,相信大家对函数的详细调用过程的认识还不是太清晰,这节我们就以 VS2010 Debug 模式为例来深入分析一下。

    请看下面的代码:

    void func(int a, int b){
        int p =12, q = 345;
    }
    int main(){
        func(90, 26);
        return 0;
    }
    

    函数使用默认的调用惯例 cdecl,即参数从右到左入栈,由调用方负责将参数出栈。函数的进栈出栈过程如下图所示:

    函数进栈分析

    步骤①到⑥是函数进栈过程:

    1. main() 是主函数,也需要进栈,如步骤①所示。

    2. 在步骤②中,执行语句func(90, 26);,先将实参 90、26 压入栈中,再将返回地址压入栈中,这些工作都由 main() 函数(调用方)完成。这个时候 ebp 的值并没有变,仅仅是改变 esp 的指向。

    3. 到了步骤③,就开始执行 func() 的函数体了。首先将原来 ebp 寄存器的值压入栈中(也即图中的 old ebp),并将 esp 的值赋给 ebp,这样 ebp 就从 main() 函数的栈底指向了 func() 函数的栈底,完成了函数栈的切换。由于此时 esp 和ebp 的值相等,所以它们也就指向了同一个位置。

    4. 为局部变量、返回值等预留足够的内存,如步骤④所示。由于栈内存在函数调用之前就已经分配好了,所以这里并不是真的分配内存,而是将 esp 的值减去一个整数,例如 esp - 0XC0,就是预留 0XC0 字节的内存。

    5. 将 ebp、esi、edi 寄存器的值依次压入栈中。

    6. 将局部变量的值放入预留好的内存中。注意,第一个变量和 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
    ", m, n);
        system("pause");
        return 0;
    }
    

    运行结果:

    0XCCCCCCCC, 0XCCCCCCCC

    虽然编译器对空白内存进行了初始化,但这个值对我们来说一般没有意义,所以我们可以认为它是垃圾值、是随机的。

    函数出栈分析

    步骤⑦到⑨是函数 func() 出栈过程:
    7) 函数 func() 执行完成后开始出栈,首先将 edi、esi、ebx 寄存器的值出栈。

    1. 将局部变量、返回值等数据出栈时,直接将 ebp 的值赋给 esp,这样 ebp 和 esp 就指向了同一个位置。

    2. 接下来将 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);
        return 0;
    }
    

    运行结果:

    n = 18

    在 func() 中,将局部变量 a 的地址赋给 p,在 main() 函数中调用 func(),函数刚刚调用结束,还没有其他函数入栈,局部变量 a 所在的内存没有被覆盖掉,所以通过语句n = *p;能够取得它的值。

  • 相关阅读:
    2020916 spring总结
    20200915--事务
    20200915-mybatis基础
    20200911--使用注解开发
    20200910--Spring配置
    20200909--spring基础-IOC
    20200909-待补充
    20200909记我所看到的问题
    20200909-spring基础一
    面向对象
  • 原文地址:https://www.cnblogs.com/still-smile/p/14900573.html
Copyright © 2011-2022 走看看