函数的栈帧

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

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

什么是栈?

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

  • 静态区
  • 堆区
  • 栈区

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

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

栈的使用规则:

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

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

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

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

函数的栈帧:

       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.mzph.cn/news/214110.shtml

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

相关文章

Error opening file for writing报错解决

报错展示及描述 在安装pycharm的时候出现了一下报错, Error opening file for writing。 报错原因 一般出现这种报错都是文件权限的原因,检查一下,果然这个文件夹权限是【只读】 查看文件权限的方式:【右击】文件夹名称&#xff0…

CSS结构伪类选择器之否定伪类:not()

结构伪类选择器是针对 HTML 层级结构的伪类选择器。 常用的结构化伪类选择器有: :root选择器、:not选择器、:only-child选择器、:first-child选择器、:last-child选择器、 :nth-child选择器、:nth-child(n)选择器、:nth-last-child(n)选择器、:nth-of-type(n)选择…

046:vue通过axios调用json地址数据的方法

第046个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下,本专栏提供行之有效的源代码示例和信息点介绍,做到灵活运用。 (1)提供vue2的一些基本操作:安装、引用,模板使…

Userwindows pc电脑生成一个电脑唯一机器码

在Windows PC上生成一个唯一标识码通常涉及到计算机硬件和软件的信息。有一些常见的方式可以获取到一个较为唯一的标识码: 1. 硬件信息: 可以通过获取计算机的硬件信息来生成一个唯一标识码。这可能包括CPU序列号、硬盘序列号、网卡MAC地址等。但请注意…

ffmpeg过滤器filter理论与实战

文章目录 前言一、DirectShow1、简介2、程序基本结构3、架构 二、过滤器1、视频过滤器 -vf2、音频过滤器 -af3、过滤器链(Filterchain)4、过滤器图(Filtergraph)①、基本语法②、Filtergraph 的分类 5、结构体间的关系图 三、过滤…

Vue项目中WebSocket封装

WEBSOCKET 封装引入初始化使用 封装 utils下建立WebSocketManager.js class WebSocketManager {constructor() {this.url null;this.websocket null;this.isConnected false;this.listeners {onopen: [],onmessage: [],onclose: [],onerror: [],};this.reconnectionOptio…

QML如何与C++层进行信号槽通讯

//QML端为槽函数 //其中serial为C类的对象 //CSerial serial(暂且可以这么理解) QML: Connections{ target: serial onStringReceived:{ console.log("receive:"receiveString) } } //C端为信号 //C //C类…

kafka 常用命令【学习笔记】

Kafka 环境变量配置 export KAFKA_HOME/opt/cloudera/parcels/CDH-6.3.2-1.cdh6.3.2.p0.1605554/lib/kafka export PATH P A T H : PATH: PATH:KAFKA_HOME/bin 查看主题 ./kafka-topics.sh --list --zookeeper localhost:2181 创建主题 ./kafka-topics.sh --create --zook…

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

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

JAVA8新特性之函数式编程详解

JAVA8新特性之函数式编程详解 前言一、初步了解函数式接口二、 Lambda表达式2.1 概述2.2 lambda省略规则2.3 lambda省略常见实例2.4 lambda表达式与函数式接口 三、 Stream流3.1 stream流的定义3.2 Stream流的特点3.3 Stream流的三个步骤3.4 Stream 和 Collection 集合的区别&a…

【HarmonyOS开发】拖拽动画的实现

动画的原理是在一个时间段内,多次改变UI外观,由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画。UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(F…

【带头学C++】----- 九、类和对象 ---- 9.12 C++之友元函数(9.12.1---12.4)

❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️创做不易,麻烦点个关注❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️ ❤️❤️❤️❤️❤️❤️❤️❤️❤️文末有惊喜!献舞一支!❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️ 目录 9.12…

TypeError: Cannot set properties of undefined (setting ‘xx‘)

在写代码中经常会遇到TypeError: Cannot set properties of undefined (setting ‘xx‘),这个问题。 一般出现的场景:在调用接口访问后端数据时,前端渲染显示空白,并报此错。例如,我在调用高德地图,输入经…

五:爬虫-数据解析之xpath解析

三:数据解析之xpath解析 1.xpath介绍: ​ xpath是XML路径语言,它可以用来确定xml文档中的元素位置,通过元素路径来完成对元素的查找,HTML就是XML的一种实现方式,所以xpath是一种非常强大的定位方式​ XPa…

vue2 element-ui select下拉框 选择传递多个参数

<el-select v-model"select" slot"prepend" placeholder"请选择" change"searchPostFn($event,123)"> <el-option :label"item.ziDianShuJu" :value"{value:item.id, label:item.ziDianShuJu}" v-for&qu…

Ubuntu系统使用快速入门实践(七)——软件安装与使用(5)

Ubuntu系统使用快速入门实践系列文章 下面是Ubuntu系统使用系列文章的总链接&#xff0c;本人发表这个系列的文章链接均收录于此 Ubuntu系统使用快速入门实践系列文章总链接 下面是专栏地址&#xff1a; Ubuntu系统使用快速入门实践系列文章专栏 文章目录 Ubuntu系统使用快速…

crypto-js加密、解密与node Crypto加解密模块的应用

前端用crypto-js实现加解密&#xff0c;node端用Crypto模块&#xff0c;两者想要相同结果的话&#xff0c;就要保持加密密钥和加密算法一致。 crypto-js加密、解密 参考&#xff1a; 『crypto-js 加密和解密』 前端使用CryptoJS加密解密 // DES算法 import CryptoJS from cryp…

【unity】【WebRTC】从0开始创建一个Unity远程媒体流app-构建可同步场景

【背景】 最近在研究远程画面&#xff0c;所以就实践了一下。技术采用我认为比较合适的WebRTC。 这篇文章的基础是我的另一篇博文&#xff0c;如果希望顺利完成本篇操作&#xff0c;请先关注我后查询我的如下博文&#xff1a; 【WebRTC】【Unity】Unity Web RTC1-Unity中简单实…

Docker架构及常用的命令

一、初识Docker 1、 docker是一个快速交付应用、运行应用的技术&#xff0c;具备下列优势&#xff1a; 可以将程序及其依赖、运行环境一起打包为一个镜像&#xff0c;可以迁移到任意Linux操作系统运行时利用沙箱机制形成隔离容器&#xff0c;各个应用互不干扰启动、移除都可以…

边缘智能网关如何应对环境污染难题

随着我国工业化、城镇化的深入推进&#xff0c;包括大气污染在内的环境污染防治压力继续加大。为应对环境污染防治难题&#xff0c;佰马综合边缘计算、物联网、智能感知等技术&#xff0c;基于边缘智能网关打造环境污染实时监测、预警及智能干预方案&#xff0c;可应用于大气保…