天天看点

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用

  本篇,我们对C语言函数的调用进行一个深入地研究。

1. main函数的调用过程

  首先我们看一个代码。

#include <stdio.h>
    #include <stdlib.h>

    int add(int x, int y) {
        return x + y;
    }

    int main() {
        int a = ;
        int b = ;
        int ret = add(a, b);
        printf("ret = %d\n", ret);

        system("pause");
        return ;
    }
           

  接下来我们打开调试,观察一下它的调用堆栈。

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用

  这时我们可以看到,原来入口函数 main函数 也是被别的函数调用的。

  一个函数名为 __tmainCRTStartup() 的函数对 main函数 进行了调用,调用如下。

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用

  从调用堆栈中我们还能发现, __tmainCRTStartup()函数 又被另一个叫做 exe!mainCRTStartup() 的调用的,调用如下。

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用

  至此,main函数的前世今生我们就算摸清楚了。

main函数在 __tmainCRTStartup 函数中调⽤的,⽽ __tmainCRTStartup 函数是

在 mainCRTStartup 被调⽤。

  我们知道每⼀次函数调⽤都是⼀个过程。这个过程我们通常称之为: 函数的调⽤过程。这个过程要为函数开辟栈空间,⽤于本次函数的调⽤中临时变量的保存、现场保护。这块栈空间我们称之为函数栈帧。

  接下来我们了解一下函数的栈帧。

2. 函数栈帧

  栈帧的维护我们必须了解ebp和esp两个寄存器。在函数调⽤的过程中这两个寄存器存放了维护这个栈的栈底和栈顶指针。

  • ebp:指向栈底的指针
  • esp:指向栈顶的指针

  栈空间的使用是由高地址向低地址增长的,栈底固定不变,栈顶向低地址增长。

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用

  接着上面的代码,我们研究一下它的反汇编。

2.1 main函数栈帧初始化

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用
int main() {
    B10  push        ebp                   // 进入main函数,先将ebp压栈
                                                // 方便main函数调用完毕返回 __tmainCRTStartup()
    B11  mov         ebp,esp               // 将esp赋值给ebp,产生新栈底
    B13  sub         esp,h              // C栈是向下生长的,esp减去0E4H,即开辟 0E4H 的栈空间
    B19  push        ebx                    
    B1A  push        esi  
    B1B  push        edi  
    B1C  lea         edi,[ebp-h]        // 接下来的四句汇编,意思为初始化栈空间
    B22  mov         ecx,h  
    B27  mov         eax,CCCCCCCCh        // 初始化的值为0CCCCCCCCh,这也就是为什么内存访问越界时会显示 烫烫烫,
                                                // 烫烫烫 ASCII码值对应的就就是0CCCCCCCCh
    B2C  rep stos    dword ptr es:[edi]  
        int a = ;
    B2E  mov         dword ptr [a],Ah     // 创建局部变量a
        int b = ;
    B35  mov         dword ptr [b],Ah     // 创建局部变量b
           

  接下来,进入到 add 函数的调用。

2.2 add函数调用

  首先,先进行函数的传参。

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用
int ret = add(a, b);
    B3C  mov         eax,dword ptr [b]     // 将[b]中存放的变量b存入寄存器eax
    B3F  push        eax                   // eax压栈
    B40  mov         ecx,dword ptr [a]     // 将[a]中存放的变量a存入寄存器ecx
    B43  push        ecx                   // ecx压栈
    B44  call        _add (F9h)      // call指令跳转到 F9h
                                                // 回来时,会跳到call指令下一条语句,即 B49
    B49  add         esp,  
    B4C  mov         dword ptr [ret],eax 
           

  call跳转。

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用

  经过call来到了 011011F9,此地址处是一条 jmp 指令,我们又要进行跳转。

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用

  这里就是 add 函数的栈帧。

int add(int x, int y) {
    CF0  push        ebp                   // 压栈当前ebp
    CF1  mov         ebp,esp               // 将esp赋值给ebp,产生新栈底
    CF3  sub         esp,C0h              // esp减去C0H,即开辟 C0H 的栈空间
    CF9  push        ebx                   
    CFA  push        esi  
    CFB  push        edi  
    CFC  lea         edi,[ebp-C0h]        // 下面四句指令同 main函数
    D02  mov         ecx,h  
    D07  mov         eax,CCCCCCCCh  
    D0C  rep stos    dword ptr es:[edi]  
        return x + y;
    D0E  mov         eax,dword ptr [x]     
    D11  add         eax,dword ptr [y]     // 进行 x+y 的运算,结果保存在寄存器eax中
    }
    D14  pop         edi               
    D15  pop         esi  
    D16  pop         ebx  
    D17  mov         esp,ebp               
    D19  pop         ebp  
    D1A  ret                               // 跳转结束,跳转回 call 下一条指令
           

  程序运行到这里,执行 ret 指令,结束跳转,执行流回到 call 下一条指令。

13. 函数的调用过程(栈帧)详解1. main函数的调用过程2. 函数栈帧2.2 add函数调用

  接下来的都是语句的具体操作,这里就不进行赘述了。

继续阅读