深入理解内存 —— 函数栈帧的创建与销毁

news2024/11/19 6:41:04

前言

        一位优秀的程序员,必须对内存的分布有深刻的理解,在初学编程的时候,往往有诸如以下很多问题困扰着初学者,而通过今天的分享,我们就可以通过自己的观察,将这些问题统统解决掉

  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机值?
  • 函数是怎么传参的?传参的顺序是怎么样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么调用的?
  • 函数调用后是怎么返回的?

目录

栈与栈帧的概念        

栈帧是如何在电脑上运作的

1.c语言代码

2.反汇编代码

主函数:

add函数:

函数栈帧的创建

1.创建 _tmainCRTStartup 的栈帧

2.创建 main 的栈帧

3.main函数数据的初始化 

4.add函数传参

5.创建add函数的栈帧

 6.add函数数据的初始化

7. add函数的返回

函数栈帧的销毁

1.add函数栈帧的销毁

2.add函数值的返回

 3.main函数栈帧的销毁


栈与栈帧的概念        

首先,什么是栈?

        在数据结构中我们学过 “栈” 这种结构,在数据结构中, 栈是限定仅在表尾进行插入或删除操作线性表。栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。

        在计算机系统中,栈也可以称之为栈内存是一个具有动态内存区域,存储函数内部(包括  main 函数)的局部变量和方法调用和函数参数值,是由系统自动分配的,一般速度较快;存储地址是连续且存在有限栈容量,会出现溢出现象程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使栈减小。 栈用于维护函数调用的上下文,离开了栈函数调用就没法实现。

那什么是栈帧呢?


        每一次函数的调用,都会在调用(call stack)上维护一个独立的栈帧(stack frame)。每个独立的栈帧一般包括:

  • 函数的返回地址和参数
  • 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  • 函数调用的上下文

        栈是从高地址低地址延伸,一个函数的栈帧用 ebp esp 这两个寄存器来划定范围.ebp 指向当前的栈帧的底部,esp 始终指向栈帧的顶部;

ebp 指向当前的栈帧的底部

ebp 寄存器又被称为帧指针(Frame Pointer)

esp 始终指向栈帧的顶部

esp 寄存器又被称为栈指针(Stack Pointer)

        另外,经过笔者的测试,这也与编译环境有关使用不同的编译器,或者不同的环境下,我们能直观看见的都是不一样的,但是俩者都是寄存器,只是体现不同罢了

  •         32位机器(esp,ebp)
  •         64位机器(rsp,rbp)

以下是笔者在VS2022上进行的测试:

栈帧是如何在电脑上运作的

        要想搞懂这个问题,我们就需要结合编译器给我们提供的反汇编代码,结合上我们写的代码进行分析

        我们先实现一个将俩个数相加的函数功能,然后在放进 main 函数中,并且进行调用,完成后输出结果,然后结束 main 函数。整个代码逻辑非常简单,具体实现如下:

1.c语言代码

#define _CRT_SECURE_NO_WARNINGS 1
#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", c);

	return 0;
}

2.反汇编代码

        我们完成上述代码后,按 F10 进行调试,然后鼠标右键单击 “转到反汇编”,然后我们就可以看到反汇编代码了

主函数:


int main()
{
001818D0  push        ebp  
001818D1  mov         ebp,esp  
001818D3  sub         esp,0E4h  
001818D9  push        ebx  
001818DA  push        esi  
001818DB  push        edi  
001818DC  lea         edi,[ebp-24h]  
001818DF  mov         ecx,9  
001818E4  mov         eax,0CCCCCCCCh  
001818E9  rep stos    dword ptr es:[edi]  
001818EB  mov         ecx,18C008h  
001818F0  call        0018132F  
	int a = 10;
001818F5  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
001818FC  mov         dword ptr [ebp-14h],14h  
	int c = 0;
00181903  mov         dword ptr [ebp-20h],0  

	c = add(a, b);
0018190A  mov         eax,dword ptr [ebp-14h]  
0018190D  push        eax  
0018190E  mov         ecx,dword ptr [ebp-8]  
00181911  push        ecx  
00181912  call        00181023  
00181917  add         esp,8  
0018191A  mov         dword ptr [ebp-20h],eax  

	printf("%d", c);
0018191D  mov         eax,dword ptr [ebp-20h]  
00181920  push        eax  
00181921  push        187B30h  
00181926  call        001810D7  
0018192B  add         esp,8  

	return 0;
0018192E  xor         eax,eax  
}
00181930  pop         edi  
00181931  pop         esi  
00181932  pop         ebx  
00181933  add         esp,0E4h  
00181939  cmp         ebp,esp  
0018193B  call        00181253  
00181940  mov         esp,ebp  
00181942  pop         ebp  
00181943  ret  

