当前位置: 首页 > news >正文

C语言 ——— 函数栈帧的创建和销毁

目录

寄存器

mian 函数是被谁调用的 

通过汇编了解函数栈帧的创建和销毁 

转汇编后(Add函数之前的部分)

转汇编后(进入Add函数之前的部分) 

转汇编后(正式进入Add函数的部分)

​编辑

总结

局部变量是怎么创建的? 

函数是怎么传参的?

形参和实参是什么关系?

函数调用的结果是怎么返回的?


寄存器

在深入了解函数栈帧之前,需要先熟悉两个关键的寄存器。在常见的寄存器如 eax、ebx、ecx、edx、ebp、esp 中,ebp 和 esp 是理解函数栈帧的基础

寄存器本质上是 CPU 内部用于存储数据的高速存储单元,而 ebp 和 esp 这两个寄存器专门用于存放内存地址,其存储的地址在函数栈帧的维护中起着至关重要的作用

在程序运行时,每一次函数调用都会在栈区分配一块独立的内存空间,这个空间被称为函数栈帧,用于存放函数执行过程中的局部变量、参数、返回地址等信息。ebp 和 esp 就负责对这个函数栈帧的空间进行维护

以 main 函数为例,当程序开始执行 main 函数时,系统会在栈区为其开辟一块特定的内存空间。其中,ebp 是指向该栈帧底部的指针,它标记了 main 函数栈帧的起始位置,负责维护 main 函数栈帧的底部边界;而 esp 则是指向栈区为 main 函数开辟的栈帧顶部的指针,它会随着栈内数据的压入和弹出而动态变化,实时反映栈帧的当前顶部位置。通过 ebp 和 esp 这两个指针的协同工作,系统能够准确地管理函数栈帧的空间,确保函数的正常执行和数据的正确存储与访问

需要注意的是:

在不同编译器与架构中,管理函数栈帧的寄存器命名会随架构升级而变化:

  • VS2013(x86 32 位架构):使用 ebp(基指针,指向栈帧底部)和 esp(栈指针,指向栈帧顶部)。
  • VS2022(x86-64 64 位架构)ebp 对应重命名为 rbp(基指针),esp 对应重命名为 rsp(栈指针),功能与原寄存器一致,仅因架构升级调整命名,仍用于维护栈帧的底部与顶部边界

mian 函数是被谁调用的 

在 VS2013 编译器环境下,通过对调用堆栈的分析可知,main函数并非程序执行入口,而是由__tmainCRTStartup()函数触发调用

__tmainCRTStartup()函数作为 C 运行时库(CRT)的关键部分,承担着初始化运行环境、设置全局变量、处理命令行参数等前置工作,为main函数的执行提供必要条件。在此之前,__tmainCRTStartup()函数本身由mainCRTStartup()函数调用。mainCRTStartup()函数负责更底层的初始化任务,包括启动 C 运行时库、加载程序依赖项等,是程序从操作系统控制权转移到用户代码执行的关键桥梁

这种调用层级设计体现了 C 运行时库对程序执行流程的精细化管理,确保main函数在稳定、初始化完成的环境中运行,从而提升程序执行的可靠性与兼容性


通过汇编了解函数栈帧的创建和销毁 

代码演示:

int Add(int x, int y)
{int z = 0;z = x + y;return z;
}
int main()
{int a = 10;int b = 20;int sum = 0;sum = Add(a, b);printf("%d\n", sum);return 0;
}

接下来通过这段代码的汇编形式了解函数的栈帧

转汇编后(Add函数之前的部分)

