函数栈帧的创建和销毁(VS2022)

news2024/11/16 12:07:46

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

前言

一、前面的困惑

二、什么是函数栈帧

三、关于函数栈帧的基础知识

1.栈

2.寄存器

2.1 什么是寄存器

2.2 相关的寄存器

 2.3 相关汇编命令

 2.4 预备知识

四、解析函数栈帧的创建和销毁

1.转到反汇编

 2. 函数栈帧的创建

3.进入自定义函数: 

 4. 函数栈帧的销毁

总结


前言

前期在学习的时候,我们可能有很多问题,比如局部变量如何创建的等......,学完这章的知识点,就都会了


一、前面的困惑

局部变量是如何创建的?
为什么局部变量不初始化内容是随机的?
函数调用时参数时如何传递的?传参的顺序是怎样的?
函数的形参和实参分别是怎样实例化的?
函数的返回值是如何带会的?

让我们一起走进函数栈帧的创建和销毁的过程中。

二、什么是函数栈帧

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。
那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
函数参数和函数返回值临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

三、关于函数栈帧的基础知识

1.栈

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。

在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。

在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的 ;

2.寄存器

2.1 什么是寄存器

寄存器(Register)是计算机硬件中的小型、高速的存储单元,它们直接连接到CPU(中央处理器)。寄存器主要用于暂存数据或指令地址,以便CPU能快速访问,提高计算效率。相比于主内存,寄存器的读写速度更快,但容量有限。常见的寄存器有通用寄存器(如ALU的运算结果寄存器)、程序计数器(PC,保存当前指令的位置)、标志寄存器(用于存放运算结果的状态信息)等。寄存器的作用至关重要,尤其是在处理频繁使用的数据或指令时,能够显著提升系统的性能。

2.2 相关的寄存器

以下就是等下讲解的时候会出现的寄存器:

eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器

eip:指令寄存器,保存当前指令的下一条指令的地址

 2.3 相关汇编命令

mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令

 2.4 预备知识

我们浅浅的举个例子:

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

我们先简单来说一下: 

 其实我们不难发现:

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

即:

注:函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2022为例。 

首先我们先来看看下面的调试内容(还是以上面的例子为例):

所以在VS2022中,main函数也是被其他函数调用的

函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main 函数调用之前,是由invoke_main 函数来调用main函数。
在 invoke_main 函数之前的函数调用我们就暂时不考虑了。

 

这里相当于打个预防针,方便理解后面的栈帧的创建与销毁;

那接下来我们从main函数的栈帧创建开始讲解:

四、解析函数栈帧的创建和销毁

1.转到反汇编

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

1.调试右击转到反汇编并关闭显示符号名:

接下来我们来解读这上面的代码:

int main()
{
00007FF60EC718A0  push        rbp  
00007FF60EC718A2  push        rdi  
00007FF60EC718A3  sub         rsp,148h  
00007FF60EC718AA  lea         rbp,[rsp+20h]  
	int a = 10;
00007FF60EC718AF  mov         dword ptr [rbp+4],0Ah  
	int b = 20;
00007FF60EC718B6  mov         dword ptr [rbp+24h],14h  
	int c = 0;
00007FF60EC718BD  mov         dword ptr [rbp+44h],0  

	c = Add(a, b);
00007FF60EC718C4  mov         edx,dword ptr [rbp+24h]  
00007FF60EC718C7  mov         ecx,dword ptr [rbp+4]  
00007FF60EC718CA  call        00007FF60EC71348  
00007FF60EC718CF  mov         dword ptr [rbp+44h],eax  
	printf("%d\n", c);
00007FF60EC718D2  mov         edx,dword ptr [rbp+44h]  
00007FF60EC718D5  lea         rcx,[00007FF60EC79C24h]  
00007FF60EC718DC  call        00007FF60EC71195  
	return 0;
00007FF60EC718E1  xor         eax,eax  
}

 2. 函数栈帧的创建

这里我看到 main 函数转化来的汇编代码如上所示。
接下来我们就一行行拆解汇编代码

00007FF60EC718A0  push        rbp  
00007FF60EC718A2  push        rdi  
00007FF60EC718A3  sub         rsp,148h  
00007FF60EC718AA  lea         rbp,[rsp+20h]  

(这里的rbp与rsp就如上讲的esp与ebp一致,上面说过,不同的编译器,甚至不同的版本都有一些差别,但不影响我们理解)

上面我就已经提过了main不是直接调用的, main 函数调用之前,是由invoke_main 函数来调用main函数。 也就是说main函数调用前,已经存在了栈顶指针rsp栈底指针rbp指向invoke_main函数的栈帧

 

 我们再进行操作:

第一句:

