【函数栈帧的创建和销毁】 -- 神仙级别底层原理,你学会了吗?
文章目录
1.函数的调用方式
2.函数在栈区上的动作
1.函数的调用方式
相信你对调用函数一点都不陌生,但是在调用函数的过程中,却存在着很多你无法见到的东西,这是底层信息,想要理解透彻,就得深入底层去观察。
本文以一个最简单的加法函数为例,深入讲解内存空间中的每一条指令。
int Add(int x, int y)
{int z = 0;z = x + y;return z;
}int main()
{int a = 10;int b = 20;int c = Add(a, b);printf("%d\n", c);return 0;
}
这是源码,以该源码为例。
首先,我们进入调式

按如下图所示进行操作。
转到反汇编后,开始观察每一条代码的执行指令,在开始之前,先提出几个常见的问题:
1.局部变量是怎么创建的?
2.局部变量未初始化为什么是随机的?
3.函数是怎么传参的?传参的顺序是怎么样的?
4.形参和实参是什么关系?
5.函数的调用是怎么做的?
6.函数调用结束后是怎么返回的?
以上问题,都会通过下面的函数栈帧一一为你解答。
以下的讲解都是以低地址处在相对高的位置,高地址在相对低的位置,如下图:

记住,每一个函数的调用,都会在栈区开辟一块内存空间。
栈空间的使用习惯是:从高地址向低地址使用。
我们在main函数被调用时,会在栈区开辟一块内存空间,如下图:
实际上,main函数也是被其他函数调用的,所以,在main函数开辟栈空间之前,一定会先开辟调用main函数的函数的栈空间。
在VS2019的环境下:

通过调用堆栈我们可以看到,main函数是被一个叫做invoke_main的函数调用的,
而该函数又是被一个叫做main_result 的函数调用的

这样逐层调用下去。所以,main函数也是被编译器中的其他函数调用的。
具体的函数调用多少,取决于不同的编译器实现。
所以,调用main函数的函数先在栈区开辟一块空间。
2.函数在栈区上的动作
首先,回到反汇编代码中,

在执行第一条 int a = 10语句之前,有许许多多的反汇编代码。
先看第一条:
ebp是一个寄存器,push ebp,是将寄存器压栈,压入栈空间的顶部。
那么寄存器是什么呢?压栈是什么呢?
先看下图:

在栈空间中,一块函数栈空间是由寄存器来维护和使用的。
两个不同的寄存器足以维护它们之间的栈空间,并且寄存器和函数地址是毫不相干的,寄存器是一个真实存在的东西,任何代码任何地方都可以使用它。
在main函数的栈空间中,使用的方式是从栈顶往栈底压栈的,所以ebp和esp两个寄存器可以形象地称为栈底指针和栈顶指针。
在执行了第一条汇编指令后,ebp寄存器中的值就被压到了调用main函数的函数的栈空间顶部。
不是ebp本身被压栈,ebp寄存器是个真实存在的东西,不可能会被真的压栈,压栈压得是ebp存放的值。

我们查看寄存器的值发现,ebp的值存放的是一个地址,该地址就是上图中的栈底指针所指向的那个地方的地址:
如下图:

而在压栈结束后,esp这个寄存器指向的地址会往上走,也就是会往低地址处走,因为它是栈顶指针。
如下图:

我们可以验证一下:

esp的值从0x00D0FADC变成了0x00D0FAD8
证实了上述的动作。
可能你会有个疑问:将ebp压栈有什么用呢?
将ebp压栈是为了记录ebp和esp最开始维护的栈空间的地址,以后ebp被调用到其他地方的时候,栈空间的地址仍然被记录,很有效的防止栈空间丢失的现象。
在执行完第一条汇编语句后,接下来执行第二条汇编语句:

该汇编语句的意思是: 将esp的值move 到ebp ,也就是把esp的值赋给ebp。
也就是说,ebp此时指向了esp指向的地址:如下图:

此时ebp和esp都指向了同一个位置,

执行第二个语句后,esp和ebp存放的值相同了。
前面我们讲过,一块栈空间是由两个寄存器来维护的,现在两个寄存器都指向了同一个位置,那之前的空间不会丢失了吗?
这就回答了第一个汇编语句:将ebp压栈的作用,此时已经记录了ebp和esp在最开始所维护的空间的地址,保证开辟的栈空间不会被忘记。
接下来执行第三条汇编语句:

这条语句的意思是:将esp存放的值减去0E4h, sub就是减法的意思。 0E4h其实是一个16进制数字,只是方便编译器识别而这样设计的,具体这个值是多少我们可以看一下,不过不需要去了解,这是为编译器使用的。

