C语言(函数)—函数栈帧的创建和销毁

news2024/10/20 23:45:21

目录

前言

补充知识

一、函数线帧是什么?

二、函数线帧的实现(举例说明)

两数之和代码

​编辑两数之和 汇编代码分析

 执行第一条语句

执行第二条语句

执行第三条语句

执行第四、五、六条语句

执行第七条语句

执行第八、九、十条语句

执行第十三条语句

执行第十四、十五条语句

执行第十六条语句

执行第十七条语句

 执行第十七、十八条语句

 执行第十九、二十条语句

进入Add函数里面 

Add函数预开辟空间

创建z

 相加

实际传参

返回z

弹出

回收没用空间,回到main函数

将数放进c中

三、总结

局部变量是怎么创建的,局部变量的创建?

 为什么局部变量的值是随机值?

函数是怎么传参的?传参的顺序是什么样的?

形参和实参是什么关系?

函数调用结果是怎么返回的?


前言

这里补充一下函数线帧的创建和销毁,我们知道函数调用一次就会占用一次栈内存。每一次函数调用都会为本次函数调用分配内存空间(是在内存的栈区),为本次函数调用分配的内存空间叫做被称为这次函数调用的栈帧空间,函数栈帧的创建和销毁。

编译器越高级,那么 就越不容易发现在函数调用的过程中线帧的创建,具体细节取决于编译器的实现。新的编译器由于考虑各种各样的问题,所以封装的更加复杂,不容易分离出来函数栈帧创建的过程。

栈空间的使用是从高地址向地地址增长。

寄存器是集成到cpu上的,跟mian函数是没有关系的,是独立的,硬盘,内存,寄存器是相互独立的。


补充知识

我们知道计算机中有寄存器,而寄存器包括很多,例如eax,ebx,ecx,edx,ebp,esp等。

而ebp,esp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。

每一个函数调用,都要在栈区上创建一个空间,而当调用哪个函数,esp和ebp就会维护哪个函数线帧。通常esp称为栈顶指针,sbp称为栈底指针。

当我们在调用main函数之前,会调用_tmainCRTStartup这个函数,这个函数内部调用了main函数,而这个函数又是被mainCRTStartup这个函数调用的。

其实在VS2013中,main函数也是被其他函数调用的。

一、函数线帧是什么?

函数栈帧是指在函数调用过程中,为了保存函数的局部变量、参数和执行上下文等信息而创建的一块存储区。

每次函数调用时,都会在栈上创建一个新的栈帧,用于存储该函数的局部变量、参数和执行上下文等信息。栈帧通常包括以下几个部分:

  1. 局部变量区域:用于存储函数内部定义的局部变量和临时变量等。

  2. 参数区域:用于存储函数的参数值。

  3. 返回地址:用于保存函数调用完成后的返回地址,以便能够返回到调用方继续执行。

  4. 上一级函数的栈帧指针:用于保存上一级函数的栈帧地址,以便能够返回到上一级函数。

  5. 其他上下文信息:如调用方的寄存器值、异常处理信息等。

函数栈帧的创建和销毁是由编译器和操作系统自动完成的,开发者一般无需手动管理。函数栈帧的创建和销毁按照函数调用的顺序,形成一个栈结构,因此也被称为调用栈或执行栈。在函数调用完成后,栈帧被销毁,栈指针回退到上一级函数的栈帧,继续执行上一级函数。这种递归结构的栈帧可以保证函数调用的嵌套和返回顺序的正确性。

二、函数线帧的实现(举例说明)

两数之和代码

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
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;
}

我们通过用vs2022来进行讲解,例子用的上述代码+两数之和函数。

两数之和 汇编代码分析

我们通过F10来调试代码,之后在代码界面点击右键,选择反汇编,就会出现一个新页面,而这个页面就是图中右侧的,这是C语言对应的汇编代码。

把显示符号取消勾选,因为选上会显示符号名,而我们想要看其地址和布局,所以就去掉。

这时候里面就写的是符号的地址了,而不是符号名字了。

因为main函数是其它函数调用的,所以调用的__tmainCRTStartup已经分配好栈空间了,此时esp和ebp就会维护这个函数线帧(前提下面是高地址,上面是高地址):

 执行第一条语句

意思是将ebp进行压栈,所以栈顶就多了一个元素(ebp),因为esp维护的是栈顶,所以esp指向往上移动了一步。内存就压进去一个ebp。

执行第二条语句

mov的意思就是把后者的值赋给前者,这里也就也是将esp的值赋给ebp,我们知道esp的值赋给了ebp,那么ebp的地址就指向esp,而esp又指向栈顶,ebp就指向了之前esp指的地方,所以就变成了下面:

执行第三条语句

