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

前言

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

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

目录

栈与栈帧的概念        

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

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.mzph.cn/news/43311.shtml

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

相关文章

keepalived集群

keepalived概述 keepalived软件就是通过vrrp协议来实现高可用功能。 VRRP通信原理 VRRP就是虚拟路由冗余协议&#xff0c;它的出现就是为了解决静态路由的单点故障。 VRRP是通过一种竞选一种协议机制来将路由交个某台VRRP路由器。 VRRP 用IP多播的方式&#xff08;多播地…

C语言中常见的一些语法概念和功能

常用代码&#xff1a; 程序入口&#xff1a;int main() 函数用于定义程序的入口点。 输出&#xff1a;使用 printf() 函数可以在控制台打印输出。 输入&#xff1a;使用 scanf() 函数可以接收用户的输入。 条件判断&#xff1a;使用 if-else 语句可以根据条件执行不同的代码…

【力扣每日一题】2023.8.15 字符中的查找与替换

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目很长&#xff0c;简而言之就是检查字符串中对应索引的位置是否有特定的字符串&#xff0c;如果有&#xff0c;那么替换&#xff0c;返…

Ceph如何操作底层对象数据

1.基本原理介绍 1.1 ceph中的对象(object) 在Ceph存储中&#xff0c;一切数据最终都会以对象(Object)的形式存储在硬盘&#xff08;OSD&#xff09;上&#xff0c;每个的Object默认大小为4M。 通过rados命令&#xff0c;可以查看一个存储池中的所有object信息&#xff0c;例如…

Optional的基础运用

Optional的基础运用 简介代码示例 简介 代码示例 package org.example;import org.junit.Test;import java.util.Optional;public class OptionalTest {Testpublic void advance() {String str "hello";str null;// of(T t):封装数据t生成Optional对象&#xff0c…

【笔试题心得】关于正则的一些整理

本文部分内容摘抄整理自 正则表达式 – 教程 | 菜鸟教程 在笔试的过程中&#xff0c;也常常会对正则表达式进行考察&#xff0c;这里对正则表达式的常见用法&#xff0c;做一个学习和总结。 正则表达式的模式可以包括以下内容&#xff1a; 字面值字符&#xff1a;例如字母、数…

数据结构:堆的实现

1.堆的概念 如果有一个关键码的集合 K { k1 &#xff0c;k2 &#xff0c;k3 &#xff0c;…&#xff0c;kn }&#xff0c;把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中&#xff0c;并且 k(i) < k(i*21) 和 k(i) < k(i*22)&#xff0c; i 0 &#xff…

MongoDB增删改查操作

数据库操作&#xff1a; 在MongoDB中&#xff0c;文档集合存在数据库中。 要选择使用的数据库&#xff0c;请在mongo shell程序中发出 use <db> 语句 // 查看有哪些数据库 show dbs;// 如果数据库不存在&#xff0c;则创建并切换到该数据库&#xff0c;存在则直接切换到…

C++之模板进阶

模板进阶 非类型模板参数模板的特化概念函数模板特化类模板特化全特化偏特化 模板分离编译什么是分离编译模板的分离编译解决方法 模板总结 非类型模板参数 模板参数分两种&#xff1a;类型形参与非类型形参。 类型形参&#xff1a;出现在模板参数列表中&#xff0c;跟在class…

drawio----输出pdf为图片大小无空白(图片插入论文)

自己在写论文插入图片时为了让论文图片放大不模糊&#xff0c;啥方法都试了&#xff0c;最后摸索出来这个。 自己手动画图的时候导出pdf总会出现自己的图片很小&#xff0c;pdf的白边很大如下如所示&#xff0c;插入论文的时候后虽然放大不会模糊&#xff0c;但是白边很大会显…

【数据结构OJ题】用队列实现栈

原题链接&#xff1a;https://leetcode.cn/problems/implement-stack-using-queues/ 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 可以用两个队列去实现一个栈&#xff0c;每次始终保持一个队列为空。 入栈相当于给非空队列进行入队操作。 出栈相…

异步电机IM-改进的电压模型磁链观测器学习

导读&#xff1a;本期文章主要介绍异步电机的改进型电压模型磁链观测器。传统纯积分形式的积分器在低速区域存在初始值问题和直流偏置问题&#xff0c;所以在实际应用中必须对电压模型进行改进。本期文章中的对电压模型改进是借鉴一篇IEEE中的方法。 如果需要文章中对应的仿真…

Apache Dubbo 云原生可观测性的探索与实践

作者&#xff1a;宋小生 - 平安壹钱包中间件资深工程师 Dubbo3 可观测能力速览 Apache Dubbo3 在云原生可观测性方面完成重磅升级&#xff0c;使用 Dubbo3 最新版本&#xff0c;你只需要引入 dubbo-spring-boot-observability-starter 依赖&#xff0c;微服务集群即原生具备以…

PSP - 基于扩散生成模型预测蛋白质结构 EigenFold 算法与环境配置

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132357976 Paper: EigenFold: Generative Protein Structure Prediction with Diffusion Models EigenFold 是用于蛋白质结构预测的扩散生成模型…

【面试八股文】每日一题:谈谈你对线程的理解

每日一题-Java核心-谈谈你对线程的理解【面试八股文】 Java线程是Java程序中的执行单元。一个Java程序可以同时运行多个线程&#xff0c;每个线程可以独立执行不同的任务。线程的执行是并发的&#xff0c;即多个线程可以同时执行。 1. 线程的特点 Java中的线程有如下的特点 轻…

react-native-webview使用postMessage后H5不能监听问题(iOS和安卓的兼容问题)

/* 监听rn消息 */ const eventListener nativeEvent > {//解析数据actionType、extraconst {actionType, extra} nativeEvent.data && JSON.parse(nativeEvent.data) || {} } //安卓用document&#xff0c;ios用window window.addEventListener(message, eventLis…

Jenkins-发送邮件配置

在Jenkins构建执行完毕后&#xff0c;需要及时通知相关人员。因此在jenkins中是可以通过邮件通知的。 一、Jenkins自带的邮件通知功能 找到manage Jenkins->Configure System&#xff0c;进行邮件配置&#xff1a; 2. 配置Jenkins自带的邮箱信息 完成上面的配置后&#xf…

DiffusionDet: Diffusion Model for Object Detection

DiffusionDet: Diffusion Model for Object Detection 论文概述不同之处整体流程 论文题目&#xff1a;DiffusionDet: Diffusion Model for Object Detection 论文来源&#xff1a;arXiv preprint 2022 论文地址&#xff1a;https://arxiv.org/abs/2211.09788 论文代码&#xf…

kubesphere 使用流水线对接 sonar

官方文档&#xff1a;使用图形编辑面板创建流水线 创建凭证 创建 sonar 凭证 创建 gitlab 凭证 创建流水线 创建流水线&#xff0c;编辑流水线 自定义流水线 拉取代码 代理选 kubernetes&#xff0c;label 填maven 添加步骤 - git 填写 git 地址&#xff0c;选…

CSS 背景属性

前言 背景属性 属性说明background-color背景颜色background-image背景图background-repeat背景图平铺方式background-position背景图位置background-size背景图缩放background-attachment背景图固定background背景复合属性 背景颜色 可以使用background-color属性来设置背景…