esp的值减去一个值,结果当然会更小,所以esp会往上走,因为低地址是在上方,所以esp会走到上面的某一个区域。如下图:

可以验证一下:esp存放的值现在是0x00D0FAD8,执行了该汇编指令后,esp的值是:0x00D0F9F4,明显小于之前的,所以证实了esp往低地址走了。

接下来执行第四条汇编指令:

ebx也是一个寄存器,该汇编指令就是把ebx压栈。如下图:

那么在执行完压栈操作后,esp又会往上走一走,

执行来看一下:

esp存的地址的的确确又往低地址处走了,之前是F4,现在是F0(地址的后两位),也就是走了4个字节。
那么,介于ebp和esp的那么大一块的空间是干嘛的呢?
下面的汇编指令会给你解答。
接下来继续执行两条压栈的汇编指令

依然是将edi这个寄存器的值压栈,

随后执行的汇编指令是:

先看第一个:lea的意思是 load effective address ,加载有效地址,
将ebp-24h的值加载到edi中,ebp的值是一个地址,ebp-24h依然是一个地址。
接下来是mov ecx 9,也就是将9赋值给ecx寄存器。
然后是将0CCCCCCCCh 赋给eax这个寄存器。
这三条语句是为下面这条语句做铺垫的,真正起作用的也是这条语句:

dword的意思是double word,word是字,单词的意思,dword就是两个字,两个单词, 一个字是两个字节,那两个字就是四个字节。
该语句的意思是:

将从edi开始的9个数量的地址全部改成0CCCCCCCCh。
多读几遍,你就读懂了,接下来验证一下:

执行该汇编代码之后,情况是这样的:

把刚才那块空间全部复制成cccccccc,现在可以解释上面的问题了:这块空间就是专门为main函数开辟的。

接下来执行的汇编语句(黄色箭头)易于理解,把0AC003H这个值存入到ecx寄存器中。
真正厉害的是接下来红色箭头指向的这一条汇编语句,请注意,在执行call指令的同时,
call指令会自动把下一条汇编代码的地址进行压栈! 如下图所示:

call指令在执行的同时就会做这件事情,把call指令的下一条指令的地址进行压栈!
那么这件事情到底有什么作用呢?
这里先把问题放这里
接下来我们按F11,

似乎此时发现了新大陆!
我们可能看不懂那些代码是什么意思,没关系,这不重要。
重要的是刚刚说的一句话:call指令在执行的时候会把它的下一条汇编指令的地址先进行压栈!
回到call指令那个地方

这里的调用指令,似乎可以理解成call指令调用内存区的已经建立好的函数。
再次点击F11,可以看到确确实实是在调用内存中已经建立好的函数。

在众多汇编代码中,真正重要的是这一句代码,与刚才的call指令形成一致,在ret就是return的意思,执行完这一句汇编代码之后,一定会出现的事情是:返回到call的下一条指令处。

点击F11,,可以看到真的回到了call的下一条指令的位置。
这就是代码的严谨的地方,不仅能出去调用函数,还会记住原来的位置并且回来。
接下来就是把a的值存入内存中:

看这两个地方,在执行了这条汇编代码之后,a被存入内存中了。
接下来就存b,然后接下来,就是在为Add函数的调用做准备工作了。

首先push ebp,对ebp寄存器进行压栈操作,为什么压栈前面已经讲过,压栈就是为了我们在开辟栈空间的时候,为了有效地记录栈空间的栈底地址而进行的操作。
与main函数的开辟如出一辙,接下来就是把esp的值给ebp,其实就相当于把ebp移动到esp的位置。
注意:在移动之前,进行的压栈操作,就是为了记录栈底空间的地址,以后调用函数结束后返回时可以找到该地址。
在main函数调用的时候也进行了压栈的操作。这些过程是相当严谨的。
接下来就是把esp的值-0CCh,就是为Add函数开辟了一块空间。

Add函数调用完成后,最重要的工作来了,如何销毁栈空间?
是这样销毁的:

pop有删除的意思,在这里是把edi弹出栈空间,然后再把edi的值赋给edi,总的来说就是弹出寄存器。
前面三个均是如此,但是最后一个弹出ebp,不知你是否还记得,我们在创建main函数和Add函数的时候,先是对ebp寄存器进行压栈的!
所以压栈的作用在这里就凸显出来了:
在弹出ebp寄存器的之后,会把ebp寄存器里面的值交给ebp。
也就是说:弹出ebp之后,ebp又记录了当时存在那个地方的值。

