实用指南:函数的栈帧的创建和销毁(超详细版本图文丰富)

前言

嗨(^_−)☆,大家又见面啦,今天我们要讲的和前面有所不同,要深入到程序运行的“幕后舞台”——函数栈帧的创建与销毁。这是理解函数调用机制、内存管理的核心环节,每个函数执行时都会搭建专属的“临时内存舞台”,用完后再有序拆除。掌握它,你就能看透函数调用的底层逻辑,对排查栈溢出、理解递归执行等问题也会豁然开朗。可能有点难度,可是我们学会它对我们后面学习会有很大的帮助,那么接下来,我们就一起拆解该“舞台”的搭建与谢幕全过程~

我把它放在数据结构的栈后面,方便大家更好的理解
通过也能够在C语言中学习,但是我感觉在栈后面学更好
在这里插入图片描述

一、为什么要理解函数栈帧

我们在学这篇之前,我们是不是想要了解它能对我们有什么帮助,能帮我们消除什么问题呢,纵然我已经在前言中提到了,可是可能不够形象,所以我们在这里再具体说一下
在大家之前的学习中,是不是有这样的疑问:

1、局部变量是如何创建的?
随机值?就是2、为什么局部变量的值
3、函数是怎么进行传参的?以及传参的顺序是怎么样的?
4、形参和实参的关系是怎么样的?
5、函数调用是怎么做到的?
6、函数调用结束后又是怎样返回的?

这些挑战都与函数栈帧有关,只要理解了函数栈帧的创建和销毁,这些问题就能够很好地理解了,让我们一起走进函数栈帧的创建和销毁的过程中吧!

二、了解函数栈帧

概念:函数栈帧(stack frame)就是函数调用过程中在应用的调用栈(call stack)所开辟的空间,这些空间是用来存放:函数参数和函数返回值,临时变量,保存上下文信息等等;
栈帧的结构
栈帧是函数调用时在栈内存里的专属“小隔间”,核心结构可简单拆为4部分:

  • 返回地址:函数执行完后,跳回调用者代码的位置。
  • 栈基址指针(ebp/esp):ebp指向当前栈帧的底部,esp指向当前栈帧的顶部,通过这两个指针来维护函数栈帧。
  • 局部变量区:存函数里的临时变量(比如int a、数组),内存连续分配。
  • 参数区:用于存放调用函数时传递给被调用函数的参数;

栈是从高地址往低地址生长的,这些部分在栈里按顺序排布,函数执行完就会销毁这个“小隔间”~

三、函数栈帧的栈帧和销毁的相关知识

1、什么是栈?

这里的栈和我们数据结构的栈不同,但核心十分相似,所以我把它放在数据结构的栈后面进行讲解

(1)栈的概念

这里的栈为程序运行中的栈(栈内存)
栈是现代计算机程序的核心概念之一,可从逻辑定义系统实现两个维度总结:

  • 逻辑特性
    栈是遵循“先进后出(FILO)”规则的特殊容器,支持“入栈(push,资料压入)”和“出栈(pop,数据弹出)”操作,类似“叠书”——先叠的书在最下、最后取出。

  • 系统实现
    在计算机系统中,栈是动态内存区域,压栈操作使栈内存增大,弹栈操作使栈内存减小;其内存增长方向为从高地址向低地址,在i386或x86-64架构下,由esp寄存器定位栈顶。

  • 程序意义
    栈是函数调用、局部变量存储的基础,几乎所有工具的函数、局部变量效果都依赖栈实现,是计算机语言运行的核心支撑。

(2)数据结构中的栈和程序运行的栈的对比

这里的“栈”需从两个维度理解,二者概念不同但核心逻辑(先进后出)高度关联:

维度数据结构的“栈”(抽象逻辑)程序运行的“栈”(物理内存)
本质一种“先进后出”的逻辑数据模型计算机内存中一块连续的物理区域
存在形式仅在算法/代码设计中存在(如数组模拟栈)真实占用物理内存,由操作系统管理分配
操作主体开发者手动实现 push/pop 等接口编译器+CPU 自动执行(函数调用、局部变量存储时触发)
核心规则严格遵循“先进后出”同样遵循“先进后出”,是函数栈帧创建/销毁的底层支撑