add函数:

int add(int x, int y)
{
00181870  push        ebp  
00181871  mov         ebp,esp  
00181873  sub         esp,0CCh  
00181879  push        ebx  
0018187A  push        esi  
0018187B  push        edi  
0018187C  lea         edi,[ebp-0Ch]  
0018187F  mov         ecx,3  
00181884  mov         eax,0CCCCCCCCh  
00181889  rep stos    dword ptr es:[edi]  
0018188B  mov         ecx,18C008h  
00181890  call        0018132F  
	int z = 0;
00181895  mov         dword ptr [ebp-8],0  
	z = x + y;
0018189C  mov         eax,dword ptr [ebp+8]  
0018189F  add         eax,dword ptr [ebp+0Ch]  
001818A2  mov         dword ptr [ebp-8],eax  

	return z;
001818A5  mov         eax,dword ptr [ebp-8]  
}
001818A8  pop         edi  
001818A9  pop         esi  
001818AA  pop         ebx  
001818AB  add         esp,0CCh  
001818B1  cmp         ebp,esp  
001818B3  call        00181253  
001818B8  mov         esp,ebp  
001818BA  pop         ebp  
001818BB  ret 

函数栈帧的创建

        我们知道,我要使用某一个函数,就要去调用他,一般常见的情况是在函数里面调用别的函数,就比如上面写的那一段很简单的代码,我们在 main 函数里面调用了 add 函数来实现了将俩个数相加的操作, main  函数是我们人为写的上去的,本身编译器是不会自带 main 函数的,当我们的代码写完了准备编译的时候,编译器得先扫描整个代码,找到 main 函数,然后从 main 函数开始执行代码,换言之 main 函数也是函数,也是需要被调用的。

        那么编译器用什么来拿到 main 函数,并且成功的调用他的呢?关于这一点,不同的编译器的实现是不一样的,比如在VS编译器中是使用的 _tmainCRTStartup 这样的内置函数来调用的。

1.创建 _tmainCRTStartup 的栈帧

编译器拿到一段完整的程序后首先会在栈区开辟一块空间,如下图所示:

2.创建 main 的栈帧

从这里开始结合反汇编代码进行观察

首先将 edp 押栈

001818D0  push        ebp  

 然后改变 edp 的指向

001818D1  mov         ebp,esp 

然后移动 esp 移动 0e4h 个单位

001818D3  sub         esp,0E4h

 到这里,其实就已经完成了对 main 函数栈区的创建,如图所示:

3.main函数数据的初始化 

 然后我们再继续结合反汇编代码 进行观察:

在这里连续押了3个元素入栈

001818D9  push        ebx  
001818DA  push        esi  
001818DB  push        edi

如图所示: 

         然后对刚才开辟的空间进行了初始化,并且全部赋值为 cccccccc ,这也解释了为什么平常没有初始化的数据的随机值是 ccccccccc 

001818DC  lea         edi,[ebp-24h]  
001818DF  mov         ecx,9  
001818E4  mov         eax,0CCCCCCCCh  
001818E9  rep stos    dword ptr es:[edi] 

 在完成初始化后,初始化 a=10,在这里一个 word 是 2 个字节,一个 dword 是 4 个字节

	int a = 10;
001818F5  mov         dword ptr [ebp-8],0Ah  

        

        我们可以成功的观察到,在 edp-8 这个位置,已经存放了 a=10,其余位置的 cccccccc 还是保留不变,这也就解释了平常随机值的大小为 cccccccc 的情况

 同理的,对 bc 都做初始化

 自此我们就完成了对数据的全部初始化,接下来就 add 函数了

4.add函数传参

在这里我们可以注意,传入的地址

  • edp-14h  就是之前初始化的 b=20
  • edp-8    就是之前初始化的 a=10

        也就是进行了函数传参的操作,通过下面的代码,我们更加可以理解函数的形参是实参的一份临时拷贝

	c = add(a, b);
