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

前言

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

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

目录

栈与栈帧的概念        

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

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  

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python Opencv实践 - 图像仿射变换

import cv2 as cv import numpy as np import matplotlib.pyplot as pltimg cv.imread("../SampleImages/pomeranian.png", cv.IMREAD_COLOR) rows,cols img.shape[:2] print(img.shape[:2])#使用getAffineTransform来获得仿射变换的矩阵M #cv.getAffineTransform(…

Microsoft ISA服务器配置及日志分析

Microsoft ISA 分析器工具&#xff0c;可分析 Microsoft ISA 服务器&#xff08;或 Forefront 威胁管理网关服务器&#xff09;的日志并生成安全和流量报告。支持来自 Microsoft ISA 服务器组件的以下日志&#xff1a; 数据包过滤器ISA 服务器防火墙服务ISA 服务器网络代理服务…

图片合成动图怎么弄?gif图制作的简单方法

许多鬼畜的表情包其实是用图片合成gif完成的&#xff0c;那么怎么将图片转gif呢&#xff1f;使用GIF中文网的gif合成&#xff08;https://www.gif.cn&#xff09;功能&#xff0c;打开浏览器就可以完成gif图片制作&#xff0c;非常简单方便&#xff0c;一起来了解一下吧。 打开…

智安网络|深入比较:Sass系统与源码系统的差异及选择指南

随着前端开发的快速发展&#xff0c;开发人员需要使用更高效和灵活的工具来处理样式表。在这个领域&#xff0c;Sass系统和源码系统是两个备受关注的选项。 Sass系统 Sass&#xff08;Syntactically Awesome Style Sheets&#xff09;是一种CSS预处理器&#xff0c;它扩展了CS…

Lnton羚通关于【PyTorch】教程:torchvision 目标检测微调

torchvision 目标检测微调 本教程将使用Penn-Fudan Database for Pedestrian Detection and Segmentation 微调 预训练的Mask R-CNN 模型。 它包含 170 张图片&#xff0c;345 个行人实例。 定义数据集 用于训练目标检测、实例分割和人物关键点检测的参考脚本允许轻松支持添加…

Modbus工业RFID设备在自动化生产线中的应用

传统半自动化生产线在运作的过程&#xff0c;因为技工的熟练程度&#xff0c;专业素养的不同&#xff0c;在制造过程中过多的人为干预&#xff0c;工厂将很难对每条生产线的产能进行标准化管理和优化。如果半自动化生产线系统是通过前道工序的作业结果和检测结果来决定产品在下…

实战指南,SpringBoot + Mybatis 如何对接多数据源

系列文章目录 MyBatis缓存原理 Mybatis plugin 的使用及原理 MyBatisSpringboot 启动到SQL执行全流程 数据库操作不再困难&#xff0c;MyBatis动态Sql标签解析 从零开始&#xff0c;手把手教你搭建Spring Boot后台工程并说明 Spring框架与SpringBoot的关联与区别 Spring监听器…

C语言好题解析(三)

目录 选择题一选择题二选择题三选择题四编程题一编程题二 选择题一 以下程序段的输出结果是&#xff08;&#xff09;#include<stdio.h> int main() { char s[] "\\123456\123456\t"; printf("%d\n", strlen(s)); return 0; }A: 12 B: 13 …

高并发内存池(centralcache)[2]

Central cache threadcache是每个线程独享&#xff0c;而centralcache是多线程共享&#xff0c;需要加锁&#xff08;桶锁&#xff09;一个桶一个锁 解决外碎片问题&#xff1a;内碎片&#xff1a;申请大小超过实际大小&#xff1b;外碎片&#xff1a;空间碎片不连续&#x…

redis 发布和订阅

目录 一、简介 二、常用命令 三、示例 一、简介 Redis 发布订阅 (pub/sub) 是一种消息通信模式&#xff1a;发送者 (pub) 发送消息&#xff0c;订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。下图展示了频道 channel1 &#xff0c;以及订阅这个频道的三个客户…

