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

【函数栈帧的创建和销毁】 -- 神仙级别底层原理,你学会了吗?

文章目录

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语言汇编语言 汇编:汇编->可重定位目标二进制文件,不可以被执行&#xff0…...

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 次):选择两个相邻的砖块,反转它们的颜色。(黑变白&#xf…...

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&#xff0…...

《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…...

手游刚开服就被攻击怎么办?如何防御DDoS?

开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

MFC内存泄露

1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

【JVM】- 内存结构

引言 JVM:Java Virtual Machine 定义:Java虚拟机,Java二进制字节码的运行环境好处: 一次编写,到处运行自动内存管理,垃圾回收的功能数组下标越界检查(会抛异常,不会覆盖到其他代码…...

libfmt: 现代C++的格式化工具库介绍与酷炫功能

libfmt: 现代C的格式化工具库介绍与酷炫功能 libfmt 是一个开源的C格式化库,提供了高效、安全的文本格式化功能,是C20中引入的std::format的基础实现。它比传统的printf和iostream更安全、更灵活、性能更好。 基本介绍 主要特点 类型安全&#xff1a…...

Vue 模板语句的数据来源

&#x1f9e9; Vue 模板语句的数据来源&#xff1a;全方位解析 Vue 模板&#xff08;<template> 部分&#xff09;中的表达式、指令绑定&#xff08;如 v-bind, v-on&#xff09;和插值&#xff08;{{ }}&#xff09;都在一个特定的作用域内求值。这个作用域由当前 组件…...

智能职业发展系统:AI驱动的职业规划平台技术解析

智能职业发展系统&#xff1a;AI驱动的职业规划平台技术解析 引言&#xff1a;数字时代的职业革命 在当今瞬息万变的就业市场中&#xff0c;传统的职业规划方法已无法满足个人和企业的需求。据统计&#xff0c;全球每年有超过2亿人面临职业转型困境&#xff0c;而企业也因此遭…...

ui框架-文件列表展示

ui框架-文件列表展示 介绍 UI框架的文件列表展示组件&#xff0c;可以展示文件夹&#xff0c;支持列表展示和图标展示模式。组件提供了丰富的功能和可配置选项&#xff0c;适用于文件管理、文件上传等场景。 功能特性 支持列表模式和网格模式的切换展示支持文件和文件夹的层…...

机器学习的数学基础:线性模型

线性模型 线性模型的基本形式为&#xff1a; f ( x ) ω T x b f\left(\boldsymbol{x}\right)\boldsymbol{\omega}^\text{T}\boldsymbol{x}b f(x)ωTxb 回归问题 利用最小二乘法&#xff0c;得到 ω \boldsymbol{\omega} ω和 b b b的参数估计$ \boldsymbol{\hat{\omega}}…...

Appium下载安装配置保姆教程(图文详解)

目录 一、Appium软件介绍 1.特点 2.工作原理 3.应用场景 二、环境准备 安装 Node.js 安装 Appium 安装 JDK 安装 Android SDK 安装Python及依赖包 三、安装教程 1.Node.js安装 1.1.下载Node 1.2.安装程序 1.3.配置npm仓储和缓存 1.4. 配置环境 1.5.测试Node.j…...

Copilot for Xcode (iOS的 AI辅助编程)

Copilot for Xcode 简介Copilot下载与安装 体验环境要求下载最新的安装包安装登录系统权限设置 AI辅助编程生成注释代码补全简单需求代码生成辅助编程行间代码生成注释联想 代码生成 总结 简介 尝试使用了Copilot&#xff0c;它能根据上下文补全代码&#xff0c;快速生成常用…...