【编译、链接、装载十三】内存中的栈——图解栈的运行过程

news2024/12/22 20:46:24

【编译、链接、装载十三】内存中的栈——图解栈的运行过程

  • 一、程序的内存布局
  • 二、栈
    • 1、什么是栈
    • 2、寄存器
  • 三、函数执行
  • 四、结合汇编——分析函数调用
    • 1、demo
    • 2、反汇编
    • 3、反汇——图解反汇编、栈帧、寄存器

一、程序的内存布局

看看加上动态链接之后进程的地址空间是如何分布的。现代的应用程序都运行在一个内存空间里, 在32位的系统里, 这个内存空间拥有4GB(2的32次方) 的寻址能力。

用户可以使用一个32位的指针访问任意内存位置。 例如:
int p = (int)0x12345678;
++*p;
这段代码展示了如何直接读写指定地址的内存数据。

大多数操作系统都会将4GB的内存空间中的一部分挪给内核使用, 应用程序无法直接访问这一段内存, 这一部分内存地址被称为内核空间。 Windows在默认情况下会将高地址的2GB空间分配给内核(也可配置为1GB) , 而Linux默认情况下将高地址的1GB空间分配给内核,

一般来讲, 应用程序使用的内存空间里有如下“默认”的区域。

  • 栈: 栈用于维护函数调用的上下文, 离开了栈函数调用就没法实现。

后面将对栈作详细的介绍。 栈通常在用户空间的最高地址处分配,通常有数兆字节的大小。

  • 堆: 堆是用来容纳应用程序动态分配的内存区域, 当程序使用malloc或new分配内存时, 得到的内存来自堆里。

堆会后面详细介绍。 堆通常存在于栈的下方(低地址方向) , 在某些时候, 堆也可能没有固定统一的存储区域。 堆一般比栈大很多, 可以有几十至数百兆字节的容量。

  • 可执行文件映像: 这里存储着可执行文件在内存里的映像,

前面已经提到过,由装载器在装载时将可执行文件的内存读取或映射到这里。在此不再详细说明。

  • 保留区: 保留区并不是一个单一的内存区域, 而是对内存中受到保护而禁止访问的内存区域的总称,

例如, 大多数操作系统里, 极小的地址通常都是不允许访问的, 如NULL。 通常C语言将无效指针赋值为0也是出于这个考虑, 因为0地址上正常情况下不可能有有效的可访问数据。

下图是Linux下一个进程里典型的内存布局。
在这里插入图片描述
图中的箭头标明了几个大小可变的区的尺寸增长方向, 在这里可以清晰地看出栈向低地址增长, 堆向高地址增长。 当栈或堆现有的大小不够用时, 它将按照图中的增长方向扩大自身的尺寸, 直到预留的空间被用完为止。

二、栈

1、什么是栈

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

  • 在经典的计算机科学中, 栈被定义为一个特殊的容器, 用户可以将数据压入栈中(入栈, push) , 也可以将已经压入栈中的数据弹出(出栈,pop) , 但栈这个容器必须遵守一条规则: 先入栈的数据后出栈(First In Last Out, FIFO)

  • 在计算机系统中, 栈则是一个具有以上属性的动态内存区域。 程序可以将数据压入栈中, 也可以将数据从栈顶弹出。 压栈操作使得栈增大, 而弹出操作使栈减小。

  • 在经典的操作系统里, 栈总是向下增长的。

在栈上压入数据会导致esp减小,弹出数据使得esp增大。相反,直接减小esp的值也等效于在栈上开辟空间,直接增大esp的值等效于在栈上回收空间。

栈在程序运行中具有举足轻重的地位。最重要的,栈保存了一个函数调用所需要的维护信息,这常常被称为堆栈帧(Stack Frame)或活动记录(Activate Record)。

堆栈帧一般包括如下几方面内容:

  • 函数的返回地址和参数。
  • 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
  • 保存的上下文:包括在函数调用前后需要保持不变的寄存器。

