13. 函数的调用过程(栈帧)详解

本文详细解析了C语言中函数调用的过程,包括main函数如何被启动函数调用,以及函数调用时栈帧的创建与维护。通过具体的代码实例及反汇编过程,帮助读者理解函数调用机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

1. main函数的调用过程

  首先我们看一个代码。

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

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

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

        system("pause");
        return 0;
    }

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

调用堆栈

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

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

__tmainCRTStartup

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

exe!mainCRTStartup()

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

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

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

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

2. 函数栈帧

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

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

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

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

2.1 main函数栈帧初始化

反汇编-1

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

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

2.2 add函数调用

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

反汇编-2

        int ret = add(a, b);
    01101B3C  mov         eax,dword ptr [b]     // 将[b]中存放的变量b存入寄存器eax
    01101B3F  push        eax                   // eax压栈
    01101B40  mov         ecx,dword ptr [a]     // 将[a]中存放的变量a存入寄存器ecx
    01101B43  push        ecx                   // ecx压栈
    01101B44  call        _add (011011F9h)      // call指令跳转到 011011F9h
                                                // 回来时,会跳到call指令下一条语句,即 01101B49
    01101B49  add         esp,8  
    01101B4C  mov         dword ptr [ret],eax 

  call跳转。

反汇编-3

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

反汇编-4

  这里就是 add 函数的栈帧。

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

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

反汇编-5

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值