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

news2024/10/1 17:22:39

文章目录

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函数也是如此。

总结:
每一次函数的调用,都会在栈区开辟一块空间,这块空间是为调用函数准备的,而在开辟的过程中,存在着许许多多的细节,动作,来保证整个过程的严谨性。
在创建栈帧的同时也考虑到调用完函数之后销毁的过程,整个逻辑是很清晰的。

阅读汇编代码,了解汇编指令在函数调用时发挥的作用对我们的帮助是很大的,相当于我们在修炼内功。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/352801.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring之AOP底层源码解析

Spring之AOP底层源码解析 1、动态代理 代理模式的解释:为其他对象提供一种代理以控制对这个对象的访问,增强一个类中的某个方法,对程序进行扩展。 举个例子 public class UserService {public void test() {System.out.println("test.…

LeetCode-216. 组合总和 III

目录题目分析回溯三部曲剪枝优化题目来源 216. 组合总和 III 题目分析 这个和leetcode77组合类似 本题k相当于树的深度,9(因为整个集合就是9个数)就是树的宽度。 例如 k 2,n 4的话,就是在集合[1,2,3,4,5,6,7,8,9]中…

我的车载开发—{ carservice启动流程 }—

carservice启动流程 大致流程: SystemServer启动CarServiceHelperService服务在调用startService后,CarServiceHelperService的onStart方法通过bindService的方式启动CarService(一个系统级别的APK,位于system/priv-app&#xf…

转转测试环境docker化实践

测试环境对于任何一个软件公司来讲,都是核心基础组件之一。转转的测试环境伴随着转转的发展也从单一的几套环境发展成现在的任意的docker动态环境docker稳定环境环境体系。期间环境系统不断的演进,去适应转转集群扩张、新业务的扩展,走了一些…

Linux系统基本设置:网络设置(三种界面网络地址配置)

网络地址配置:图形界面配置、命令行界面配置、文本图形界面配置 命令行界面配置 查看网络命令: 想要知道你有多少网卡,都可以通过这两个命令来查看 手动设置网络参数,我们可以使用nmcli这个命令来设置,我们需要知道…

【react实战小项目:笔记】用React 16写了个订单页面

视频地址 React 16 实现订单列表及评价功能 简介:React 以其组件化的思想在前端领域大放异彩,但其革命化的前端开发理念对很多 React 初学者来说, 却很难真正理解和应用到真实项目中。本课程面向掌握了 React 基础知识但缺乏实战经验的开发…

状态机分析

写在前面 状态机是指某事物具有有限状态,且在这些状态之间相互转换的抽象,比如门的开是一个状态,关又是一个状态。本文就一起来看下。 1:状态机的术语 1.1:state 状态,即当前所处的状态,如汽…

电子技术——内部电容效应以及MOS与BJT的高频响应模型

电子技术——内部电容效应以及MOS与BJT的高频响应模型 耦合和旁路电容决定了放大器的低频响应,同时内部电容效应决定了放大器的高频响应。本节,我们简单简单介绍一下内部电容效应,并且更重要的是如何在小信号模型中模型化内部电容效应。 MOS…

C语言操作符经典例题

一、选择题 1、下面哪个是位操作符:( ) A.& B.&& C.|| D.! 答案解析: 答案:A A正确,&——按(二进制)位与,对应的二进制位:有0则0&#…

将python代码封装成c版本的dll动态链接库

前言 将python程序打包成DLL文件,然后用C调用生成的DLL文件,这是一种用C调用python的方法,这一块比较容易遇到坑。网上关于这一块的教程不是很多,而且大部分都不能完全解决问题。我在傻傻挣扎了几天之后,终于试出了一个…

第八章认识 Vue.js基础

vue.js 是一套用于构建用户界面的渐进式前端框架 vue.js 核心实现: 相应式的数据绑定:当数据发生改变,视图可以自动更新,不用关心DOM操作,而转型数据库操作 可组合的视图组件:把视图按照功能切分成若干的…

vr电力刀闸事故应急演练实训系统开发

电力事故是在电力生产和输电过程中可能发生的意外事件,它们可能会对人们的生命财产安全造成严重的威胁。因此,电力事故应急演练显得尤为重要。而VR技术则可以为电力事故应急演练提供一种全新的解决方案。 在虚拟环境中,元宇宙VR会模拟各种触电…

07 react+echart+大屏

reactechart大屏大屏ECharts 图表实际步骤React Typescript搭建大屏项目,并实现屏幕适配flexible rem实现适配1. 安装插件对echarts进行的React封装,可以用于React项目中,支持JS、TS如何使用完整例子官网参考大屏 ECharts 图表 ECharts 图…

【Java基础】泛型(二)-泛型的难点:通配符

本文将尝试将通配符和泛型中的继承,多态一并讲解 关于泛型中继承的注意事项 因为Integer、Double继承了Number,根据多态性,以下语句是合法的 Number n new Integer(10); // OK, 父类引用变量可以指向子类对象 n 2.9 // OK,n实…

Mac-Charles

Charles是什么 HTTP代理服务器,HTTP监视器 Charles可以当作一个代理服务器 当浏览器链接这个代理服务器的时候 Charles会监控浏览器发出和接收的所有数据(reques,response,HTTP Headers(cookies和cash)) 反转代理器 Charles主要功能、 1.SSL代理 2.模拟慢速网络…

双目立体视觉:SAD算法

算法原理SAD(Sum of absolute differences)是一种图像匹配算法。基本思想:差的绝对值之和。此算法常用于图像块匹配,将每个像素对应数值之差的绝对值求和,据此评估两个图像块的相似度。该算法快速、但并不精确,通常用于多级处理的…

如何在Power Virtual Agents中实现身份验证

今天我们介绍一下如何通过身份验证的方式来使用Power Virtual Agents。首先进入“Microsoft 365-管理-Azure Active Directory管理中心”。 进入“Azure Active Directory管理中心”后选择“Azure Active Directory”中的“应用注册”-“新注册”。 输入新创建的应用程序名称后…

XXL-JOB分布式任务调度框架(一)-基础入门

文章目录1.什么是任务调度2.常见定时任务方案2.1. 传统定时任务方案示例2.2. 缺点分析3.什么是分布式任务调度?3.1. 并行任务调度3.2. 高可用3.3. 弹性扩容3.4. 任务管理与监测4.市面上常见的分布式任务调度产品5.初识xxl-job6.xxl-job架构设计6.1.设计思想6.2.架构…

程序人生 - 学习和分享

文章目录记于 230217学习安排泛学AI 和 未来记于 230217 刚入行时,经常看到技术博客中,博主们分享生活,比如相亲、上班生活,甚至还有人发结婚照。这个栏目通常被称为:程序人生。 这个现象已经很久没看到了&#xff0c…

BFC 是什么

在页面布局的时候,经常出现以下情况: 这个元素高度怎么没了?这两栏布局怎么没法自适应?这两个元素的间距怎么有点奇怪的样子?...... 原因是元素之间相互的影响,导致了意料之外的情况,这里就涉及…