函数的栈帧

news2024/11/20 6:31:27

       我们每次在调用函数的时候,都说会进行传参。每次创建函数,或者进行递归的时候,也会说会进行压栈。

       那么,今天我们就来具体看看函数到底是如何进行压栈,传参的操作。

什么是栈?

       首先我们要知道,我们将内存一般划分为三个区域:

  • 静态区
  • 堆区
  • 栈区

       我们平时创建的临时变量,函数都会在栈区中占据空间:

       此时我们也要知道栈区的使用规则:从高地址向低地址使用

栈的使用规则:

       我们知道抢的弹夹,我们要逐个把子弹往里面压,之后如果取出子弹,就需要将上一次压入的子弹取出,之后逐个取出子弹,并只能按照顺序取出。

       栈就是这样的使用规则,遵循先进后出,后进先出。        此时你会想,不能把任意的数据取出,必须一个一个拿,这种结构真的好用吗?

        起初我也这样认为,但是计算机就喜欢用这种结构。

        在内存中,栈区的使用规则是从高地址向低地址使用的。

函数的栈帧:

       C语言中,我们要想观察函数栈帧就需要用到调试。当我们调试时所在的函数(此时函数未运行完),每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。

       因为我们知道,只要运行函数就会进行压栈操作,所以分析出一下信息:

  1. 栈帧是一块因函数运行而创建的的临时空间。
  2. 每调用一次函数都会创建一个独立的函数栈帧。
  3. 栈帧中存放着函数重要信息,如局部变量,函数返回地址,函数参数等。
  4. 当函数运行完毕后栈帧会销毁。

       既然会创建函数栈帧,那么就会维护其空间,计算机使用寄存器维护空间。

什么是寄存器?

       这里牵扯很多内容,我们只给出笼统解释:寄存器是集成到CPU上的,是独立的,寄存器可以暂存指令,地址和数据。所以寄存器也可以理解为指针。

       我们会使用很多寄存器,要理解清楚函数栈帧,就必须理解ebp和esp,这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。我们详细来讲esp和ebp两个寄存器。其余寄存器混个脸熟即可,一会会用到。

esp寄存器:

       维护栈顶,始终指向栈顶(此时esp寄存器存储栈顶地址)。

ebp寄存器:

       维护当前函数栈帧的栈低(此时ebp寄存器存储函数栈帧栈低地址)。

几个必要的汇编指令:

       我们观察函数栈帧的创建和销毁,就要知道几个汇编指令,这样可以更好的阅读以下内容。

       记不住没关系,我们一下会一一讲解。 