sub是前者减去后者,也就是esp减去后面的数字,0E4h是八进制,esp减去这个数后值变小了,所以esp就指向了上面的某一位置,我们知道esp和ebp之间的就是函数的栈帧空间,所以他们俩之间的就是开辟出来main函数的预留空间,也就是main函数的栈帧。

执行第四、五、六条语句

分别压栈压进去三个元素,ebx,esi,edi,同时esp的值会往上走。每一个push,esp都会往上挪一下。

执行第七条语句

lea实际上是 load effecitive address(加载有效地址)的意思,这里面就是将后面的有效地址加载到edi中,相当于edi里面加上了一个地址。它实际上就似乎找了第三条语句的地址。

执行第八、九、十条语句

 这三步真正有意义的是第三步,前两步是把后面的值放到前面的里面。而第三步则是从edi开始,将39h这么多个空间全部改成eax的内容,每一次初始化(dword个字节,一个4个字节)。实际就是将main函数预开辟的空间的全部数据改成0ccccccch。

自此,main函数的栈帧就已经准备好了。接下来才是操作的代码。

执行第十三条语句

这里就是将0Ah(10) 放到ebp-8这个地址的地方,也就是a的地方,这块地方就放了10

执行第十四、十五条语句

与第十三条语句一样差不多,都是赋值给对应地址。

这就可以总结出函数创建的时候局部变量的创建规律:

首先,创建这个函数的函数栈帧

之后找到一些空间把变量放进去

 接下来就是调用函数,因为函数调用需要传参

执行第十六条语句

从这里我们可以看见后面这个地址与前面的有一个地址一样,这里就是b的地址,所以这里把b的值也就是20放进了eax.

执行第十七条语句

这里进行压栈,把eax压栈,放到栈顶。如下图:

 执行第十七、十八条语句

这里与前两句一样,先把a的值存到ecx中,之后在把ecx进行压栈。 

 执行第十九、二十条语句

执行call指令,会调用一个地址,并且把地址压入栈中,而压入栈中的地址就是下一条指令的地址,也就是add的地址00521918(每次编译可能会不同)。

为什么要调用下一条指令的地址,因为call指令会进入add函数,但进入函数后还需要回来,回来怎么回来,就需要一个地址来进行返回,来回到call指令的下一条指令,再从这个地址往下执行命令。

进入Add函数里面 

Add函数预开辟空间

再往下运行,就进入到了Add函数里面,而上面这些就是为了为Add函数预开辟线帧空间。

与之前开辟main函数的线帧空间一样。开辟之后就如上图所示。

创建z

在ebp-8的地址传入z,这里面放的是0

 相加

我们知道ebp -8就是ecx的位置,ebp-12就是eax的位置,同时ecx里面是a的值,eax里面是b的值,所以给他们给他们命名为是a'和b'。

这里面吧ebp+8的值(10)加到eax里面去,所以eax就为10,之后吧ebp+0ch(12)的值(20)再加入eax中,这时候eax里面就为30,加起来之后再把eax的值放进ebp-8的地址里面去,而z恰好是ebp-8的地址,所以z就为30。

 这里我们发现函数参数x,y并没有,而是通过调用指令进行传参,将形参进行push压栈,压到某一位置,参数是从右向左传的,当真的来到函数内部来调用这两个数相加的时候会发现,形参根本不是在函数内部创建的,而是回来找刚才调入进来传参传入的这个空间,压进去这个空间,上图a'就认为是x,b'就认为是y,也就是x+y,之后传入z的空间。

实际传参

实际传参是还没有调用这个函数的时候,参数a和b就已经传过去了,在函数栈帧中压入了两个参数(b和a),压进去之后,真正用函数内部的时候,其实是再找回之前压入栈中的这两个值,然后相加之后再给z。所以形参是实参的临时拷贝这句话得到了验证。

返回z

因为我们知道函数调用完后会销毁,同时z也会销毁,所以把ebp-8里的值也就是z的值30放进eax寄存器中,寄存器不会因为函数的销毁而销毁,所以放在这里就安全了,先保存起来。

弹出

pop就是弹出的意思,弹出一个esp就往下移动一个地址,之后就如下图了:

这时候发现有些空间没有用到,所以应该回收。

回收没用空间,回到main函数

这里将ebp的位置传入esp,用pop(把栈顶的元素 弹出来放到ebp里面去),因为此时ebp(main地址)存的是之前下面的ebp地址,所以ebp又回到了下面(之前的地方),而esp则指向下call指向下一条指令的地址,因为之前的ebp已经弹出了。

这样就回到了main函数里面,这样栈帧空间又是又esp和ebp开始维护。

现在栈顶元素是call指令下一条指令的地址