53.Linux day03 文件查看命令,vi/vim常用命令

今天进行了新的学习。 目录 1.cat a.查看单个文件的内容&#xff1a; b.查看多个文件的内容&#xff1a; c.将多个文件的内容连接并输出到一个新文件&#xff1a; d.显示带有行号的文件内容&#xff1a; 2.more 3.less 4.head 5.tail 6.命令模式 7.插入模式 8.图…

Nginx反向代理技巧

跨域 作为一个前端开发者来说不可避免的问题就是跨域&#xff0c;那什么是跨域呢&#xff1f; 跨域&#xff1a;指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的&#xff0c;是浏览器对javascript施加的安全限制。浏览器的同源策略是指协议&#xff0c;域名…

SQL Server Express 自动备份方案

文章目录 SQL Server Express 自动备份方案前言方案原理SQL Server Express 自动备份1.创建存储过程2.设定计划任务3.结果检查sqlcmd 参数说明SQL Server Express 自动备份方案 前言 对于许多小型企业和个人开发者来说,SQL Server Express是一个经济实惠且强大的数据库解决方…

机器学习基础之《分类算法(1)—sklearn转换器和估计器》

一、转换器 1、什么是转换器 之前做特征工程的步骤&#xff1a; &#xff08;1&#xff09;第一步就是实例化了一个转换器类&#xff08;Transformer&#xff09; &#xff08;2&#xff09;第二步就是调用fit_transform&#xff0c;进行数据的转换 2、我们把特征工程的接口称…

在 React+Typescript 项目环境中创建并使用组件

上文 ReactTypescript清理项目环境 我们将自己创建的项目环境 好好清理了一下 下面 我们来看组件的创建 组件化在这种数据响应式开发中肯定是非常重要的。 我们现在src下创建一个文件夹 叫 components 就用他专门来处理组件业务 然后 我们在下面创建一个 hello.tsx 注意 是t…

基于Echarts的大数据可视化模板:智慧门店管理

目录 引言智慧门店管理的重要性Echarts在智慧门店管理中的应用智慧门店概述定义智慧门店的概念和核心智慧门店的关键技术智慧门店的发展趋势与方向智慧门店管理的作用Echarts与大数据可视化Echarts库以及其在大数据可视化领域的应用优势开发过程和所选设计方案模板如何满足管理…

SpringBoot中的可扩展接口

目录 # 背景 # 可扩展的接口启动调用顺序图 # ApplicationContextInitializer # BeanDefinitionRegistryPostProcessor # BeanFactoryPostProcessor # InstantiationAwareBeanPostProcessor # SmartInstantiationAwareBeanPostProcessor # BeanFactoryAware # Applicati…

ARM体系结构学习笔记:CPU并不直接访问内存

CPU并不直接访问内存 原因: 寄存器可以更快的进行访问存取指令集 LDR STR 寻址模式 Pre-index [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KFSzzeZB-1692235692358)(https://cdn.jsdelivr.net/gh/nzcv/picgo/20220117071415.png)] Post-index …

Docker资源控制

目录 一、CPU 资源控制 1.设置CPU使用率上限 2.设置CPU资源占用比&#xff08;设置多个容器时才有效&#xff09; 3.设置容器绑定指定的CPU 二、对内存使用的限制 三、对磁盘IO配额控制&#xff08;blkio&#xff09;的限制 一、CPU 资源控制 cgroups&#xff0c;是一个非常强…

Spring源码深度解析二(AOP)

书接上文 9. AOP源码深度剖析 概述 AOP&#xff08;Aspect Orient Programming&#xff09;&#xff1a;面向切面编程&#xff1b; 用途&#xff1a;用于系统中的横切关注点&#xff0c;比如日志管理&#xff0c;事务管理&#xff1b; 实现&#xff1a;利用代理模式&#x…