图解: 

        这里我们使用VS2013来观察,由于VS2022太过高级,有些内部细节就会看不到,所以用VS2013来观察。此时我们执行以下代码:

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;
}

       esp和ebp就是维护当前调用的函数。 

       点击F10开始调试。       接下来我们看main函数被谁调用了,我们进入main函数,并直接执行完。

       在VS2013中,main函数也是被其他函数调用的。没想到main函数也是被调用的函数,也理解了为什么每次都要有返回值。 

       mainCRTStartup函数调用 __tmainCRTStartup函数,__tmainCRTStartup函数调用main函数。

       之后我们按住F10,之后右击鼠标找到“转到反编汇”,就可以找到C语言所对应的汇编代码,箭头的指向就可以一行一行的执行,我们来逐过程分析:

       

        push ebp,将ebp压入栈区。因为esp维护栈顶,所以esp指向改变。我们可以观察其存放地址的改变。

       之后执行move,move是把后面的值赋到前面去。

       此时ebp和esp指向的位置相同。

        之后,就要创建函数的栈帧了,执行sub,就是将其寄存器存放地址减去一个地址,因为栈区是高地址到底地址,所以该地址向上。

 

       此时esp维护main函数的栈顶,ebp维护main函数的栈低。我们可以看内存:

       这些内存都是为main函数开辟的空间。

       之后有执行了3次push,push时会有一个动作,就是栈顶指针esp会变,一直指向栈顶。

       我们通过内存窗口来观察:        因为我们说过,寄存器既可以存地址,也可以存数据,此时ebx存的是数据(就是内存里面的内容),而esp存放的是ebx的地址。压入ebx以后,继续将esi、edi压入。

       之后执行lea : load effective address 加载有效地址。        此时会发现,正好是加载的空间正好是最开始esp减去的空间,就是main函数栈帧的低地址。

       之后执行的命令,我们就需要先讲解一下了。

       我们应该听过字节的概念,1字节等于8比特位,那么字和字节又什么关系?一个字等于两个字节。

       比特记为bit,字节记为Byte,字记为word,所以有如下关系:

  • 1Byte=8bits
  • 1word=2Bytes=16bits 
  • dword:一个word是两个字节,d代表double,就是双字,就是4个字节。

       此时我们要看其以下的三个步骤:

        此时edi里面存放main函数栈帧的低地址。

        这样就可以理解为什么每次打印未初始化的空间,打印出来的字符都是一个汉字“烫烫烫烫”了。

       此时才会开始执行有效的代码。在此之前都是为main函数开辟的空间。

       此时就要调用Add函数了,一样的,我们要改变ebp和esp的指向,因为进入Add函数就需要维护Add函数栈帧了,但是还是要做以下准备,就是传参,我们来看形式参数的创建。

       这两个动作相当于传参,之后执行call,就是调用函数,要记住call的地址,此时点击F11才能进入Add函数。

       我们可以发现就在ecx的下一个地址里面存储了call指令下一个执行的地址。为什么要记录地址?我们先埋个伏笔,此时我们会先进入Add函数,流程如下:

       注意此时main函数的函数栈帧已经增长到call指令的下一个地址了。        此时我们来观察Add函数的细节:将esp减去一个地址改变指向:

       之后还是main函数栈帧的那一套操作,压入3个寄存器并初始化空间,并将z初始化为0: 

       此时先将 ebp + 8 的值赋给 eax ,此时 eax = 10;之后又执行add,将 ebp + 12 的值等于30,最后将eax的值赋给 ebp - 8 ,此时 ebp - 8 地址的值是30.        我们可以发现,我们使用Add函数并没有创建形参,在我们传参时其实已经压栈过了,而且参数是从右向左传参的。

       返回的话z会被销毁,我们来观察其如何返回。

       我们将结果放入eax寄存器当中,此时就不用担心函数销毁。

       此时将上面的3个寄存器弹出栈顶。

       之后mov esp的位置,esp的指向改变:        此时弹出ebp,ebp弹出以后会指向main函数的栈低,因为之前记录着mian函数的栈低。

 

        当前栈顶元素为call指令的下一个指令的地址,ret这条指令就是找到之前call指令记录的地址,并pop一次栈顶元素。

        此时执行add   esp,8 因为没有dword 所以是改变指向。

       此时将形参x,y的空间还给操作系统。此时又执行mov,将eax存放的值赋给 ebp - 20h 就是给c赋值。 

       此时main函数执行完,也是以上步骤,我们不再赘述。

总结: 

       我们通过观察函数栈帧的创建和销毁,最后返回值是由寄存器带回来的;也可以理解为什么局部变量的值是随机的,形参和实参的关系,确实是一份临时拷贝。希望大家下去多加练习,逐渐就会顿悟其中的原理。

       爆肝一整天,点点赞吧,呜呜~

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

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

相关文章

Python (五) 处理图像

程序员的公众号:源1024,获取更多资料,无加密无套路! 最近整理了一波电子书籍资料,包含《Effective Java中文版 第2版》《深入JAVA虚拟机》,《重构改善既有代码设计》,《MySQL高性能-第3版》&…

深度学习(生成式模型)——ADM:Diffusion Models Beat GANs on Image Synthesis