这条指令就是回到了call指向下一条指令的地址。再往下执行就是:

esp+8之后,esp就指向了如下图:

当指向这里的时候,就说明把ecx和eax就还给了操作系统。所以这时候形参的空间就还给了操作系统,释放了。

将数放进c中

 这里将eax(计算的和)放进ebp-20h(c)里面去,这样返回值就带了回来。

至此,函数线帧的创建和销毁就结束了。

总图:

三、总结

局部变量是怎么创建的,局部变量的创建?

首先先分配好栈帧空间,然后在这个空间里面分配一些空间来创建局部变量

 为什么局部变量的值是随机值?

因为这个随机值是我们自己放进去的,初始化后就把随机值覆盖了,这时候就不是随机值了

函数是怎么传参的?传参的顺序是什么样的?

当我们要调用这个函数的时候,其实函数还没有调用的时候,就已经把两个参数从右向左push压栈压进去了,当真正进入函数的时候,通过指针的偏移量来找到之前传入的这两个形参参数

形参和实参是什么关系?

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

函数调用结果是怎么返回的?

之前就把call指令的下一个指令的地址就压进栈了,把ebp调用这个函数的上一个函数栈帧的ebp就存进去了,当我们函数调用完要返回时候,弹出edp,就可以找到原始(上一个函数调用的ebp),指针往下走就可以找到esp的地址,回到了mian函数。

记住call指令的下一个指令地址就可以回来进行下一个命令。

返回值通过寄存器来临时保存,之后进行返回。

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

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

相关文章

Scroll 生态首个 meme 项目 $Baggor,我们可以有哪些期待?

在最近几个月里&#xff0c;加密市场整体表现平稳&#xff0c;无论是比特币还是山寨币板块&#xff0c;都处于震荡状态&#xff0c;并未显示出突破前高的迹象。然而&#xff0c;在这样的市场背景下&#xff0c;meme币却持续扛起了大旗&#xff0c;令这个看似不太熊的熊市不断引…

递归算法笔记

根据b站视频整理的 **视频地址&#xff1a;**https://www.bilibili.com/video/BV1S24y1p7iH/?spm_id_from333.788.videopod.sections&vd_source6335ddc7b30e1f4510569db5f2506f20 最常见的一个递归例子&#xff1a; 斐波那契数列&#xff1a;1&#xff0c;2&#xff0c;3…

Linux 使用xtrabackup备份MySQL数据

目录 一&#xff1a;xtrabackup 介绍二&#xff1a;实现数据备份1. 实现全备份2. 实现增量备份3. 实现差异备份4. 全备份时压缩数据5. 全备份时排除指定表不备份6. 全备份时排除指定库不备份 三&#xff1a;实现数据还原1. 全备份数据恢复流程2. 全备份压缩后的数据恢复流程3. …

神仙公司名单(长沙)

神仙公司&#xff08;长沙&#xff09; 小周末&#xff0c;继续 神仙公司系列。 长沙&#xff0c;湖南省的省会城市&#xff0c;不仅以其深厚的历史文化底蕴著称&#xff0c;同时也是一个充满活力的现代都市。 长沙的经济活力、教育资源、医疗资源、就业机会、居住环境、生活成…

python中堆的用法

Python 堆&#xff08;Headp&#xff09; Python中堆是一种基于二叉树存储的数据结构。 主要应用场景&#xff1a; 对一个序列数据的操作基于排序的操作场景&#xff0c;例如序列数据基于最大值最小值进行的操作。 堆的数据结构&#xff1a; Python 中堆是一颗平衡二叉树&am…

15分钟学Go 第2天:安装Go环境

第2天&#xff1a;安装Go环境 1. 引言 在学习Go语言之前&#xff0c;首先需要配置好本地开发环境。本节将详细介绍如何在Windows 11上安装和配置Go语言环境&#xff0c;包括安装步骤、环境变量设置、VS Code配置与测试、以及常见问题解决方案。完成这些步骤后&#xff0c;你将…

Excel:vba实现筛选出有批注的单元格

实现的效果&#xff1a;代码&#xff1a; Sub test() Dim cell As RangeRange("F3:I10000").ClearlastRow Cells(Rows.Count, "f").End(xlUp).Row MsgBox lastrow For Each cell In Range("a1:a21")If Not cell.Comment Is Nothing ThenMsgBox…

【AIGC】2024-arXiv-InstantStyle:文本到图像生成中保持风格的免费午餐

2024-arXiv-InstantStyle: Free Lunch towards Style-Preserving in Text-to-Image Generation InstantStyle&#xff1a;文本到图像生成中保持风格的免费午餐摘要1. 引言2. 相关工作2.1 文本到图像的传播模型2.2 风格化图像生成2.3 扩散模型中的注意力控制 3. 方法3.1 动机3.2…