简言之,数据结构的“栈”是逻辑规则,程序中的“栈”是该规则的物理实现。函数栈帧的创建(压栈)、销毁(出栈),就是借助“栈内存”的先进后出特性来完成的——这也是两者的核心关联。
今天我们讲的就是程序运行的栈(物理内存)

2、相关寄存器和汇编指令

(1)相关寄存器

  • eax:通用寄存器,保留临时素材,常用于函数返回值。
  • ebx:通用寄存器,保留临时数据。
  • eip:指令寄存器,存储下一条要执行的指令的地址。
  • ebp:栈底寄存器。
  • esp:栈顶寄存器。

注意

  • 函数栈帧的空间。就是每一次函数调用,都要为本次函数调用开辟空间,就
  • 这块空间的维护是使用了2个寄存器:esp和ebp,ebp记录的是栈底的地址,esp记录的是栈顶的地址。
    在这里插入图片描述
    esp指向栈顶,也叫栈顶指针;ebp指向栈底,也叫栈底指针。
    所以,esp维护的是栈顶(顶部),ebp维护的是栈底(底部)。

(2)相关汇编指令(后面要用)

  • push:将操作数压入栈中,同时调整栈顶指针esp。
  • pop:从栈中弹出数据到指定位置,同时调整栈顶指针esp。
  • mov:数据传送指令,在寄存器之间、寄存器与内存之间传送数据。
  • add:加法指令,将两个操作数相加,结果存于指定寄存器。
  • sub:减法指令,将两个操作数相减,结果存于指定寄存器。
  • call:过程调用,压入返回地址或转入调用函数。
  • lea:加载有效地址指令,将操作数的地址加载到指定寄存器。
  • ret:返回地址指令,回到调用位置。

四、函数的栈帧的创建和销毁

1、 函数的调用堆栈

下面我们来实现函数的调用堆栈,我们先来一个简单的代码来观察
代码如下:

#include<stdio.h>
  int Add(int x, int y)
  {
  int z = 0;
  z = x + y;
  return z;
  }
  int main()
  {
  int a = 10;
  int b = 20;
  int c = 0;
  c = Add(a, b);
  printf("%d\n", c);
  return 0;
  }

这段代码对应的栈区的图如下
在这里插入图片描述

大家进行调试窗口中的调用堆栈来观察
在这里插入图片描述
按F10进行调试,大家知道Add是被main调用的但我们发现main也是被调用的,是被谁调用的呢
在这里插入图片描述
我们继续按F10,我们会发现我们到这个页面
在这里插入图片描述
我们可以从这个页面向上翻,可以看出main是被 __tmainCRTStartup()函数调用的,再按F10可以看出 __tmainCRTStartup()函数也是被mainCRTStartup调用的,如下图
在这里插入图片描述
在这里我们只需要理解它的调用逻辑就行啦,
总的来说可以用箭头表示,箭头表示被调用,表示main被调用过程
main --> __tmainCRTStartup --> mainCRTStartup
由高地址到低地址,所以函数的栈帧分配如下图:就是大家每次调用函数都要分配相应的空间,而程序的运行是从高地址到底地址,所以函数分配的空间也

在这里插入图片描述
我们现在已经有了大概的轮廓啦,接下来我们来看一下它的具体调用过程

2、函数的栈帧的创建和销毁的具体过程

可能大家还有所疑惑,那么接下来我们来看一下具体的调用过程
这里还是F10调试,直接右键点反汇编

(1)观察前的操作

在这里插入图片描述
通过这里屏幕右边就出现了C语言的汇编代码,能够看清每一步的操作啦
在这里插入图片描述

我们这里还要把符号名关掉,便于我们观察
在这里插入图片描述
在这里插入图片描述

(2)开始观察