2、寄存器

在x86系统的CPU中,rsp是栈指针寄存器,这个寄存器中存储着栈顶的地址。 rbp中存储着栈底的地址。 函数栈空间主要是由这两个寄存器来确定的。当程序运行时,栈指针RSP可以移动,栈指针和帧指针rbp一次只能存储一个地址,所以,任何时候,这一对指针指向的是同一个函数的栈帧结构。而帧指针rbp是不移动的,访问栈中的元素可以用-4(%rbp)或者8(%rbp)访问%rbp指针下面或者上面的元素。

ebp固定在图中所示的位置,不随这个函数的执行而变化,相反地,esp始终指向栈顶,因此随着函数的执行,esp会不断变化。固定不变的ebp可以用来定位函数活动记录中的各个数据。

总结

  • 这些寄存器是cup中的,不是内存。寄存器存的是内存地址。通过这个地址来操作。
  • rsb、rbp等这些寄存器只有一个。一个rsb,一个rbp.
  • rsp,存储的地址是变动的。
  • rbp,栈底的地址是不变的,但是可以通过-4(%rbp)访问局部变量。

三、函数执行

函数总是这样调用的:

  • 把所有或一部分参数压入栈中, 如果有其他参数没有入栈, 那么使用某些特定的寄存器传递。
  • 把所有或一部分参数压入栈中, 如果有其他参数没有入栈, 那么使用某些特定的寄存器传递。
  • 把当前指令的下一条指令的地址压入栈中。
  • 跳转到函数体执行。

其中第2步和第3步由指令call一起执行。 跳转到函数体之后即开始执行函数, 而i386函数体的“标准”开头是这样的(但也可以不一样) :

  1. 第1步

push ebp: 把ebp的值压入栈中(把上一个函数栈帧的栈底的值,也就是rbp的值,压入栈中。) 。

  1. 第2步

mov ebp, esp: ebp = esp(这时ebp保存的值指向栈顶, 而且我们之前说过,栈顶是变化的) 。我们上一步把ebp的值保存了,现在又给ebp赋了新值,新值是啥呢?当然就是那个变动的栈帧的值了。

  1. 第3步

【可选】 sub esp, XXX: 在栈上分配XXX字节的临时空间。这个空间的大小和这个程序内的局部变量的多少有关系的,不包括调用的函数。我做过实验,如果被调用函数比较简单,没有调用其他的话,可能就没有这一句,也就是不用分配临时空间。

  • 分了临时空间,栈顶的值就又变了呵。比如

sub $0x10,%rsp

$0x10代表16个字节的空间,rsp-10,就是新的rsp的值了。

  • 这个值是以16字节递增的,比如你的临时变量用10个字节,那么就会分16个字节,如果用了20个字节,那么就会分32个字节。

四、结合汇编——分析函数调用

1、demo

#include<stdio.h>
int g_y =2;
int add(int a,int b)
{
	int c = a+b;
	printf("c = %d",c);
	return c;
}


int main()
{
	int x = 1;

	int stack1 = 1;
	int stack2= 1;

	int sum = add(x,g_y);
	printf("sum = %d",sum);
	return 1;
}

2、反汇编

[dev1@localhost test]$ gcc main.c
[dev1@localhost test]$ objdump -d -x a.out
00000000004004d7 <add>:
  4004d7:       55                      push   %rbp
  4004d8:       48 89 e5                mov    %rsp,%rbp
  4004db:       48 83 ec 20             sub    $0x20,%rsp
  4004df:       89 7d ec                mov    %edi,-0x14(%rbp)
  4004e2:       89 75 e8                mov    %esi,-0x18(%rbp)
  4004e5:       8b 55 ec                mov    -0x14(%rbp),%edx
  4004e8:       8b 45 e8                mov    -0x18(%rbp),%eax
  4004eb:       01 d0                   add    %edx,%eax
  4004ed:       89 45 fc                mov    %eax,-0x4(%rbp)
  4004f0:       8b 45 fc                mov    -0x4(%rbp),%eax
  4004f3:       89 c6                   mov    %eax,%esi
  4004f5:       bf f0 05 40 00          mov    $0x4005f0,%edi
  4004fa:       b8 00 00 00 00          mov    $0x0,%eax
  4004ff:       e8 ec fe ff ff          callq  4003f0 <printf@plt>
  400504:       8b 45 fc                mov    -0x4(%rbp),%eax
  400507:       c9                      leaveq
  400508:       c3                      retq