0018190A  mov         eax,dword ptr [ebp-14h]  
0018190D  push        eax  
0018190E  mov         ecx,dword ptr [ebp-8]  
00181911  push        ecx

5.创建add函数的栈帧

这里的 call 就是调用的意思

00181912  call        00181023  
00181917  add         esp,8  
0018191A  mov         dword ptr [ebp-20h],eax

  

         按 F11 进入函数观察,我们会发现,这里的操作和上述 main 函数栈帧的操作几乎一模一样,也就是说,这里实际上是在创建 add 函数的栈帧

int add(int x, int y)
{
00181870  push        ebp  
00181871  mov         ebp,esp  
00181873  sub         esp,0CCh  
00181879  push        ebx  
0018187A  push        esi  
0018187B  push        edi  
0018187C  lea         edi,[ebp-0Ch]  
0018187F  mov         ecx,3  
00181884  mov         eax,0CCCCCCCCh  
00181889  rep stos    dword ptr es:[edi]  
0018188B  mov         ecx,18C008h 

 

 6.add函数数据的初始化

和上述 main 函数数据的初始化基本上是一样的

int z = 0;
00181895  mov         dword ptr [ebp-8],0  
	z = x + y;
0018189C  mov         eax,dword ptr [ebp+8]  
0018189F  add         eax,dword ptr [ebp+0Ch]  
001818A2  mov         dword ptr [ebp-8],eax  

这里就不再赘述,结果就是对 edp 附近的字节进行操作,最终达到成功赋值的目的

7. add函数的返回

        我们知道,函数使用的空间是临时的,在退出这个函数之后,他使用的这部分空间就被销毁了,那空间都被销毁了,该怎么样把返回值返回呢?

这是返回值 z 的创建位置: edp-8

int z = 0;
00181895  mov         dword ptr [ebp-8],0  

这是返回时的语句

return z;
001818A5  mov         eax,dword ptr [ebp-8] 

        我们观察发现,编译器是将 edp-8 的值放在了 eax 中,那 eax 是什么呢? eax 其实是寄存器寄存器不会因为 add 函数的销毁而销毁,他会持续的存在,用来保存 z 的值

函数栈帧的销毁

1.add函数栈帧的销毁

        pop 是弹出栈的意思,连续从栈顶弹出三个寄存器,之后继续更改 esp edp 指向的位置,最后,ret 会回到之前 call 指令留下的下一条指令的地址


001818A8  pop         edi  
001818A9  pop         esi  
001818AA  pop         ebx  
001818AB  add         esp,0CCh  
001818B1  cmp         ebp,esp  
001818B3  call        00181253  
001818B8  mov         esp,ebp  
001818BA  pop         ebp  
001818BB  ret 

如图所示:

 

         此时的栈顶指针,栈底指针就可以做到重新维护 main 函数的栈帧空间,因为之前 call 指令留下的地址,我们就可以做到 “出去又可以回来” 这对于我们管理空间是非常高效稳定的+

2.add函数值的返回

        这里实际上是更改栈顶指针的指向,通过这样的操作,我们就可以达到释放形参的目的,值得注意的是这段代码的最后一行

c = add(a, b);
0018190A  mov         eax,dword ptr [ebp-14h]  
0018190D  push        eax  
0018190E  mov         ecx,dword ptr [ebp-8]  
00181911  push        ecx  
00181912  call        00181023  
00181917  add         esp,8  
0018191A  mov         dword ptr [ebp-20h],eax  

        我们会发现,这里的 ebp-20h 和 eax 分别对应了前面对于 c 的初始化和对于 z 的值的保存,也就是说,这里就是将之前 eax 寄存器里放的 z 的值赋给 c,从而达到了

	c = add(a, b);

 的语句效果

int c = 0;
00181903  mov         dword ptr [ebp-20h],0  
return z;
001818A5  mov         eax,dword ptr [ebp-8]  

 3.main函数栈帧的销毁

        这里也是连续从栈顶弹出三个寄存器,之后继续更改 esp edp 指向的位置,最后 ret 退回上一级调用 main 函数的内置函数中,具体过程同上,这里就不再继续赘述

00181930  pop         edi  
00181931  pop         esi  
00181932  pop         ebx  
00181933  add         esp,0E4h  
00181939  cmp         ebp,esp  
0018193B  call        00181253  
00181940  mov         esp,ebp  
00181942  pop         ebp  
00181943  ret  

        以上就是本次分享的全部内容了,希望对屏幕前的您有所帮助,如有内容上的错误,欢迎指出,也欢迎积极讨论,内容制作不易,给个三连支持一下吧

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

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

相关文章

架构师必备--高可用高性能分布式数据库Tidb安装部署实践

本文针对分布式、高可用的tidb数据库&#xff0c;从搭建实际生产环境的集群服务&#xff0c;介绍下tidb的安装流程、安装前的环境检测和系统优化、服务访问等方面介绍下具体的流程&#xff0c;希望对大家熟悉和了解tidb数据库有所帮助&#xff0c;减少不必要的弯路。 1.概述 …

图形推理|解题思路汇总

一、图形推理题常见题型 &#xff08;一&#xff09;图形的位置 图形的位置主要是指与图形位置相关的图形推理考点规律。 这类题目的图形特点通常有以下两类: (1)题干图形相似程度很高&#xff0c;只是某些部分的位置有差异-&#xff0c;考虑移动、旋转与翻转。…

自动编码器中的马尔可夫链蒙特卡罗期望最大化 (MCMC-EM):使用贝叶斯推理增强学习

一、介绍 自动编码器 自动编码器是强大的无监督学习算法&#xff0c;用于表示学习和降维。它们的工作原理是将输入数据编码为低维表示形式&#xff0c;然后将其解码回以重建原始数据。训练自动编码器通常涉及优化参数以最小化重建误差。然而&#xff0c;传统的优化技术&#xf…

如何通过MAT排查生产环境服务内存溢出

前言 前段时间&#xff0c;运维反馈生产环境翻译服务某个节点触发内存告警了。运维在重启节点之前&#xff0c;生成了dump快照&#xff0c;这里介绍下如何使用MAT内存分析工具来排查服务内存高占用问题。 MAT简介 MAT是Memory Analyzer的简称&#xff0c;它是一款功能强大的…

前端技术Vue学习笔记--005

Vue学习笔记 一、非父子通信-event bus 事件总线 作用&#xff1a;非父子组件之间&#xff0c;进行简易消息传递。&#xff08;复杂场景用----Vuex&#xff09; 使用步骤&#xff1a; 创建一个都能访问的事件总线 &#xff08;空Vue实例&#xff09;-----utils/EventBus.js /…

浅谈更糟糕的 CS_CLASSDC 标志位的作用

在上一篇文章中&#xff0c;我们了解了 CS_OWNDC 标志位的历史&#xff0c;也说明了设计它的初衷。 这个标志位一开始看起来是个挺好的设计&#xff0c;但是如果你多琢磨一会儿&#xff0c;就会发现它不是一个好主意。今天我们来看看更糟的。 CS_CLASSDC 标志位有点类似 CS_OW…

shell脚本文本三剑客sed

shell脚本文本三剑客sed 一.Sed编辑器1.1sed概述1.2sed工作流程1.3sed基本法1.4sed常用选项1.5sed命令的常用操作 二.sed命令使用2.1打印内容2.2删除内容示例5&#xff1a;先备份内容在删除2.3插入内容2.4取反2.5搜索替代2.6分组调用 一.Sed编辑器 1.1sed概述 sed编辑器是一种…

Linux 虚拟机Ubuntu22.04版本通过远程连接连接不上,输入ifconfig只能看到127.0.0.1的解决办法

之前给虚拟机配置静态IP之后&#xff0c;可以直接通过主机Vscode远程连接。但是前一段时间把主机的TCP/IPV4静态IP设置了一下之后&#xff0c;再连接虚拟机就连不上了&#xff0c;于是参考解决虚拟机不能上网ifconfig只显示127.0.0.1的问题&#xff0c;又可以连接上了&#xff…

Python “贪吃蛇”游戏,在不断改进中学习pygame编程

目录 前言 改进过程一 增加提示信息 原版帮助摘要 pygame.draw pygame.font class Rect class Surface 改进过程二 增加显示得分 改进过程三 增加背景景乐 增加提示音效 音乐切换 静音切换 mixer.music.play 注意事项 原版帮助摘要 pygame.mixer pygame.mix…

SpringBoot复习:(56)使用@Transactional注解标记的方法的执行流程

首先&#xff0c;如果在某个类或某个方法被标记为Transactional时&#xff0c;Spring boot底层会在创建这个bean时生成代理对象&#xff08;默认使用cglib) 示例&#xff1a; 当调用studentService的addStudent方法时&#xff0c;会直接跳到CglibAopProxy类去执行intercept方…

vscode里配置C#环境并运行.cs文件

vscode是一款跨平台、轻量级、开源的IDE, 支持C、C、Java、C#、R、Python、Go、Nodejs等多种语言的开发和调试。下面介绍在vscode里配置C#环境。这里以配置.Net SDK v5.0&#xff0c;语言版本为C#9.0&#xff0c;对应的开发平台为VS2019&#xff0c;作为案例说明。 1、下载vsc…

vue3小知识点汇总——基础积累

下面的小知识点比较零散&#xff0c;但是脑子不太好使&#xff0c;只能先记录一下啦&#xff0c;后面知识丰富起来后&#xff0c;慢慢就懂了。 1.最新版node.js已经不兼容vue2的项目了&#xff0c;学习vue3势在必行 node.js的安装及vue的搭建详细步骤&#xff1a;http://t.cs…

比特币暴跌的4个原因

作者&#xff1a;秦晋 加密市场每隔一段时间&#xff0c;就会迎来一次「暴跌」&#xff0c;而且每次暴跌原因各不相同。但归根到底都是「恐慌情绪」在作怪。继「312暴跌」、「519暴跌」之后&#xff0c;又迎来一个「8.18暴跌」。相比前两次暴跌&#xff0c;此次暴跌的原因或许略…

想做赴日程序员 有一定技术经验不学日语可以赴日IT吗?

有的小伙伴问&#xff1a;我有一定的IT技术和经验&#xff0c;不学日语的话&#xff0c;能去做赴日IT工作吗&#xff1f;说实话啊&#xff0c;我感觉如果行的话&#xff0c;那只能说明你运气不错&#xff0c;因为日本的IT行业在日本来说&#xff0c;并不是非常高薪的行业&#…

Redis中的有序集合

前言 本文着重介绍Redis中的有序集合的底层实现中的跳表 有序集合 Sorted Set Redis中的Sorted Set 是一个有序的无重复值的集合&#xff0c;他底层是使用压缩列表和跳表实现的&#xff0c;和Java中的HashMap底层数据结构&#xff08;1.8&#xff09;链表红黑树异曲同工之妙…

【深入了解PyTorch】PyTorch实战项目示例:深入探索图像分类、目标检测和情感分析

【深入了解PyTorch】PyTorch实战项目示例:深入探索图像分类、目标检测和情感分析 PyTorch实战项目示例:深入探索图像分类、目标检测和情感分析项目一:图像分类数据集准备构建模型训练模型模型评估和预测项目二:目标检测数据集准备构建模型训练模型模型评估和预测项目三:情…

记录因暴露阿里最高权限的Accesskey和secretKey导致的反弹shell攻击过程

Accesskey和SecretKey的泄露的原因 说到这个最高权限的key的泄露&#xff0c;绝对是低级的设计导致的。为了减少服务端的压力&#xff0c;直接让app直连oss服务&#xff0c;而且把最高权限的Accesskey和secretKey 下发到客户端&#xff0c;那么结果就是只要安装了该app的人&am…

js判断用户当前网络状态和判断网速

前端判断用户当前网络状态和判断网速 一、第一种是通过 HTML5 提供的 navigator 去检测网络(1)、原理介绍:(2)、兼容性 二、监听window.ononline和window.onoffline事件:三、通过ajax进行请求判断(兼容性好-推荐)(1)、原理介绍:(2)、注意: 四、navigator.connection方法监听网络…

PP-TS基于启发式搜索和集成方法的时序预测模型,使预测更加准确

时间序列数据在各行业和领域中无处不在&#xff0c;如物联网传感器的测量结果、每小时的销售额业绩、金融领域的股票价格等等&#xff0c;都是时间序列数据的例子。时间序列预测就是运用历史的多维数据进行统计分析&#xff0c;推测出事物未来的发展趋势。 为加快企业智能化转型…

Vue elementui 实现表格selection的默认勾选,翻页记录勾选状态

需求&#xff1a;当弹出一个列表页数据&#xff0c;对其进行筛选选择。 列表更新&#xff0c;填充已选数据 主要使用toggleRowSelection 代码如下&#xff1a; <el-table v-loading"loading" :data"drugList" selection-change"handleSelection…