keil中编译遇到错误“error #94-D the size of an array must be greater than zero”解决方法

这一期&#xff0c;我们来看一个在keil中编译时候遇到定义数组元素个数为0时候遇到的一个错误。 错误&#xff1a; 先看错误&#xff1a;编译提示错误“error: #94-D: the size of an array must be greater than zero” &#xff0c;意思是这个数组内元素个数不能定义为0个&…

【AIGC】解锁高效GPTs:ChatGPT-Builder中系统提示词Prompt的设计与应用

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;系统提示词系统提示词的作用与重要性系统提示词在构建GPTs中的作用结论 &#x1f4af;ChatGPT-Builder系统提示词的详细解读OpenAI为Builder编写的系统提示词系统提示词对…

R语言详解predict函数

R语言中predict函数在建立模型&#xff0c;研究关系时常用。但是不同type得到的结果常常被混为一谈&#xff0c;接下来&#xff0c;探讨predict得到的不同结果。 #数据 set.seed(123) n<-1000 age<-rnorm(n,mean50,sd10) gender<-rbinom(n,1,0.5) disease<-rbinom…

OpenCV高级图形用户界面(12)用于更改指定窗口的大小函数resizeWindow()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::resizeWindow() 函数用于更改指定窗口的大小。这使得你可以根据需要调整窗口的宽度和高度。 注释 指定的窗口大小是指图像区域的大小。工具栏…

Maxwell 底层原理 详解

Maxwell 是一个 MySQL 数据库的增量数据捕获&#xff08;CDC, Change Data Capture&#xff09;工具&#xff0c;它通过读取 MySQL 的 binlog&#xff08;Binary Log&#xff09;来捕获数据变化&#xff0c;并将这些变化实时地发送到如 Kafka、Kinesis、RabbitMQ 或其他输出端。…

字节跳动青训营——入营考核解答(持续更新中~~~)

考核内容&#xff1a; 在指定的题库中自主选择不少于 15 道算法题并完成解题&#xff0c;其中题目难度分配如下&#xff1a; 简单题不少于 10 道中等题不少于 4 道困难题不少于 1 道 解答代码 20. 百分位数&#xff08;中等&#xff09; 代码实现&#xff1a; import jav…

Uiautomator2与weditor配置一直报错咋办

作者在配置这两个的时候绞尽脑汁了&#xff0c;u2的init总是报错并且无法自动在手机上安装atx&#xff0c;weditor可以打开但是只要对元素操作或者任意操作就会让你去重新init&#xff0c;搞得作者焦头烂额&#xff0c;而且网上各种各样的报错信息眼花缭乱&#xff0c;作者几乎…

【深入学习Redis丨第八篇】详解Redis数据持久化机制

前言 Redis支持两种数据持久化方式&#xff1a;RDB方式和AOF方式。前者会根据配置的规则定时将内存中的数据持久化到硬盘上&#xff0c;后者则是在每次执行写命令之后将命令记录下来。两种持久化方式可以单独使用&#xff0c;但是通常会将两者结合使用。 一、持久化 1.1、什么…

基于neo4j知识图谱的菜谱推荐系统

&#x1f374; AI菜谱推荐系统让你“煮”事半功倍&#xff01; &#x1f374; 找不到做饭灵感的时候&#xff0c;是不是总觉得“今天吃啥”这道选择题简直是终极挑战&#xff1f;别急&#xff0c;我们基于Neo4j知识图谱的菜谱推荐系统&#xff0c;正是为了解决你的困扰而设计&a…

linux线程 | 同步与互斥 | 全解析信号量、环形生产消费者模型

前言: 本节内容讲述linux下的线程的信号量&#xff0c; 我们在之前进程间通信那里学习过一部分信号量&#xff0c; 但是那个是systemV版本的信号量&#xff0c;是以进程间通信的视角谈的。 但是本篇内容会以线程的视角谈一谈信号量。 ps&#xff1a;本篇内容建议学习了生产者消…

集合collection和泛型

collection可以直接打印内容&#xff0c;而不是地址&#xff0c;内部已经重写了。 List家族&#xff1a; package com.itheima.d6_collection_update_delete;import java.util.ArrayList; import java.util.Iterator; import java.util.List;/**目标&#xff1a;研究集合遍历并…

解决关于HTML+JS + Servlet 实现前后端请求Session不一致的问题

1、前后端不分离情况 在处理session过程中&#xff0c;如果前后端项目在一个容器中&#xff0c;session是可以被获取的。例如如下项目结构&#xff1a; 结构 后端的代码是基本的设置值、获取值、销毁值的内容&#xff1a; 运行结果 由此可见&#xff0c;在前后统一的项目中&a…