00007FF60EC718A0  push        rbp   
 //把rbp寄存器中的值进行压栈,此时的rbp中存放的是
 //invoke_main函数栈帧的rbp,rsp-8;

 

在执行前我们还可调出内存窗口看看是否如此:

执行前:

执行后:

我们看到rsp的地址减去了8(64位) 

 第二句:

同理:

00007FF60EC718A2  push        rdi  
//把rdi寄存器中的值进行压栈,rsp-8
//不用知道这个rdi存的是什么,对本次理解不影响

  

 第三句:

00007FF60EC718A3  sub         rsp,148h  
//sub会让rsp中的地址减去一个16进制数字148h,产生新的
rsp,此时的rsp是main函数栈帧的rsp;

 如图:

第四句:

00007FF60EC718AA  lea         rbp,[rsp+20h]
//lea的意思是load effecitive address“加载有效地址”也就是将后面的地址给rbp
//这里的rbp与前面的rsp就形成了main函数的栈帧
//,这一段空间中将存储main函数中的局部变量,临时数据已经调试信息等。

 

 我们接着看下面句:

	int a = 10;
00007FF60EC718AF  mov         dword ptr [rbp+4],0Ah  
	int b = 20;
00007FF60EC718B6  mov         dword ptr [rbp+24h],14h  
	int c = 0;
00007FF60EC718BD  mov         dword ptr [rbp+44h],0  

这几句的意思一样:我们只说第一句 

	int a = 10;
00007FF60EC718AF  mov         dword ptr [rbp+4],0Ah  //将10存储到rbp+4的地址处,rbp+4的位置其实就是a变量

其他的两句同理:

 

接着往下看:

	c = Add(a, b);
00007FF60EC718C4  mov         edx,dword ptr [rbp+24h]  
00007FF60EC718C7  mov         ecx,dword ptr [rbp+4]  
00007FF60EC718CA  call        00007FF60EC71348  
00007FF60EC718CF  mov         dword ptr [rbp+44h],eax  

 前面很好理解,就是将后面地址存的数据(其实不难发现,就是前面的a,b),读取到前面的寄存器;

对函数有很深的理解的同学就会发现,这不就是函数传参吗,对,确实是这样的;

我们不难发现,函数传参是从b到a即从右到左;

 我们这个时候监视变量页可以看看是否与我们预想的一样:

 与我们预想的一致(截图中显示的是16进制)

我们接着往下看,注意此时栈顶指针的值,和栈顶的值:

call的意思就要进入函数了,此时调试要进入函数就要摁F11了 

执行后:

 我们看到了,他不仅执行了一个压栈操作,压入的值不是一个随便的数,而是call的下一句代码的地址,这是为了执行完Add函数后可以回来:

3.进入自定义函数: 

int Add(int x, int y)
{
00007FF60EC717A0  mov         dword ptr [rsp+10h],edx  
00007FF60EC717A4  mov         dword ptr [rsp+8],ecx  
00007FF60EC717A8  push        rbp  
00007FF60EC717A9  push        rdi  
00007FF60EC717AA  sub         rsp,0E8h  
00007FF60EC717B1  mov         rbp,rsp  
	int z = 0;
00007FF60EC717B4  mov         dword ptr [rbp+4],0  
	z = x + y;
00007FF60EC717BB  mov         eax,dword ptr [rbp+0000000000000108h]  
00007FF60EC717C1  mov         ecx,dword ptr [rbp+0000000000000100h]  
00007FF60EC717C7  add         ecx,eax  
00007FF60EC717C9  mov         eax,ecx  
00007FF60EC717CB  mov         dword ptr [rbp+4],eax  
	return z;
00007FF60EC717CE  mov         eax,dword ptr [rbp+4]  
}
00007FF60EC717D1  lea         rsp,[rbp+00000000000000E8h]  
00007FF60EC717D8  pop         rdi  
00007FF60EC717D9  pop         rbp  
00007FF60EC717DA  ret  

下面我们继续解读:

有了前面的基础我们理解起来就很容易了:

00007FF60EC717A0  mov         dword ptr [rsp+10h],edx  
00007FF60EC717A4  mov         dword ptr [rsp+8],ecx  

 

 

00007FF60EC717A8  push        rbp  
00007FF60EC717A9  push        rdi  
00007FF60EC717AA  sub         rsp,0E8h  
00007FF60EC717B1  mov         rbp,rsp 

 

 

	int z = 0;
00007FF60EC717B4  mov         dword ptr [rbp+4],0  
	z = x + y;
00007FF60EC717BB  mov         eax,dword ptr [rbp+0000000000000108h]  
00007FF60EC717C1  mov         ecx,dword ptr [rbp+0000000000000100h]  
00007FF60EC717C7  add         ecx,eax  
00007FF60EC717C9  mov         eax,ecx  
00007FF60EC717CB  mov         dword ptr [rbp+4],eax  

 

