【手撕C语言 第八集】函数栈帧的创建与销毁

文章目录

  • 一、什么是函数栈帧?
  • 二、函数栈帧能解决什么问题呢?
    • (1)局部变量是如何创建的?
    • (2)为什么局部变量不初始化内容是随机的?
    • (3)函数调用时参数是如何传递的?传参的顺序是什么样?
    • (4)形参和实参的关系?
    • (5)函数的返回值是如何带回的?
  • 三、函数栈帧的创建与销毁解析
    • 1.什么是栈?
    • 2.认识相关寄存器和汇编指令
    • 3.解析函数栈帧的创建与销毁
      • 1.预备知识
      • 2.函数的调用堆栈
    • 4.准备环境
    • 5.转到反汇编
    • 6.函数栈帧的创建
      • 小知识:烫烫烫~
    • 7.函数栈帧的销毁
    • 8.拓展了解:
  • 四、易混乱点


一、什么是函数栈帧?

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。

那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
🎗️函数参数和函数返回值
🎗️临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

二、函数栈帧能解决什么问题呢?

(1)局部变量是如何创建的?

首先要为局部变量所在的函数创建函数栈帧,再在函数栈帧中找到一些空间把局部变量存进去。

(2)为什么局部变量不初始化内容是随机的?

因为随机值是我们放进去的,例如:0ccccccch,初始化意味着覆盖随机值。

(3)函数调用时参数是如何传递的?传参的顺序是什么样?

在函数调用前就已经push push,把两个实参从右向左拷贝过去。在调用的函数栈帧中通过指针偏移量找到形参。
传参的顺序是从右向左。

(4)形参和实参的关系?

形参确实是在压栈时开辟的空间。它和实参的值相同,空间是独立的,所以形参是实参的一份临时拷贝,改变形参不会影响实参。

(5)函数的返回值是如何带回的?

在调用函数之前,就把call指令的下一条指令的地址存进去,把调用该函数的上一个函数栈帧的ebp存进去。当函数调用完返回的时候,将此时的ebp的值赋给esp,弹出ebp就能找到上一个函数的ebp的位置,(这个过程回收了调用的函数空间,esp,ebp此时维护main函数栈帧),由于之前记住了call指令的下一条指令的地址,当call调用完函数后,通过记住的地址找到下一条指令继续往下执行。在函数调用的过程中将函数的返回值存储到寄存器中,再通过后续的指令操作将寄存器的值放到主函数用于接收返回值的变量中。
所以函数的返回值是通过寄存器的方式带回来的。

三、函数栈帧的创建与销毁解析

1.什么是栈?

🎗️栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
🎗️在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
🎗️在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
🎗️在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的。

2.认识相关寄存器和汇编指令

相关寄存器
在这里插入图片描述
相关汇编命令
在这里插入图片描述
🎗️在这里插入图片描述

3.解析函数栈帧的创建与销毁

1.预备知识

首先我们达成一些预备知识才能有效的帮助我们理解,函数栈帧的创建和销毁。

  1. 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
  2. 这块空间的维护是使用了2个寄存器: esp 和 ebp , ebp 记录的是栈底的地址, esp 记录的是栈顶的地址。
    如图所示:
    在这里插入图片描述

🎗️(1)栈区的使用习惯是先使用高地址,再使用低地址
🎗️(2)空间从高地址向低地址消耗
注意
在不同的编译器下,函数栈帧的创建和销毁是略有差异的,大体逻辑是一致的,具体细节取决于编译器的实现。

2.函数的调用堆栈

演示代码

#include <stdio.h>
int Add(int x, int y)
{int z = 0;z = x + y;return z;
} 
int main()
{int a = 3;int b = 5;int ret = 0;ret = Add(a, b);printf("%d\n", ret);return 0;
}

这段代码,如果我们在VS2019编译器上调试,调试进入Add函数后,我们就可以观察到函数的调用堆栈
(右击勾选【显示外部代码】),如下图:
在这里插入图片描述
🎗️函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main 函数调用之前,是由invoke_main 函数来调用main函数。
🎗️在 invoke_main 函数之前的函数调用我们就暂时不考虑了。
🎗️那我们可以确定, invoke_main 函数应该会有自己的栈帧, main 函数和 Add 函数也会维护自己的栈帧,每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间。

在VS2013上,我们可以清晰的观察到, main 函数调用之前,是由_ _tmainCRTStartup 函数来调用main函数,__tmainCRTStartup函数调用之前,是由mainCRTStartup 函数来调用__tmainCRTStartup函数的。
在这里插入图片描述
那接下来我们从main函数的栈帧创建开始讲解

4.准备环境

为了让我们研究函数栈帧的过程足够清晰,不要太多干扰,我们可以关闭下面的选项,让汇编代码中排除一些编译器附加的代码:
在这里插入图片描述

5.转到反汇编

调试到main函数开始执行的第一行,右击鼠标转到反汇编。
注意:VS编译器每次调试都会为程序重新分配内存,本次反汇编代码是一次调试代码过程中数据,每次调试略有差异。
在这里插入图片描述