文章目录 前言基础模型结构UNet结构Timestep Embedding关于为什么需要timestep embedding global attention layer 如何提升diffusion model生成图像的质量Classifier guidance实验结果 前言 在前几篇博文中,我们已经介绍了DDPM、DDIM、Classifier guidance等相关的…

【Scala】Scala中的一些基本数据类型的特性 列表、元组、构造器、单例对象、伴生类、伴生对象、抽象类与特质

列表 使用List(“”,“”,“”)去声明 sliding 和 groued表示迭代器 val iter List("Hadoop", "Spark", "Scala") sliding 2// sliding 和 groued 是有区别的while (iter.hasNext){println(iter.next())}for (elem <- iter){println(elem)}…

11 月 NFT 动态:交易量增长,Layer 2 格局剧变

作者&#xff1a;stellafootprint.network 11 月份&#xff0c;随着比特币和以太坊价格的提升&#xff0c;加密货币市场活动频繁&#xff0c;市场呈现进一步复苏的迹象。NFT 领域中&#xff0c;Blur 的交易量飙升&#xff0c;进一步巩固地位&#xff1b;Blast 的亮相&#xff…

数字图像处理(实践篇)二十二 使用opencv进行人脸、眼睛、嘴的检测

目录 1 xml文件 2 涉及的函数 3 实践 使用opencv进行人脸、眼睛、嘴的检测。 1 xml文件 方法① 下载 地址&#xff1a;https://github.com/opencv/opencv/tree/master/data/haarcascades 点击haarcascade_frontalface_default.xml文件 对着Raw右键&#xff0c;选择“链接…

centos7 安装nnDetection环境

nnunet和nnDetection更新导致默认安装可能会出现无法调用GPU的问题&#xff0c;这里稍微细致的记录下安装nnDetection环境过程。 1.创建虚拟环境&#xff1a; Please note that nndetection requires Python 3.8. Please use PyTorch 1.X version for now and not 2.0 这里要…

Linux——Web网站服务(二)

一、httpd服务的访问控制 1、客户机地址限制 通过Require配置项&#xff0c;可以根据主机的主机名或P地址来决定是否允许客户端访问。在 httpd服务器的主配置文件的<Location>&#xff0c;<Directory>、<Files>、<Limit>配置段中均可以使用Require配置…

【SpringBoot教程】SpringBoot 实现前后端分离的跨域访问(CORS)

作者简介&#xff1a;大家好&#xff0c;我是撸代码的羊驼&#xff0c;前阿里巴巴架构师&#xff0c;现某互联网公司CTO 联系v&#xff1a;sulny_ann&#xff08;17362204968&#xff09;&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗…

分布式锁实现方案 - Lock4j 使用

一、Lock4j 分布式锁工具 你是不是在使用分布式锁的时候&#xff0c;还在自己用 AOP 封装框架&#xff1f;那么 Lock4j 你可以考虑一下。 Lock4j 是一个分布式锁组件&#xff0c;其提供了多种不同的支持以满足不同性能和环境的需求。 立志打造一个简单但富有内涵的分布式锁组…

云贝教育 | 分享课:12月12日周二晚Oracle分享课享来了

Oracle 19c OCM分享课分享主题: Introduction to Clusterware 讲师&#xff1a;郭一军 直播分享平台&#xff1a;云贝教育视频号 时间&#xff1a;12月12日 周二晚 19: 30

物联网平台:网络调试助手+HTTP+上传数据到onenet

目录 onenet设备初始化 获取设备id和密钥key 尝试上传数据&#xff0c;实例 onenet设备初始化 获取设备id和密钥key 进入官网 中移坤灵 - 中国移动物联网开放平台 (10086.cn)https://open.iot.10086.cn/ 创建登录账号后&#xff0c;点击左上角的开发者中心 点击左上角的全…

ARP协议:地址解析协议