因为main是由_tmainCRTStartup调用的,所以刚开始内存布局如图:
在这里插入图片描述

1、push ebp

图形理解

这里相当于压栈,加个元素进去,将_tmainCRTStartup的ebp元素压栈
在这里插入图片描述
在这里插入图片描述

由于esp维护栈顶元素。所以esp的地址也要向上移动,如图
在这里插入图片描述

地址理解

我们也允许通过看一下esp的地址变化
在这里插入图片描述
F10
在这里插入图片描述
esp的地址-4,所以向上走了

内存块观察

我们还许可通过内存块来观察
在这里插入图片描述
地址搜索esp可看出
这里根据大小端字节序,由于VS是小端,所以要倒着读,地址就是与008ffbf4相同,说明我们把该元素给压进去啦

在这里插入图片描述

2、mov ebp,esp

接下来是mov,这里就把图形和地址观察放在一起啦
mov 就是把后面的值赋给前面去·
在这里插入图片描述
在这里插入图片描述

3、(创建main的栈帧)sub esp,0E4h

sub就是将前一个元素的值-去一个值
这里0E4h是16进制数字,我们可以借助监视来显示
在这里插入图片描述
在这里插入图片描述
我们发现它是228
下面进行操作
esp-228,esp向上移动
在这里插入图片描述
这里就相当于esp和ebp预开辟出main函数的栈帧,如图:
在这里插入图片描述

4、push ebx 、push esi 、push edi

这里再压3个元素这里ebx,esi,edi不用管他们是什么
在这里插入图片描述

5、(初始化)lea edi,[ebp+FFFFFF1Ch]、mov ecx,39、eax,0CCCCCCCCh,ret stos dword ptr es:[edi]

lea加载有效地址
这里我们把显现符号名再加上就好啦

在这里插入图片描述

  • 就是将后面的地址放在edi中,这里放的这个地址就是esp没有进行上一步操作时的地址
  • mov ecx,39h中39h次是一会儿在这进行复制的时候准确的一个次数,ecx,39h就是把39h的值放到ecx这个寄存器里面。
  • 而ecx,OCCCCCCCh表示一会复制的内容
  • 这一次的所以操作,主要是末了一步起作用,将edi(ebp-0E4h)向下39h次dword(4个字节)的数据全部初始化成这样的素材
  • (word为2个字节,dword(double word)4个字节)
    大家可能从内存中看到的确初始化成了,这里只展示一部分
    在这里插入图片描述
    我们还用图来表示一下
    在这里插入图片描述

6、3个mov,正式开始执行代码

这里我们还是把显示符号名去掉
在这里插入图片描述
其中0Ah=10,14h=20,0=0,
所以这三步是对a,b,c进行赋值
其中dword ptr 表示是指明操作数类型为指向 4 字节数据的指针
ebp为栈低指针,其中ebp-8,-14h,-20h表示对应a,b,c三个变量的地址
这里用图来表示,main中一个小格子表示4个字节
在这里插入图片描述

7、进入函数调用前

先讲传参过程
在这里插入图片描述
这里Add函数传参数,从右向左读取

在这里插入图片描述

这里先把b元素的数据传到eax这个寄存器中,再进行压栈
a的同理
在这里插入图片描述
进行完后
在这里插入图片描述

8、进入函数调用 call

接下来我们就要正式进入Add函数中

在这里插入图片描述

call函数为调用函数,这里我们就不要按F10啦,要按F11
按之前
在这里插入图片描述
通过按之后,大家能够发现call在调用时call又把下一个地址压进去了
在这里插入图片描述
就形成了这样,这是为了在执行完调用函数时,方便继续执行代码

在这里插入图片描述
再按F11,就真正进到调用函数里面了
在这里插入图片描述
在这里插入图片描述

我们发现下面绿色框起来的,和我们之前执行过的一样,这里我就不再解释了
在这里插入图片描述
执行后
在这里插入图片描述
接下来,我们来执行Add函数中的指令