00007FF60EC717CE  mov         eax,dword ptr [rbp+4] 
将rbp+4地址处的值放在eax中,其实就是
把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。

 4. 函数栈帧的销毁

00007FF60EC717D1  lea         rsp,[rbp+0E8h]  
00007FF60EC717D8  pop         rdi  
00007FF60EC717D9  pop         rbp  
//弹出栈顶的值存放到rbp,栈顶此时的值恰好就是main函数的rbp,rsp+4,此时恢复了main函数的栈帧维//护,rsp指向main函数栈帧的栈顶,rbp指向了main函数栈帧的栈底。
00007FF60EC717DA  ret  
//ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指
//令下一条指令的地址,此时rsp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行。

回到了call指令的下一条指令的地方:

 继续执行下面的代码:

00007FF60EC718CF  mov         dword ptr [rbp+44h],eax
//将eax放到rbp+44h(c存的地方)

这里虽然后面还有不少的代码,我们不做了解了 


总结

这期比较抽象,小编写这章主要然大家了解函数的调用,现在,你能回答上面的问题了吗?

下期见!

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

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

相关文章

盘点4款可以免费使用的高效ai PPT制作工具。

平时我们自己制作一个PPT还是需要比较长的时间的&#xff0c;从构思内容&#xff0c;到制作主题和逻辑框架&#xff0c;然后是挑选模板、排版配色等&#xff0c;过程比较繁琐且费时。但是&#xff0c;现在出现了很多的AIPPT制作工具&#xff0c;能够快速的帮助用户生成一个完整…

来自工业界的知识库 RAG(六),独特的 RAG 框架 dsRAG 核心亮点解读

背景介绍 在前面介绍了较多的开源 RAG 框架&#xff0c;比如主打 Rerank 的 QAnything, 主打精细文件解析的 RagFlow, 主打模块化灵活组合的 GoMate。这些库的设计除了少量的独特之处外&#xff0c;相似的部分很多。 最近有注意到一款另类的 RAG 框架 dsRAG&#xff0c;使用了…

openGauss在龙芯平台部署的实践

服务器环境 系统信息 NAME"Loongnix-Server Linux" VERSION"8" ID"loongnix-server" ID_LIKE"rhel fedora centos" VERSION_ID"8" PLATFORM_ID"platform:lns8" PRETTY_NAME"Loongnix-Server Linux 8"…

第四十篇-TeslaP40+Ollama+Ollama-WebUI(自编译)

本文介绍用自己编辑ollama-webui,链接本地ollama 环境 系统&#xff1a;CentOS-7 CPU: 14C28T 内存&#xff1a;32G 显卡&#xff1a;Tesla P40 24G 驱动: 535 CUDA: 12.2 Ollama: 0.3.0本地ollama 参考 [第二十四篇-Ollama-在线安装](https://blog.csdn.net/hai4321/articl…

2024软考:一场与“难”共舞的奇妙冒险,你值得拥有!

在这个时代&#xff0c;如果说有什么考试能让IT界的勇士们闻风丧胆&#xff0c;又爱又恨&#xff0c;那软考绝对能C位出道&#xff0c;成为众多技术大佬心中的“白月光”与“朱砂痣”。随着岁月悠悠&#xff0c;2024年的软考似乎又悄悄地在难度上动了点小心思&#xff0c;让人不…

vue设置水印