所以ebp就回到了之前存的栈底位置的地址。
这样Add函数的销毁就完成了。
因为一块函数栈帧空间,是由两个寄存器共同维护的。现在寄存器esp回去了,那么这块栈帧空间就会归还给操作系统。
同理,对于main函数也是如此。
总结:
每一次函数的调用,都会在栈区开辟一块空间,这块空间是为调用函数准备的,而在开辟的过程中,存在着许许多多的细节,动作,来保证整个过程的严谨性。
在创建栈帧的同时也考虑到调用完函数之后销毁的过程,整个逻辑是很清晰的。
阅读汇编代码,了解汇编指令在函数调用时发挥的作用对我们的帮助是很大的,相当于我们在修炼内功。
相关文章:
【函数栈帧的创建和销毁】 -- 神仙级别底层原理,你学会了吗?
文章目录1.函数的调用方式 2.函数在栈区上的动作 1.函数的调用方式 相信你对调用函数一点都不陌生,但是在调用函数的过程中,却存在着很多你无法见到的东西,这是底层信息,想要理解透彻,就得深入底层去观察。 本文以…...
Promise的使用及原理
此文章主要讲解核心思想和基本用法,想要了解更多细节全面的使用方式,请阅读官方API 这篇文章假定你具备最基本的异步编程知识,例如知道什么是回调,知道什么是链式调用,同时具备最基本的单词量,例如page、us…...
怎么拥有一个帅气的 CMD 命令窗口 ❓ - Windows
自从拥有这样一个炫酷的命令窗口,我都舍不得关掉它了 关于我为什么我要闲的去 “打扮” 一个命令窗口,这要从星期五下午的一场 摸鱼 🐠 开始,当时我要创建一个 vue ts vite 的项目练练手,为新项目开始做准备&#x…...
时隔多年再学习Vuex,什么?原来如此简单!
时隔多年再学习Vuex,什么?原来如此简单! start 写 Vue 写了好多年了,少不了和 Vuex 打交道。虽然使用它的次数非常频繁,但是潜意识里总觉得这东西很难,导致遇到与之相关的问题就容易慌张。时至今日,升级版…...
Linux笔记_gcc
Linux_gcc程序的翻译链接库make与makefile关于gcc的一些笔记。 程序的翻译 gcc/g是一个编译器。 预处理:头文件展开、条件编译、宏替换、去注释 编译:C语言汇编语言 汇编:汇编->可重定位目标二进制文件,不可以被执行࿰…...
2023美赛MCM A题 详细思路
2023美赛(MCM/ICM)如期开赛,为了尽早的帮大家确定选题。这里我们加急为大家编辑出A赛题详细思路,方便大家快速对A题目的难度有个大致的了解。同时,我们也给出了A题目简要的解题思路,以及该问题在实际解决中可能会遇到的难点。A题的…...
c#: NetTopologySuite凹凸多边形计算
环境: .net 6.0NetTopologySuite 2.5.0vs2022平面二维 一、夹角计算 1.1 计算向量与x轴正方向的夹角 方法: AngleUtility.Angle(Coordinate p) 下图上的t2即为p,之所以这么写是为了和AngleUtility.AngleBetweenOriented做比较 注意: 结果…...
NFT Insider #86:A16z 领投,YGG 获得 1380 万美元融资,The Sandbox与《北斗神拳》合作
引言:NFT Insider由NFT收藏组织WHALE Members、BeepCrypto联合出品,浓缩每周NFT新闻,为大家带来关于NFT最全面、最新鲜、最有价值的讯息。每期周报将从NFT市场数据,艺术新闻类,游戏新闻类,虚拟世界类&#…...
Sort_Algorithm
排序算法前言插入排序折半插入排序希尔排序冒泡排序快速排序选择排序堆排序归并排序前言 排序算法:将一堆数据元素按关键字递增或者递减的顺序,进行排序。 排序算法的评价指标:时间复杂度,空间复杂度,算法稳定性。 算…...
【初探人工智能】2、雏形开始长成
【初探人工智能】2、雏形开始长成【初探人工智能】2、雏形开始长成安装Flask封装Web接口雏形设置接收参数功能验证聊天写代码代码补全生成图片写在后面笔者初次接触人工智能领域,文章中错误的地方还望各位大佬指正! 【初探人工智能】2、雏形开始长成 在…...
【LeetCode】剑指 Offer(2)
目录 写在前面: 题目: 题目的接口: 解题思路: 代码: 过啦!!! 写在最后: 写在前面: 今天的每日一题好难,我不会dp啊啊啊啊啊啊。 所以&am…...
【JavaSE】Lambda、Stream(659~686)
659.每天一考 1.写出获取Class实例的三种常见方式 Class clazz1 String.class; Class clazz2 person.getClass(); //sout(person); //xxx.yyy.zzz.Person... Class clazz3 Class.forName(String classPath);//体现反射的动态性2.谈谈你对Class类的理解 Class实例对应着加载…...
有限差法(Finite Difference)求梯度和Hessian Matrix(海森矩阵)的python实现
数学参考 有限差方法求导,Finite Difference Approximations of Derivatives,是数值计算中常用的求导方法。数学上也比较简单易用。本文主要针对的是向量值函数,也就是f(x):Rn→Rf(x):\mathbb{R^n}\rightarrow \mathbb{R}f(x):Rn→R当然&…...
day33 贪心算法 | 1005、K次取反后最大化的数组和 134、加油站 135、分发糖果
题目 1005、K次取反后最大化的数组和 给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。) 以这种方式修改…...
《蓝桥杯每日一题》递推·AcWing 3777. 砖块
1.题目描述n 个砖块排成一排,从左到右编号依次为 1∼n。每个砖块要么是黑色的,要么是白色的。现在你可以进行以下操作若干次(可以是 0 次):选择两个相邻的砖块,反转它们的颜色。(黑变白…...
mysql读写分离(maxscale)
1. 环境架构 需要三台服务器。192.168.2.10(master)192.168.2.20(slave)192.168.2.30(maxscale) 2. 部署mysql主从同步 mysql主从同步可以参考mysql主从同步 3. 部署maxscale服务 MaxScale中间件软件 …...
第八章 - 数据分组( group by , having , select语句顺序)
第八章 - 数据分组 group by数据分组过滤分组 having分组排序groub by语句的一些规定select语句顺序数据分组 在使用group by进行分组时,一般都会配合聚合函数一起使用,实现统计数据的功能。比如下面例子,需要按性别计算人数。按性别进行分组…...
Git(GitHub,Gitee 码云,GitLab)详细讲解
目录第一章 Git 概述1.1 何为版本控制1.2 为什么需要版本控制1.3 版本控制工具1.4 Git 简史1.5 Git 工作机制1.6 Git 和代码托管中心第二章 Git 安装第三章 Git 常用命令3.1 设置用户签名3.2 初始化本地库3.3 查看本地库状态3.3.1 首次查看(工作区没有任何文件&…...
策略模式(Strategy Pattern)
编写鸭子项目,具体要求如下: 1) 有各种鸭子(比如 野鸭、北京鸭,水鸭等,鸭子有各种行为,比如 叫,飞行等) 2)显示鸭子的信息 传统方案解决鸭子问题 1࿰…...
《Qt6开发及实例》6-2 Qt6基础图形的绘制
目录 一、绘图框架设计 二、绘图区的实现 2.1 PaintArea类 2.2 PaintArea类讲解 三、主窗口的实现 3.1 MainWidget类 3.2 MainWidget类讲解 3.3 槽函数编写 3.5 其他内容 一、绘图框架设计 界面 两个类 二、绘图区的实现 2.1 PaintArea类 paintarea.h #ifndef…...
【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试
作者:Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位:中南大学地球科学与信息物理学院论文标题:BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接:https://arxiv.…...
理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...
MySQL 主从同步异常处理
阅读原文:https://www.xiaozaoshu.top/articles/mysql-m-s-update-pk MySQL 做双主,遇到的这个错误: Could not execute Update_rows event on table ... Error_code: 1032是 MySQL 主从复制时的经典错误之一,通常表示ÿ…...
LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用
中达瑞和自2005年成立以来,一直在光谱成像领域深度钻研和发展,始终致力于研发高性能、高可靠性的光谱成像相机,为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...
uniapp获取当前位置和经纬度信息
1.1. 获取当前位置和经纬度信息(需要配置高的SDK) 调用uni-app官方API中的uni.chooseLocation(),即打开地图选择位置。 <button click"getAddress">获取定位</button> const getAddress () > {uni.chooseLocatio…...
AT模式下的全局锁冲突如何解决?
一、全局锁冲突解决方案 1. 业务层重试机制(推荐方案) Service public class OrderService {GlobalTransactionalRetryable(maxAttempts 3, backoff Backoff(delay 100))public void createOrder(OrderDTO order) {// 库存扣减(自动加全…...