0000000000400509 <main>:
  400509:       55                      push   %rbp
  40050a:       48 89 e5                mov    %rsp,%rbp
  40050d:       48 83 ec 10             sub    $0x10,%rsp
  400511:       c7 45 fc 01 00 00 00    movl   $0x1,-0x4(%rbp)
  400518:       c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
  40051f:       c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%rbp)
  400526:       8b 15 f8 0a 20 00       mov    0x200af8(%rip),%edx        # 601024 <g_y>
  40052c:       8b 45 fc                mov    -0x4(%rbp),%eax
  40052f:       89 d6                   mov    %edx,%esi
  400531:       89 c7                   mov    %eax,%edi
  400533:       e8 9f ff ff ff          callq  4004d7 <add>
  400538:       89 45 f0                mov    %eax,-0x10(%rbp)
  40053b:       8b 45 f0                mov    -0x10(%rbp),%eax
  40053e:       89 c6                   mov    %eax,%esi
  400540:       bf f7 05 40 00          mov    $0x4005f7,%edi
  400545:       b8 00 00 00 00          mov    $0x0,%eax
  40054a:       e8 a1 fe ff ff          callq  4003f0 <printf@plt>
  40054f:       b8 01 00 00 00          mov    $0x1,%eax
  400554:       c9                      leaveq
  400555:       c3                      retq
  400556:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40055d:       00 00 00

3、反汇——图解反汇编、栈帧、寄存器

《程序员的自我修养》这本书挺好的,但是这个章节的分析,不少地方很容易让人误解和曲解,所以,我就不贴里面的图和分析了。

我们从main函数开始分析。

  1. push %rbp: 将 %rbp 寄存器的值压入栈,为后续的函数调用准备栈帧。
  400509:       55                      push   %rbp
  • 图解:内存的那里我写了rbp,是想表达rbp寄存器存的值是这个位置,为了大家更好的理解。并非说寄存器在内存中,这里大家心中明白就行,后面我不再单独说明了。
    在这里插入图片描述
  1. mov %rsp,%rbp: 将当前栈指针 %rsp 的值复制到基址指针 %rbp,建立当前函数的栈帧。
  40050a:       48 89 e5                mov    %rsp,%rbp
  • 图解:我们假设,rsp的值就在图中的位置
    在这里插入图片描述
  1. sub $0x10,%rsp: 从栈指针 %rsp 中减去 16 字节(0x10),为局部变量分配空间。这创建了一个大小为 16 字节的栈帧。
  40050d:       48 83 ec 10             sub    $0x10,%rsp
  • 图解
    在这里插入图片描述

sub $0x10,%rsp会改变栈顶的值吗
ChatGPT
是的,sub $0x10,%rsp 指令会改变栈顶的值。这条指令的作用是将栈指针 %rsp 的值减去 16 字节(0x10),以在栈上为局部变量分配空间。
由于栈向下增长,减去一个较小的值(16 字节)将使栈指针向下移动,即将栈顶指针向下推移。这样,栈顶的位置会向较低的地址移动,为新分配的局部变量提供空间。
在这种情况下,sub $0x10,%rsp 指令会将栈顶指针向下移动 16 字节,为后续的局部变量分配提供空间。

  1. movl $0x1,-0x4(%rbp): 将值 1 存储在相对于 %rbp 偏移为 -4 的位置,即在栈帧中分配一个 4 字节的整数变量。