6.函数栈帧的创建

接下来我们就一行行拆解汇编代码:
在这里插入图片描述
在这里插入图片描述
上面的这段代码最后4句,等价于下面的伪代码
在这里插入图片描述
在这里插入图片描述

小知识:烫烫烫~

在这里插入图片描述
之所以上面的程序输出“烫”这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间的其中每一个字节都被初始化为0xCC,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。

接下来我们再分析main函数中的核心代码
在这里插入图片描述
在这里插入图片描述
Add函数的传参
在这里插入图片描述
在这里插入图片描述
函数调用过程
在这里插入图片描述
call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。
这里记住call指令的下一条指令的地址,当call调用完函数之后,通过记住的地址找到下一条指令继续执行
在这里插入图片描述
当我们跳转到Add函数,就要开始观察Add函数的反汇编代码了。
在这里插入图片描述
代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。
在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。

  1. 将main函数的 ebp 压栈
  2. 计算新的 ebp 和 esp
  3. 将 ebx , esi , edi 寄存器的值保存
  4. 计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的
    参数,这就是形参访问。
  5. 将求出的和放在 eax 寄存器尊准备带回

在这里插入图片描述
图片中的 a’ 和 b’ 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程,以及函数在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。

7.函数栈帧的销毁

当函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。
那具体是怎么销毁的呢?我们看一下反汇编代码。
在这里插入图片描述
回到了call指令的下一条指令的地方:
在这里插入图片描述
但调用完Add函数,回到main函数的时候,继续往下执行,可以看到:
在这里插入图片描述

8.拓展了解:

其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。

四、易混乱点

(1)函数内部创建的静态变量是在全局开辟的;函数内部创建的局部变量是在栈区上创建的。
(2)每个函数开辟的空间不是一样大的,这取决于编译器(编译器会计算需要多大空间)
(3)寄存器独立于内存,不在内存上,是集成到CPU上的

电脑上的存储(相互独立):
硬盘
内存
寄存器

(4)为什么局部变量a和b创建的位置不是相邻的,中间隔了一块空间?
这完全取决于编译器,中间空多大空间也取决于编译器。
(5)计算机删除数据是直接允许后续操作直接覆盖的。
(6)事实上,在函数开辟空间上进行的是z=x+y操作,x,y形参空间的开辟是在main函数和add函数之间的一块空间,也可以认为是在main函数的栈帧空间里,但是不在ad函数的栈帧空间里。

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

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

相关文章

电磁兼容设计要点

电磁兼容的问题往往发生于高频状态下&#xff0c;个别情况除外&#xff08;Dips电压暂降与中断&#xff09;除外。高频思维&#xff0c;总而言之&#xff0c;就是器件的特性、电路的特性&#xff0c;在高频情况下和常规中低频状态下是不一样的&#xff0c;如果仍然按照普通的控…

Go 定时器:如何避免潜在的内存泄漏陷阱

这篇文章将探讨的是 Go 中如何高效使用 timer&#xff0c;特别是与select 一起使用时&#xff0c;如何防止潜在的内存泄漏问题。 引出问题 先看一个例子&#xff0c;我们在 Go 中的 select 使用定时器&#xff0c;实现为消息监听加上超时能力。 核心代码&#xff0c;如下所示…

已解决Error:AttributeError: module ‘numpy‘ has no attribute ‘bool‘.

文章目录 引言报错分析解决方案1&#xff1a;降低NumPy版本解决方案2&#xff1a;更改NumPy源码 结尾 引言 在Python编程的世界里&#xff0c;NumPy无疑是一个不可或缺的库。它不仅在处理大规模数值计算中发挥着核心作用&#xff0c;而且为众多开发者提供了强大的支持。然而&a…

无刷电机学习-方波电调 程序篇1(AM32)

一、AM32简介 AM32 固件专为 ARM 处理器设计&#xff0c;用于控制无刷电机 (BLDC)。该固件旨在安全、快速、平滑、快速启动和线性油门。它适用于多种车辆类型和飞行控制器。 AM32具有以下特点&#xff1a; 可通过 betaflight 直通、单线串行或 arduino 升级固件伺服 PWM、Dsh…

Spring RabbitMQ那些事(3-消息可靠传输和订阅)

目录 一、序言二、生产者确保消息发送成功1、为什么需要Publisher Confirms2、哪些消息会被确认处理成功 三、消费者保证消息被处理四、Spring RabbitMQ支持代码示例1、 application.yml2、RabbigtMQ配置3、可靠生产者配置4、可靠消费者配置5、测试用例 一、序言 在有些业务场…

外包干了3年,跑路了。。。

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 故事的开始 最近这几天有点忧伤&#xff0c;因为带我的师傅要跑…

Vue3 Suspense 优雅地处理异步组件加载

