图文解析函数栈帧

前言:

        在C语言程序中,编写函数是我们必不可少的技能,但是我们通常会忽视了函数在内存中的开辟过程,一般地我们通过将一些功能封装到函数中,通过将实参传入,在主函数main.c中调用函数,但是事实上,函数调用的过程十分复杂,若是要追根究底的话,需要有一定的汇编知识,这篇文章我们略过汇编代码,通过用图文的知识去理解函数栈帧。

        

一、函数栈帧是什么?

        在程序运行过程中,函数栈帧(Function Stack Frame) 是栈内存中为单个函数调用分配的一块独立内存区域

主要作用如下:

        

①用于管理函数的参数、局部变量、返回地址、寄存器状态等信息

        

②它是函数调用和返回的基础,确保了函数之间的独立执行和数据隔离。        

        关于内存的分布如下图所示:

        

       

二、前言知识的准备

        栈是一种 “简单却强大” 的数据结构。     

        简单来说,栈相当于一支乒乓球桶,我们放入乒乓球通过桶口放入,我们拿出乒乓球通过桶口拿出,底下的乒乓球先被放进去,顶上的乒乓球后被放进去,顶上的乒乓球先被拿出,底下的乒乓球后被拿出,所以说栈是一个先进后出的数据结构

        

其中放入乒乓球被称为压栈(push) 

        

其中拿出乒乓球被称为出栈(pop)

                

1.压栈

        

2.出栈

        

3.维护栈的指针(两个寄存器)

①EBP(栈底指针)固定指向当前栈帧的 “基地址”,用于定位栈帧内的参数、变量(偏移量固定)。

        

②ESP(栈顶指针)始终指向栈的 “顶部”,随压栈(push)/ 弹栈(pop)动态移动。

        

4.CPU 的EAX寄存器

主要作用就是传递函数返回值     

        

三、案例代码详解函数栈帧

        代码示例:主函数中调用add函数,实现x+y

#include <stdio.h>

// 被调用函数:计算x+y
int add(int x, int y) 
{
    int z;          // 局部变量
    z = x + y;      // 计算逻辑
    return z;       // 返回结果
}

// 调用者函数
int main() 
{
    int a = 2, b = 3;  // main的局部变量
    int c = add(a, b); // 调用add,接收返回值
    printf("c=%d", c);
    return 0;
}

        

阶段 1初始状态

如下图所示:

               main 函数执行中,未调用 add 前, 变量a先压入栈中,变量b后压入栈中,此时esp栈顶指针指向下一个即将压入栈中元素的位置,ebp维护的是main函数栈帧中的基底位置。

             

        

阶段2:调用 add 前

        main 准备参数,压栈,C 语言函数参数默认从右向左压栈(先压最右边的参数y,再压左边的x,然后压入 “返回地址”(add 执行完后,main 要回到的代码位置)。

        

        

步骤 2.1:压入 add 的第二个参数 

如下图所示:

  • 执行 push 3(将b的值压栈)
  • ESP 向下移动 4 字节(int 占 4 字节),指向新栈顶

        

        

步骤 2.2:压入 add 的第一个参数 

如下图所示:

  • 执行 push 2(将a的值压栈)
  • ESP 再向下移动 4 字节

        

        

步骤 2.3:压入 “返回地址”

如下图所示:

  • 当 add 执行完后,需要回到 main 的int c = add(a,b);下一条指令(即printf("c=%d", c);的地址,记为0x00401234);
  • 执行 push 0x00401234(压入返回地址);
  • ESP 再向下移动 4 字节。

        

        

阶段3:  进入 add 函数

       创建 add 的栈帧,此时 CPU 跳转到 add 函数的代码,开始初始化 add 的栈帧 —— 核心是 “保存 main 的 EBP” 和 “设置 add 的 EBP”。

        

             

步骤 3.1:保存 main 的 EBP

如图所示:

  • 为了 add 执行完后能恢复 main 的栈帧,需要先把 main 的 EBP压栈;
  • 执行 push ebp
  • ESP 向下移动 4 字节。

        

        

步骤 3.2:设置 add 的 EBP

    如图所示: 

  • 将当前 ESP 的值赋给 EBP,此时 EBP 成为 add 栈帧的 “基地址”;
  • 此时 EBP 固定指向 add 栈帧的 “基地址”,后续 add 访问参数 / 变量都通过 EBP 的偏移量(如EBP+8是 x,EBP+12是 y)

        

        

步骤 3.3:分配 add 的局部变量空间

  • 此时esp向低地址处一定的字节大小,同时add开辟一定的空间
  • add 有局部变量int z,需要在栈中至少预留 4 字节空间;

        

阶段4:执行 add 函数逻辑

如图所示:计算 z = x + y

        

核心逻辑:

  1. x:访问EBP+8,值为 2;
  2. y:访问EBP+12,值为 3;
  3. 计算2+3=5,将结果存入z的地址

        

        

阶段 5:add 函数返回

        

如图所示:销毁 add 的栈帧,回到 main        

步骤 5.1:传递返回值

        z=5 存入 EAX通过将函数中的返回值存入到Cpu中的EAX寄存器中,然后将其带回主函数。

        

步骤 5.2:释放 add 的局部变量空间

将 ESP 拉回 EBP 的位置,相当于 “回收” 局部变量z的空间。

        

简单来说,add函数所开辟的空间被操作系统回收,用户不在拥有访问权限,后续会被其他在栈上开辟的空间所覆盖。

        

步骤 5.3:恢复 main 的 EBP

如图所示:

                弹出旧 的EBP,相当于让EBP重新回到了main函数基底的位置。      

        

        

步骤 5.4:弹出返回地址,回到 main 执行

如图所示:

  • 执行 ret:将栈顶(0x1008)的 “返回地址 = 0x00401234” 弹出,CPU 跳转到这个地址;
  • ESP 向上移动 4 字节,指向 (栈顶现在是参数 x=2)

        

        

步骤 5.5:清理 add 的参数(main 回收参数空间)

如图所示:

  • main 调用完 add 后,需要回收之前压入的参数(x=2、y=3);
  • 执行 add esp, 8(ESP 向上移动 8 字节,因为两个 int 参数共 8 字节);

        

        

阶段 6:main 接收返回值,继续执行

        

如图所示:

  • main 从EAX寄存器中取出返回值 5,赋值给局部变量cint c = 5);
  • 后续执行printf("c=%d", c),输出c=5,程序结束。

        

四、总结:

 函数栈帧的核心要点:  

  1. 创建栈帧:调用者压参数→压返回地址→被调用者压旧 EBP→设新 EBP→分配局部变量。
  2. 使用栈帧:通过 EBP 的固定偏移访问参数(EBP+偏移)和局部变量(EBP-偏移)。
  3. 销毁栈帧:释放局部变量→恢复旧 EBP→弹出返回地址→清理参数→回到调用者。

        

 既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值