400511:       c7 45 fc 01 00 00 00    movl   $0x1,-0x4(%rbp)
  • 图解
    在这里插入图片描述
  1. movl $0x1,-0x8(%rbp): 将值 1 存储在相对于 %rbp 偏移为 -8 的位置,再分配一个 4 字节的整数变量。
  400518:       c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
  1. movl $0x1,-0xc(%rbp): 将值 1 存储在相对于 %rbp 偏移为 -12 的位置,分配一个额外的 4 字节的整数变量。
  40051f:       c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%rbp)
  • 图解
    在这里插入图片描述
  1. mov 0x200af8(%rip),%edx: 从相对于 %rip 偏移为 0x200af8 的内存位置读取值到 %edx 寄存器中。这是一个全局变量 g_y 的值。
  400526:       8b 15 f8 0a 20 00       mov    0x200af8(%rip),%edx        # 601024 <g_y>
  1. mov -0x4(%rbp),%eax: 将相对于 %rbp 偏移为 -4 的位置的值读取到 %eax 寄存器中,即读取前面存储的值 1。
  40052c:       8b 45 fc                mov    -0x4(%rbp),%eax
  • 图解
    在这里插入图片描述
  1. mov %edx,%esi: 将 %edx 寄存器的值复制到 %esi 寄存器,为函数调用准备第一个参数。
  40052f:       89 d6                   mov    %edx,%esi
  1. mov %eax,%edi: 将 %eax 寄存器的值复制到 %edi 寄存器,为函数调用准备第二个参数。
  400531:       89 c7                   mov    %eax,%edi
  1. callq 4004d7 : 调用 add 函数,执行函数调用并跳转到地址 4004d7。
  400533:       e8 9f ff ff ff          callq  4004d7 <add>

这里其实有两个过程,

  • 第一步:把当前指令的下一条指令的地址——压入栈中。

  • 第二步:跳转到函数执行
    在这里插入图片描述

  • 接下来,我们跳转到了add函数,我们来分析下。其实前三行指令,和main函数中是一样的。我们假设跳转前,原来rbp的值是oxbfff eeee,rsp的值为0xbfff ee00
    在这里插入图片描述

12.同main中一样,add中的前三行指令

00000000004004d7 <add>:
  4004d7:       55                      push   %rbp
  4004d8:       48 89 e5                mov    %rsp,%rbp
  4004db:       48 83 ec 20             sub    $0x20,%rsp
  • 第1步,保存rbp的值,把rbp压入栈中,(你没看错,压入rbp的值,rsp的值又变小了)
    在这里插入图片描述

  • 第2步,rbp = rsp
    在这里插入图片描述

  • 第3步,分配32字节的空间
    在这里插入图片描述

  1. add中的最后两条指令
  400507:       c9                      leaveq
  400508:       c3                      retq

在这段代码中,可以看到指令 leaveq,它的作用是恢复栈帧并返回。

指令 leaveq 的作用等效于以下两条指令的组合:

mov %rbp, %rsp
pop %rbp
  • 首先,mov %rbp, %rsp 将栈指针 %rsp 的值设置为基址指针 %rbp 的值,这会将栈指针移回到调用者的栈帧上。

1.也就是rsp=rbp。开始进函数时rbp=rsb,把rsp的值赋值给rbp,现在离开了把rbp的值赋值给rsp,有没有觉得,rsp又恢复到调用前了,对不对,老铁。
2.弹出rbp
在这里插入图片描述

然后,pop %rbp 会从栈上弹出之前保存的基址指针 %rbp 的值,恢复调用者的栈帧。
在这里插入图片描述

  • 返回地址,也就是main函数中,调用函数call指令的下一条指令的地址。

add函数中,从寄存器取值我们就不说了,原理很简单,main函数往寄存器里面存值,add函数中从寄存器中取值,毕竟都是一个寄存器吗。