✨ 专栏介绍 在当今Web开发领域中&#xff0c;构建交互性强、可复用且易于维护的用户界面是至关重要的。而Vue.js作为一款现代化且流行的JavaScript框架&#xff0c;正是为了满足这些需求而诞生。它采用了MVVM架构模式&#xff0c;并通过数据驱动和组件化的方式&#xff0c;使…

力扣 | 73. 矩阵置零

public class Problem_73_MatrixToZero {public void setZeros(int[][] matrix) {int m matrix.length;int n matrix[0].length;boolean flagRow false;boolean flagCol false;//先记录第一行本身是否包含0for (int i 0; i < n; i) {if (matrix[0][i] 0) flagRow tru…

使用Burp Collaborator验证无回显的RCE漏洞

使用Burp Collaborator验证无回显的RCE漏洞 1.概述2.Collaborator演示3.通过DNS查找外带命令执行结果1.概述 当应用程序容易受到命令注入攻击,但命令是异步执行时,就会发生异步操作系统命令注入漏洞。这意味着它对应用程序的响应没有明显影响 Burp Collaborator 可以帮助您…

Jenkins如何重置build number?

Jenkins如何重置build number&#xff1f; Jenkins调试过程中, 难免会产生很多无用的编译号&#xff0c;但需要清除无用build数据的时候&#xff0c;可以使用Script Consle来达到目的. 解决方案 直接说答案。 在脚本中填写如下语句&#xff1a; def jobName "xxx-Build&…

leetcode—— 腐烂的橘子

腐烂的橘子 在给定的 m x n 网格 grid 中&#xff0c;每个单元格可以有以下三个值之一&#xff1a; 值 0 代表空单元格&#xff1b;值 1 代表新鲜橘子&#xff1b;值 2 代表腐烂的橘子。 每分钟&#xff0c;腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。 返回 直到…

【Linux C | 进程】Linux 进程间通信的10种方式(1)

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

log4j2配置文件命名及优先级

log4j 2.x版本不再支持像1.x中的.properties后缀的文件配置方式&#xff0c;2.x版本配置文件后缀名只能为".xml",“.json"或者”.jsn"。 命名规则 默认配置文件名&#xff1a; log4j2.xml 或 log4j2.json 测试或特定环境配置文件名&#xff1a;可以以 -t…

Kafka(二)原理详解

一 、kafka核心总控制器&#xff08;Controller&#xff09; 在Kafka集群中会有一个或者多个broker&#xff0c;其中有一个broker会被选举为控制器&#xff08;Kafka Controller&#xff09;&#xff0c;它负责管理整个集群中所有分区和副本的状态。 作用&#xff1a;leader副…

【wayn商城】本地开发指南

这篇文章给大家带来我自己写的开源项目【wayn商城】的本地开发指南&#xff0c;帮助各位朋友在本地快速运行【wayn商城】&#xff0c;避免踩坑&#xff0c;减少不必要的精力在软件下载安装上。 &#x1f525;waynboot-mall 是一套全部开源的 H5 商城项目&#xff0c;实现了一套…

Jedis(一)与Redis的关系

一、Jedis介绍&#xff1a; 1、背景&#xff1a; Jedis是基于Java语言的Redis的客户端&#xff0c;Jedis Java Redis。Redis不仅可以使用命令来操作&#xff0c;现在基本上主流的语言都有API支持&#xff0c;比如Java、C#、C、PHP、Node.js、Go等。在官方网站里有一些Java的…

【系统DFX】如何诊断占用过多 CPU、内存、IO 等的神秘进程?

热门面试问题&#xff1a;如何诊断占用过多 CPU、内存、IO 等的神秘进程&#xff1f; 下图展示了 Linux 系统中有用的工具。 &#x1f539;’vmstat’ - 报告有关进程、内存、分页、块 IO、陷阱和 CPU 活动的信息。&#x1f539;’iostat’ - 报告系统的 CPU 和输入/输出统计信…

【制作100个unity游戏之23】实现类似七日杀、森林一样的生存游戏2(附项目源码)

本节最终效果演示 文章目录 本节最终效果演示系列目录前言添加小动物模型动画动物AI脚本效果 添加石头石头模型拾取物品效果 源码完结 系列目录 【制作100个unity游戏之23】实现类似七日杀、森林一样的生存游戏1&#xff08;附项目源码&#xff09; 【制作100个unity游戏之23】…

ubuntu22.04安装filebeat报错解决

1、查看报错 journalctl -u filebeat 或者 filebeat -c /etc/filebeat/filebeat.yml找到报错信息 runtime/cgo: pthread_create failed: Operation not permitted 2、解决报错 在filebeat.yml配置文件添加如下配置&#xff0c;重启filebeat seccomp:default_action: allow…

Prometheus 薪资翻倍的监控系统?

1. 介绍与架构 Prometheus是一个开源的系统监控和警报工具包&#xff0c;用于收集和存储时间序列数据&#xff0c;包括指标信息、记录时间戳以及可选的键值对标签。许多公司使用Prometheus监控K8s集群。 2. 合适与不合适场景 合适场景 Prometheus适用于记录各种数字时间序列…