在这里插入图片描述
先初始化z=0
在这里插入图片描述
在这里插入图片描述
再将ebp+8所对应的值传给eax,而ebp+8对应的就是当时大家第7步是进行的传参过程对应的a的值,下一个add将eax(a)的值与ebp+0ch(b)相加,相当于eax+=b;
在这里插入图片描述

最后把eax得到的值传给对应的z,但是程序结束要销毁,但还得返回z的值,所以我们再把z的值传给寄存器eax
所以我们发现
传参数时参数从左向右读取,调用时参数从左向右调用

完整示意图
在这里插入图片描述

9、调用函数的销毁

在这里插入图片描述
这里就不做解释了,其中mov操作就把调用函数给销毁啦
pop ebp
这个处理就是把ebp栈底指针,回到main的栈底
ret
这个就是从栈顶弹出call的下一条指令
再按F10我们发现,回来main了
在这里插入图片描述

10、继续main函数的操作

在这里插入图片描述
add esp,8就是将esp向下移动8个字节,也就是把传参的临时变量销毁

在这里插入图片描述
这个操作将eax的值给ebp-20h对应的,也就是将返回值给C
函数栈帧的创建和创建的具体过程就讲的差不多了,后面的操作就不进行讲解啦

五、简单回顾

现在大家来简单回顾一下函数的栈帧的创建和销毁

  1. 先是函数的调用堆栈
    了解函数调用栈帧的简单过程
    在这里插入图片描述

  2. 再是转到反汇编来看函数栈帧的创建

  3. 创建

  4. 存“返回地址”(函数执行完回哪里);

  5. 存“调用者的栈底(ebp)”;

  6. 设“当前函数的栈底(ebp)”;

  7. 为局部变量“开辟栈空间”。

  8. 销毁

  9. 释放局部变量的栈空间;

  10. 恢复“调用者的栈底(ebp)”;

  11. 跳回“返回地址”继续执行。

  12. 关于参数
    传参数时参数从左向右读取,调用时参数从左向右调用
    核心就是**“先建环境执行函数,再清环境回归调用者”**,全程靠栈“先进后出”的规则保障。

六、总结

我们来回顾先前的疑问
在这里插入图片描述

1. 局部变量是如何创建的?

首先为函数分配好栈帧空间,栈帧空间里面我们初始化完一部分空间之后,再给局部变量在栈帧里面分配空间。

2. 为什么局部变量的值是随机值?

如果局部变量不初始化,那么它就会得到随机值,我们设置变量自动赋的随机值,如下:
在这里插入图片描述
如果初始化就会把随机值覆盖掉

什么样的?就是3. 函数调用时参数是如何传递的?传参的顺序

函数调用时,参数通常按约定顺序(如从右到左)压入栈中,被调用函数通过栈地址偏移读取参数。

4. 形参和实参的关系是怎么样的?

形参是函数定义时用于接收数据的临时变量,实参是调用时传入的实际数据,调用时实参的值传递给形参,形参仅为实参的副本,对形参的修改(值传递下)不影响实参。

5. 函数调用是怎么做到的?

如上文

6. 函数调用结束后又是怎样返回的?

函数调用结束后,会先将返回值存入指定寄存器(如x86的eax),再销毁当前栈帧(mov esp, ebp释放局部变量、pop ebp恢复调用者栈底),最后执行ret指令弹出返回地址,跳回调用者的下一条指令继续执行。

7、注意

传参数时参数从左向右读取,调用时参数从左向右调用
通过我们创建的函数栈帧理论上能够被占满,但是我们的编译器会解决这个障碍不会让他慢的

八、结束语

我觉得学完数据结构的栈再来学更容易理解,两个栈的核心十分相同。本篇有点难度,不用全部掌握,只需要理解就行啦!本篇如果博主有写的不好的地方,欢迎大家在评论区建议或讨论!感谢大家的支持啦!就是嗨|ू・ω・` ),本文到这里就结束啦,我把它放在数据结构和C语言中,但
在这里插入图片描述

posted @ 2025-12-18 10:07  clnchanpin  阅读(23)  评论(0)    收藏  举报