因为栈是向下生长的,因此,我们的图解,也是向下生长的。

到此,我们就分析完了,现在有没有觉得函数调用过程,栈帧,寄存器,调用过程清晰多了?

参考
1、《程序员的自我修养链接装载与库》

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

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

相关文章

详解Spring配置文件

⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;努力输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;JavaEE、Spring Spring配置文件 1、简介2、XML3、\<Bean\>3.1、Bean标签范围配置3.2、生命周期3.3、⭐实例…

word文档批量生成工具(附免费软件)(按Excel表格内容自动替换内容生成文档)

批量生成word文档是让人无比厌恶但有时又不得不做的事情。比如学校要给拟录取的学生发通知书&#xff0c;就可能需要批量生成一批只有“姓名”、“学院”和“专业”不同&#xff0c;其他内容都相同的word文档以供打印&#xff08;事实上直接生成pdf是更好的选择&#xff0c;这个…

chatgpt赋能python:Python如何计算p值?

Python如何计算p值&#xff1f; 在统计学中&#xff0c;p值是估计观察到的结果是由随机因素导致的概率。在Python中&#xff0c;我们可以使用一些统计库来计算p值。 常见的统计库 Python中有很多统计库可以用来计算p值。其中&#xff0c;SciPy是最常用的统计库之一。它包含了…

【American English】去超市买东西常用对话,物品名字

不懂不丢人&#xff0c;不懂装懂才丢人。最近有点犯这毛病&#xff0c;多写一些东西消除一下。 无论什么知识都是多了才能成体系&#xff0c;更多自己在美国的小总结可见专栏&#xff1a;English。 文章目录 找寻物品优惠或折扣试吃结账退货离开 找寻物品 Excuse me, where can…

Linux——文件的概念、操作和理解

引言 文件 文件内容 文件属性 要操作文件&#xff0c;就要先打开文件。根据冯诺依曼体系&#xff0c;只能操作内存中的数据。因此要先把文件内容加载到存储器&#xff0c;即内存中。 文件接口 语言层面的文件接口 FILE* fopen(const char *path, const char *mode); int …

网页3行字,成立4周就快成独角兽!大模型创业狂飙中

先来看看这家公司的官网截图&#xff1a; 对的&#xff0c;你没有看错&#xff0c;加上公司名字也就只有4行字。 人工智能正在迅速发展&#xff1a;一家成立仅四周的初创公司获得了1.13亿美元的种子轮融资&#xff0c;将与OpenAI竞争&#xff0c;在构建、训练和应用大型语言模…

Ubuntu 如何启动、停止或重启服务

在本文中&#xff0c;我们向您介绍在 Ubuntu 中启动、停止和重启服务的方法。 列出 Ubuntu 中的所有服务 在开始之前&#xff0c;先获取计算机上所有服务的列表&#xff0c;因为我们需要知道服务名称来管理服务。 service --status-all 它将显示 Ubuntu 上的完整服务列表。…

八卦图总结

第一阶段知识汇总 版本号&#xff1a;V1.1 作 者 姓 名&#xff1a; 学 科 专 业&#xff1a; 软件测试 文 档 类 型&#xff1a;第一阶段 汇 总 测 试 基 础 …

strapi系列--如何建表并在原有官方自动生成的api基础上扩展接口,定制化自己的业务逻辑

为什么要进行后端定制呢&#xff1f; 在实际开发过程中&#xff0c;项目中有些需求需要我们定制化自己的业务逻辑&#xff0c;那么我们该如何处理这个需求呢&#xff1f;本文以图文并茂的形式&#xff0c;基于原有官方自动生成的api&#xff0c;定制一个我们自己的业务逻辑接口…

“破解求职之谜:应届生如何应对职场场景挑战?