目录 引言 什么是ARP协议&#xff1f; ARP协议的工作原理 1. ARP请求 2. ARP应答 3. ARP缓存 ARP协议的应用 结语 其他链接 引言 在计算机网络中&#xff0c;地址解析协议&#xff08;ARP&#xff0c;Address Resolution Protocol&#xff09;扮演着重要的角色。ARP协议…

I2C 应用编程

1. I2C 框架结构 1.1 I2C 硬件框架 I2C 总线拓扑图 ⚫ 在一个芯片 (SoC) 内部&#xff0c;有一个或多个 I2C 控制器 ⚫ 在一个 I2C 控制器上&#xff0c;可以连接一个或多个 I2C 设备 ⚫ I2C 总线只需要 2 条线&#xff1a;时钟线 SCL 、数据线 SDA ⚫ 在 …

tamcat乱码

学习springmvc时tamcat乱码 ①、启动时tomcat控制台乱码 解决方法是&#xff1a;1、先把idea设置里的默认字节码改成utf-8 ​ 2、把idea显示编码改成utf-8&#xff0c;在末尾加上&#xff08; -Dfile.encodingUTF-8&#xff09; ​ 3、最后重启idea 加上这个 -Dfile.encodingU…

外贸获客怎么做?有哪些技巧?

外贸获客是许多企业拓展海外市场的关键一环&#xff0c;为了成功地吸引潜在客户&#xff0c;我们需要了解一些基本的获客技巧&#xff0c;本文将分享一些实用的方法和技巧&#xff0c;帮助您在外贸领域获得更多的客户。 一、了解目标客户 在开展外贸业务之前&#xff0c;了解…

保研毕业论文查重率多少通过【保姆教程】

大家好&#xff0c;今天来聊聊保研毕业论文查重率多少通过&#xff0c;希望能给大家提供一点参考。 以下是针对论文重复率高的情况&#xff0c;提供一些修改建议和技巧&#xff1a; 保研毕业论文查重率多少通过 在保研过程中&#xff0c;毕业论文的查重率是衡量学术诚信和论文…

【Maven教程】(十二):版本管理 ——版本号定义约定及相关概念,自动化版本发布与创建分支,GPG签名 ~

Maven 版本管理 1️⃣ 版本管理的概念2️⃣ Maven 的版本号定义约定3️⃣ 主干、标签与分支4️⃣ 自动化版本发布5️⃣ 自动化创建分支6️⃣ GPG签名6.1 GPG 及其基本使用6.2 Maven GPG Plugin &#x1f33e; 总结 一个健康的项目通常有一个长期、合理的版本演变过程。例如JUn…

Vue H5项目,怎么引入uni.webview sdk,调用uni postMessage实现手机蓝牙连接打印功能(uniapp)

前言 目前公司Vue H5项目&#xff0c;用webview打包成APP&#xff0c;现产品提出这样打包出来的app运行较慢&#xff0c;需要用uniapp方式&#xff08;即使用HBuilder编辑器来打包H5&#xff09;来打包&#xff0c;那需要的基座就不是安卓的基座而是uniapp的基座&#xff0c;而…

如何通过内网穿透工具实现任意浏览器远程访问Linux本地zabbix web管理界面

前言 Zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案。能监视各种网络参数&#xff0c;保证服务器系统的安全运营&#xff1b;并提供灵活的通知机制以让系统管理员快速定位/解决存在的各种问题。 本地zabbix web管理界面限制在只能局域…

GD32F30X-RT-Thread学习-线程管理

1. 软硬件平台 GD32F307E-START Board开发板MDK-ARM Keil 2.RT-Thread Nano 3.RT-Thread 内核学习-线程管理 ​ 在多线程操作系统中&#xff0c;可以把一个复杂的应用分解成多个小的、可调度的、序列化的程序单元&#xff0c;当合理地划分任务并正确地执行时&#xff0c;这…