水印图例 1.新建Watermark.js 文件 const watermark {}const setWatermark (text, sourceBody) > {const id Math.random() * 10000 - Math.random() * 10000 / Math.random() * 10000if (document.getElementById(id) ! null) {document.body.removeChild(document.getE…

阿里云服务器开放端口的完整版图文教程

原文&#xff1a;阿里云服务器开放端口完整版教程&#xff1a;https://www.yundashi168.com/488.html 笔者近期开发完成的服务端程序部署在阿里云的ECS云服务器上面&#xff0c;一些应用程序配置文件需要设置监听的端口&#xff08;如Tomcat的8080、443端口等&#xff09;&…

萤石云 移动端demo指南

再来一篇&#xff0c;这次是萤石开放平台移动端demo的使用指南 一、Demo使用指南 从官网下载demo&#xff0c;下载地址demo首页如下&#xff1a; 填入对应参数 输入框是否必填解释服务器区域是国内选择Asia-China&#xff0c;海外选择对应的区域。选择后ApiUrl和WebUrl会自动…

餐饮行业eHR人力资源管理系统应该如何选择?

数字化转型与增长成为餐饮企业品牌竞争的创新壁垒&#xff0c;越来越多的餐饮企业&#xff08;门店&#xff09;依托数字化工具和手段&#xff0c;覆盖从内部组织到外部的数字化升级&#xff0c;包括员工管理、营销、客户管理&#xff0c;以及采购供应链等各环节的数字化运营。…

Netty从入门到超神-NIO 三大核心(selector,channel,buffer)(二)

前言 上一篇文章认识了一下Java的三大IO&#xff0c;这一章节我们详细了解一下NIO的工作原理以及三大核心Selector,Channel,Buffer并尝试来做一些小案例。 Java NIO 模型 Java NIO有三个核心的组件&#xff1a; selector 选择器 &#xff0c; channel 通道 &#xff0c; buf…

SpringBoot日常:Spring之@PostConstruct解析

简介 spring的Bean在创建的时候会进行初始化&#xff0c;而初始化过程会解析出PostConstruct注解的方法&#xff0c;并反射调用该方法。 PostConstruct 的使用和特点 只有一个非静态方法能使用此注解&#xff1b;被注解的方法不得有任何参数&#xff1b;被注解的方法返回值必…

Marin说PCB之TP测试的Layout设计要求

提及到TP点这个器件想必诸位道友们肯定不会陌生吧&#xff0c;我们的单板在量产之前都是需要做很多测试的&#xff0c;一般在产品研发的A版本和B版本的时候都是需要在单板上加上这个器件的。小编我最近在做一个改板&#xff0c;项目组为了降本增效&#xff0c;把单板的尺寸缩小…

Git 忽略已经提交的文件

对于未提交过的文件直接用ignore文件即可,不再赘述 对于已经提交过的文件,但是实际上不需要的,可以用git rm --cached命令 比如下图这个 .vsconfig被我误提交了或者忘了在ignore里添加了 但是我实际上不想要这个文件,那么在项目根目录打开git bash ,输入 git rm --cached .vsc…

LMDeploy 量化部署

创建环境和模型 conda create -n lmdeploy python3.10 -y conda activate lmdeploy conda install pytorch2.1.2 torchvision0.16.2 torchaudio2.1.2 pytorch-cuda12.1 -c pytorch -c nvidia -y pip install timm1.0.8 openai1.40.3 lmdeploy[all]0.5.3 mkdir /root/models …

[海思3403] 初始配置

虚拟机和板卡桥接 首先将虚拟机设置为桥接模式 板卡用网线和PC机连接&#xff0c;PC机用VMware打开Ubuntu虚拟机 点击虚拟网络编辑器&#xff0c;点击更改设置

爬取数据时,如何避免违法问题

目录 如何判断一个网站是否有明确禁止爬取数据&#xff1f; 如何处理爬取到的个人隐私数据以符合数据保护法规&#xff1f; 在爬取数据时&#xff0c;如何避免给目标网站带来过多的流量压力&#xff1f; 思维导图 在爬取数据时&#xff0c;避免违法问题的关键在于确保遵守相…

智慧工地:物联网技术和传感器技术的应用

随着科技的不断发展&#xff0c;物联网技术在各个领域得到了广泛的应用。在建筑行业中&#xff0c;智慧工地系统中物联网应用正逐渐成为一种趋势。本文将深入探讨智慧工地系统中物联网和传感器技术应用的内容。 物联网&#xff08;IoT&#xff09;和传感器技术在智慧工地中扮演…

养宠家庭除浮毛必入!希喂、安德迈、有哈宠物空气净化器真实对比

养过猫咪的铲屎官应该都体验过被换毛季支配的恐惧吧&#xff0c;夏天布偶的掉毛量已经全新升级了&#xff01;不仅是物体表面&#xff0c;连空气中都夹杂着浮毛&#xff0c;早上起来鼻子里偶尔都能发现它们的身影。长期生活在这样的环境中&#xff0c;肯定会对身体健康造成损害…

Spring数据访问层管理 ▎集成MyBatis ▎AOP ▎事务管理 ▎SpringWeb配置

前言: 在现代软件开发中&#xff0c;数据访问层的管理至关重要。Spring框架凭借其模块化结构和易用性&#xff0c;成为Java EE开发的首选。本文将探讨Spring在数据访问层的管理、MyBatis的集成、面向切面编程&#xff08;AOP&#xff09;、事务管理和Spring Web配置。 数据访…

【Material-UI】Select组件中的Native Select与TextField详解

文章目录 一、Select 组件概述1. 组件介绍2. Native Select 与 TextField 的区别 二、Native Select 组件详解1. 何为 Native Select2. Native Select 的基本用法3. Native Select 的优势与适用场景4. 自定义 Native Select 的样式 三、TextField 与 Select 的结合使用1. TextF…