今天&#xff0c;我想与你分享一个关于应届生面试的故事。这是一个真实且令人深思的故事&#xff0c;也是我个人在求职过程中的一次经历&#xff0c;这也是对尚未面试者的警醒对已面试者的勉励。 在暑期还未开始之前&#xff0c;我充满着憧憬和期待地投递了许多简历&#xff0c…

chatgpt赋能python:Python第几行是什么?了解Python中的行号

Python第几行是什么&#xff1f;了解Python中的行号 如果你是一名Python工程师&#xff0c;你肯定曾经遇到过错误提示中提到的Python第几行。那么Python中的行号是什么&#xff1f;本文将为您解答关于Python中行号的一些基本问题。 什么是Python中的行号&#xff1f; 在Pyth…

测试用例设计方法

等价类划分法 设计测试用例步骤 需求分析划分等价类&#xff1a;有效和无效设计用例 无效有5种情况 规则&#xff08;需求本身&#xff09;长度类型是否为空&#xff08;必填项&#xff09;是否重复 案例1&#xff1a;QQ号6-10位自然数 划分等价类 有效等价类有效数据无…

深入理解深度学习——BERT派生模型:BART(Bidirectional and Auto-Regressive Transformers)

分类目录&#xff1a;《深入理解深度学习》总目录 UniLM和XLNet都尝试在一定程度上融合BERT的双向编码思想&#xff0c;以及GPT的单向编码思想&#xff0c;同时兼具自编码的语义理解能力和自回归的文本生成能力。由脸书公司提出的BART&#xff08;Bidirectional and Auto-Regre…

[Hadoop] 期末答辩问题准备

0.相关概念 1.什么是NameNode&#xff1f; NameNode是整个文件系统的管理节点&#xff0c;它维护着整个文件系统的文件目录树&#xff0c;文件/目录的元信息和每个文件对应的数据块列表。并接收用户的操作请求。 2.SecondaryNameNode的主要作用&#xff1f; SecondaryNameN…

JUC阻塞队列BlockingQueue---ArrayBlockingQueue

JUC阻塞队列BlockingQueue---ArrayBlockingQueue ArrayBlockingQueue示例代码原理构造方法内部常量入队put方法出队take方法 什么是阻塞队列&#xff1f; ArrayBlockingQueue ArrayBlockingQueue是典型的有界阻塞队列&#xff0c;其内部是用数组存储元素的&#xff0c;初始化…

chatgpt赋能python:Python程序安装指南

Python程序安装指南 介绍 Python 是一种高级编程语言&#xff0c;广泛应用于数据分析、人工智能、Web开发等领域。安装 Python 程序是使用 Python 和运行 Python 脚本的必要步骤。在本文中&#xff0c;我们将提供详细的 Python 程序安装指南。 安装Python程序步骤 步骤1: 下…

1.1-python课程简介

一、python入门 1、python来源2、什么是python3、python编程软件下载4、python软件安装5、python软件运行和调试 1、python来源 Guido van Rossum 于1989年在荷兰国家数学和计算机科学研究所设计出来的。 2、什么是python Python 是一个高层次的结合了解释性、编译性、互动性…

【推荐】win 安装 rust 1.70 (GNU)

目录 一、下载二、安装三、配置环境变量四、检查是否安装成功五、参考文章 一、下载 官网地址&#xff1a;https://www.rust-lang.org/zh-CN/ https://forge.rust-lang.org/infra/other-installation-methods.html 历史版本下载地址&#xff1a; 二、安装 注意&#xff1a;安…

TypeScript ~ TS Webpack构建工具 ⑦

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; TypeScript ~ TS &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &…

chatgpt赋能python:Python相加:实现快速、高效的计算

Python相加&#xff1a; 实现快速、高效的计算 Python 运用广泛&#xff0c;是一种功能强大的编程语言。它不仅易于学习&#xff0c;而且具备许多强大的功能&#xff0c;其中包括 Python 相加。今天&#xff0c;我们将介绍如何写 Python 相加的代码&#xff0c;以及如何实现快…