int main()
{
00C21410  push        ebp
00C21411  mov         ebp,esp
00C21413  sub         esp,0E4h
00C21419  push        ebx
00C2141A  push        esi
00C2141B  push        edi
00C2141C  lea         edi,[ebp-0E4h]
00C21422  mov         ecx.39h
00C21427  mov         eax,0CCCCCCCCh
00C2142C  rep stos    dwrd ptr es:[edi]int a= 10;
00C2142E  mov         dword ptr [ebp-8],0Ahint b=20;
00C21435  mov         dword ptr [ebp-14h],14hint c=0;
00C2143C  mov         dword ptr [ebp-20h],0

第一条指令:【push   ebp】

在程序运行过程中,main 函数由 __tmainCRTStartup 函数调用。在调用 main 函数之前,rbp(基指针)和 rsp(栈指针)指向 __tmainCRTStartup 函数的栈帧,这表明当前程序执行环境处于 __tmainCRTStartup 函数的上下文

当开始调用 main 函数时,main 函数的第一条汇编指令通常是 push rbp。在汇编语言里,push 指令用于执行压栈操作。这里执行 push rbp 时,会将当前 rbp 的值压入栈区,具体位置是在 __tmainCRTStartup 函数栈帧的上方

这一操作具有重要意义,它是为 main 函数开辟独立栈空间的关键步骤。通过将当前 rbp 压栈,可以保存之前函数(即 __tmainCRTStartup 函数)的栈帧基地址,后续在 main 函数执行过程中,就可以基于这个保存的值恢复之前的栈帧状态。同时,后续还会有其他操作来进一步调整栈指针,从而完成 main 函数栈空间的完整开辟,以存放 main 函数执行过程中的局部变量、临时数据等

示意图:

第二条指令: 【mov   ebp,esp】

其中,mov 指令的作用是数据移动,它会把源操作数的值复制到目标操作数所在的位置

当执行 mov ebp, esp 这条指令时,esp 寄存器的值会被赋值给 ebp 寄存器。这就意味着,原本指向栈顶的 esp 所保存的地址值,现在被复制到了 ebp 寄存器中。其结果是,ebp 和 esp 这两个寄存器会同时指向 esp 原本所指向的位置。此时,ebp 和 esp 重合,这一操作往往是函数栈帧初始化过程中的重要步骤,它为后续在函数栈帧内进行数据的存储和访问奠定了基础

示意图:

第三条指令:【sub   esp,0E4h】

sub 指令用于执行减法运算,该指令的功能是将 esp 寄存器(栈顶指针)的值减去十六进制常数 0E4h(十进制为 228)

由于内存中的栈空间遵循 “向下增长” 的规则(即栈顶指针向低地址方向移动),当对 esp 执行减法操作时,栈顶指针会向低地址方向移动 0E4h 字节的距离。这一过程实际上是在为 main 函数在栈区开辟一段新的存储空间

通过 sub esp, 0E4h 指令,程序在栈顶指针原位置的基础上,向下扩展出 0E4h 字节的空间,用于存放当前函数的局部变量、临时数据或其他需要在栈帧中存储的信息。这是函数栈帧构建过程中的关键步骤,确保函数在执行时有独立的内存区域可供使用

示意图:

第四、五、六条指令:【push   ebx】 【push   esi】 【push   edi】

在 esp 指针为 main 函数成功开辟好栈空间之后,这里依次将 ebxesiedi 压入栈,会使得栈指针 esp 相应地向低地址方向移动,每个寄存器占用一定的栈空间。这样做的好处是,在 main 函数执行完毕后,可以通过出栈操作恢复这些寄存器的原始值,保证程序上下文的完整性,让程序能够正常地继续执行后续代码

示意图:

第七条指令:【lea   edi,[ebp-0E4h]】

lea 指令的作用是将ebp寄存器的值减去0E4h(十进制 228)后的地址加载到edi寄存器中

edi作为目标操作数指针,指向当前函数栈帧的起始位置(即esp开辟的空间底部)。此时ebp指向栈帧底部,ebp-0E4h正好是esp在执行sub esp, 0E4h后指向的位置

第八条指令:【mov   ecx.39h】

将十六进制数39h(十进制 57)赋值给ecx寄存器 

ecx作为计数器,用于控制后续重复操作的次数。由于每次stos指令处理 4 字节(DWORD),因此39h * 4 = 228字节,恰好等于之前开辟的栈空间大小0E4h 

第九条指令:【mov   eax,0CCCCCCCCh】 

0CCCCCCCCh赋值给eax寄存器,eax作为源操作数,存储要填充的值。在调试模式下,0xCCCCCCCC是微软编译器常用的未初始化内存标记值(对应 C++ 中的int 3断点指令),用于帮助检测未初始化变量的使用 

第十条指令:【rep stos     dwrd ptr es:[edi]】 

  • rep:重复执行后续指令,直到ecx为 0
  • stos:将eax的值存储到edi指向的内存位置,并根据方向标志(DF)调整edi
  • dwrd ptr es:[edi]:明确操作数为双字(4 字节),使用附加段寄存器es

eax中的0xCCCCCCCCh重复填充到edi开始的内存区域,共填充ecx次(即 228 字节)。每次填充后,edi自动增加 4 字节(因 DF 默认值为 0,即正向增长)

总结七、八、九、十条指令:

这四条指令的组合实现了对main函数栈空间的初始化,将其全部填充为0xCCCCCCCCh。这样做的目的是:

  1. 调试辅助:未初始化的变量会被自动填充为0xCC,当程序意外访问这些内存时会触发异常(INT 3断点),帮助开发者快速定位问题
  2. 内存清理:覆盖栈空间中的旧数据,确保函数执行环境的一致性

解析:

    int a= 10;
00C2142E  mov         dword ptr [ebp-8],0Ahint b=20;
00C21435  mov         dword ptr [ebp-14h],14hint c=0;
00C2143C  mov         dword ptr [ebp-20h],0
  1. int a = 10;
    该语句将整数 10 赋值给变量 a。对应的汇编指令将数值 0Ah(即十进制 10)写入到栈帧中相对于基址指针 ebp 偏移量为 8 字节的位置。这表明变量 a 在栈中的存储位置是 ebp-8,编译器为其分配了 4 字节空间(32 位整数),并初始化为 10

  2. int b = 20;
    该语句将整数 20 赋值给变量 b。对应的汇编指令将数值 14h(即十进制 20)写入到 ebp-14h(即偏移 20 字节)的位置。这说明变量 b 在栈中的位置是 ebp-20,同样被分配 4 字节空间,并初始化为 20

  3. int c = 0;
    该语句将整数 0 赋值给变量 c。对应的汇编指令将数值 0 写入到 ebp-20h(即偏移 32 字节)的位置。这表明变量 c 在栈中的位置是 ebp-32,分配 4 字节空间并初始化为 0

转汇编后(进入Add函数之前的部分) 

解析: 

  1. mov eax, dword ptr [ebp - 14h]:将栈帧中 ebp - 14h 地址处存储的双字数据(即变量 b 的值 20)读取到 eax 寄存器
  2. push eax:将 eax 中的值(b 的值)压入栈,作为 Add 函数的第二个参数
  3. mov ecx, dword ptr [ebp - 8]:将栈帧中 ebp - 8 地址处存储的双字数据(即变量 a 的值 10)读取到 ecx 寄存器
  4. push ecx:将 ecx 中的值(a 的值)压入栈,作为 Add 函数的第一个参数
  5. call 00C210E1:调用 Add 函数。该指令会先将返回地址压入栈,然后跳转到 00C210E1 处执行函数代码
  6. add esp, 8:调用函数后,栈中保留了两个参数(各占 4 字节,共 8 字节),通过 add esp, 8 调整栈指针,清理这两个参数占用的栈空间
  7. mov dword ptr [ebp - 20h], eax:将 Add 函数执行后的返回结果(存于 eax 寄存器)存储到栈帧中 ebp - 20h 地址处,即变量 c 的存储空间

 

解析:

在执行完上面的第五条 call 代码后,就会跳转到此条代码,执行时会直接跳转到 00C213C0 地址处。在函数调用流程中,它可能用于跳转到 Add 函数的实际代码位置,是编译器在生成代码时实现函数跳转的一种方式 

转汇编后(正式进入Add函数的部分)

解析: 

  1. push ebp

    将上一层函数的基址指针 ebp 压入栈中保存,用于后续恢复上一层栈帧
  2. mov ebp, esp

    将当前栈顶指针 esp 的值赋给 ebp,建立 Add 函数新的栈帧基址,为后续栈内变量访问提供基准
  3. sub esp, 0CCh

    通过减法操作,使栈顶指针 esp 向低地址方向移动 0CCh(十进制 204)字节,为 Add 函数在栈区开辟所需的局部变量存储空间
  4. push ebx / push esi / push edi

    依次将通用寄存器 ebxesiedi 的值压入栈中保存。这是为了在函数执行过程中保护这些寄存器的原始值,避免影响函数外的程序逻辑
  5. lea edi, [ebp+FFFFF34h]

    计算并加载目标内存地址到 edi 寄存器。ebp+FFFFF34h 等价于 ebp - 0CCh(十六进制补码运算),即指向刚开辟的栈空间起始位置,为后续内存初始化做准备
  6. mov ecx, 33h

    将循环次数 33h(十进制 51)赋值给计数器寄存器 ecx。由于每次操作处理 4 字节(双字),33h × 4 = 0CCh 字节,恰好匹配开辟的栈空间大小
  7. mov eax, 0CCCCCCCCh

    将特定值 0CCCCCCCCh 赋值给 eax 寄存器。在调试模式下,该值常用于标记未初始化的内存,便于检测程序错误(如访问未初始化变量)
  8. rep stos dword ptr es:[edi]

    重复执行 stos 操作,直到 ecx 为 0。每次操作将 eax 中的 0CCCCCCCCh 存储到 edi 指向的内存位置,并使 edi 自动递增 4 字节(正向增长)。最终效果是将 Add 函数的栈空间初始化为 0CCCCCCCCh
  9. mov dword ptr [ebp - 8], 0

    将数值 0 存储到 ebp - 8 地址处,即初始化局部变量 z 为 0ebp - 8 表示从栈帧基址 ebp 向低地址方向偏移 8 字节的位置,是为 z 分配的内存空间
  10. mov eax, dword ptr [ebp + 8]

    将 ebp + 8 地址处的双字数据(即函数参数 x 的值)读取到 eax 寄存器。在 x86 调用约定中,x 作为第一个参数,被压入栈后位于 ebp + 8 位置
  11. add eax, dword ptr [ebp + 0Ch]

    将 ebp + 0Ch 地址处的双字数据(即函数参数 y 的值)与 eax 中的值(x)相加,结果存回 eaxebp + 0Ch 是第二个参数 y 在栈中的位置
  12. mov dword ptr [ebp - 8], eax

    将 eax 中的值(x + y 的结果)存储到 ebp - 8 地址处,即把计算结果赋给局部变量 z

 

解析:

  1. mov eax, dword ptr [ebp - 8]

    将局部变量 z 的值(x + y 的结果)读取到 eax 寄存器。由于函数返回值通常通过 eax 传递,这一步为函数返回做准备
  2. pop edi / pop esi / pop ebx

    依次从栈中弹出数据,恢复寄存器 ediesiebx 的原始值,确保函数外的程序逻辑不受影响
  3. mov esp, ebp

    将栈顶指针 esp 恢复为栈帧基址 ebp 的值,释放 Add 函数开辟的栈空间,为栈帧销毁做准备
  4. pop ebp

    从栈中弹出数据,恢复上一层函数的基址指针 ebp,还原上一层栈帧结构
  5. ret

    从当前函数返回。该指令会从栈中弹出返回地址(即调用 Add 函数时压入的下一条指令地址),并跳转到该地址继续执行程序

总结

局部变量是怎么创建的? 

局部变量的创建,需在为函数分配栈帧空间并完成初始化之后,才会在栈帧内为其分配特定存储位置

函数是怎么传参的?

函数传参时,在调用函数前,参数会被存入存储器保存,同时相关寄存器压栈。传参遵循从右至左的顺序,这种方式便于函数通过指针偏移量定位参数,且在函数内部从左至右依次使用参数

形参和实参是什么关系?

形参在函数栈帧开辟前通过压栈分配空间,与实参值相同但存储空间独立。形参本质是实参的临时拷贝,其改变不会影响实参

函数调用的结果是怎么返回的?

函数调用结果的返回,依赖于调用 call 指令前对下一条指令地址的预先记录,以此确保能准确回到调用处。返回值通过寄存器传递,因函数执行完毕后,其栈帧及参数空间需释放归还操作系统,故提前将返回值存入寄存器,确保调用方获取结果

相关文章:

  • Kafka 解惑
  • 你对于JVM底层的理解
  • Python面向对象编程(OOP)深度解析:从封装到继承的多维度实践
  • Room持久化库:从零到一的全面解析与实战
  • 5. 动画/过渡模块 - 交互式仪表盘
  • 车载网关作为车辆网络系统的核心枢纽
  • spark MySQL数据库配置
  • 基于 Amazon Bedrock 和 Amazon Connect 打造智能客服自助服务 – 设计篇
  • 涌现理论:连接万物的神秘力量
  • Kafka、RabbitMQ、RocketMQ的区别
  • 地址簿模块-01.需求分析
  • python训练营day23
  • Spark,RDD中的行动算子
  • 深度剖析:Vue2 项目兼容第三方库模块格式的终极解决方案
  • 正则表达式常用验证(一)
  • 【python】—conda新建python3.11的环境报错
  • 无人机信号监测系统技术解析
  • 【Java】网络编程(Socket)
  • Mac上安装Mysql的详细步骤及配置
  • git-gui界面汉化
  • “一码难求”的Manus开放注册但价格不菲,智能体距离“实用”还有多远
  • 视频丨美国两名男童持枪与警察对峙,一人还试图扣动扳机
  • 湖南湘西州副州长刘冬生主动交代问题,接受审查调查
  • 习近平会见塞尔维亚总统武契奇
  • 上海市委常委会会议暨市生态文明建设领导小组会议研究基层减负、生态环保等事项
  • 《2025城市青年旅行消费报告》发布,解码青年出行特征