内存分布和栈空间---Memory Layout And The Stack

内存分布和栈空间

Memory Layout And The Stack

原作者:Peter Jay Salzman

译者:刘瀚文

原文:Using GNU’s GDB Debugger Memory Layout And The Stack

导语

想要快速的学习使用GDB,你必须理解frames数据帧的含义,数据帧又叫做堆栈帧,因为堆栈就是由数据帧构成的。想要学习堆栈的知识,我们需要了解一个运行中的程序在内存中是如何分布的。这篇文章讨论的主要是理论知识,但是为了让这篇文章读起来没有那么枯燥乏味,我们将引入关于在堆栈堆栈帧上使用GDB的例子。

这篇文章虽然理论性较强,但是却能为我们提供十分有价值的学习目标:

  1. 想要使用典型的debugger比如说GDB,就必须理解堆栈。
  2. 通过学习程序的内存分布将有利于我们理解什么是segmentation fault段错误,并且明白为什么有时候会出现这个错误而有时候该出现的时候却没有出现(有时后者更关键)。总之,段错误是最常见的将程序变成‘定时炸弹’的原因。
  3. 了解程序的内存空间分布常常让我们能不通过printf、编译器或甚至GDB就找到一些隐藏的非常深的错误。下一节的内容是由我们的客人、我的朋友——Mark Kim写的,我们能看到像福尔摩斯一样的侦探,Mark在冗长的代码中找到了一个隐藏的很深的bug,并且他仅仅用了5到10分钟。他找到这个bug全靠盯着代码研究和运用程序内存分布的知识,这个例子一定为让读者印象深刻的。

好了,我们就不绕弯子了,赶快来看看程序是怎么在内存中分布的吧!

Virtual Memory (VM)——虚拟内存

每当一个程序开始运行,系统内核就会提供一大块能被从头到尾访问的physical memory物理内存。但是多亏了Virtual Memory虚拟内存,程序本身认为自己已经拥有访问整个计算机内存空间的能力。你可能在有关硬件的文章中听过虚拟内存这个概念,尽管那个也叫虚拟内存,但是实际上我们要讨论的这个虚拟内存不是那个当物理内存用光了之后才使用的虚拟内存。我们讨论的虚拟内存应该由以下的原则确定:

  1. 每一个进程拥有的物理内存块叫做这个进程的虚拟内存空间。

  2. 进程不知道自己物理内存的实际结构,只能知道这个被分配给它的内存块的大小和这个块的地址空间是从0开始算的。

  3. 每一个进程没办法了解到其他进程拥有的虚拟内存块
  4. 就算一个进程已经知道了其他虚拟内存块,但是仍然没有办法访问到那一块内存。

每当一个进程想要对内存进行读写,它都会从VM address虚拟内存地址转换成physical memory address物理内存地址。反之,当一个OS操作系统内核想要访问一个虚拟内存,就要把实际内存地址转换为虚拟内存地址。那么这里就会出现两个问题:

  1. 计算机要不断的访问内存,这个转换过程必定非常常见,所以这个转换过程就必须非常的迅速。
  2. 操作系统怎么来保证一个进程不会‘踏入’另一个进程的虚拟内存块中呢?

这两个问题的答案就是实际上操作系统本身不会去管理每个进程的虚拟内存,而是通过CPU的帮助。很多CPU拥有一个叫Memory Management Unit(MMU)的内存管理单元。操作系统MMU会共同管理虚拟内存、进行物理内存地址虚拟内存地址的转换、决定进程访问内存中某个位置的权限和决定进程在(甚至是本身的)虚拟内存空间的读写权限

过去Linux只能运行在含有MMU的CPU上(Linux没有办法运行在X286框架的CPU上)。然而在1998年时,Linux装载在了没有MMU68000上。这也是嵌入式Linux和在类似于Palm Pilot设备上的Linux的一大进步。

练习:

  1. 读一个关于MMU简短百科
  2. 选做:如果想要了解更多有关于VM虚拟内存的介绍点击这里,实际上这已经远远超过阅读这篇文章你需要了解的范畴。

Memory Layout——内存分布

上面讲了虚拟内存是怎么工作的。对于多数情况,每一个进程的虚拟内存是按照一个相似的、可预测的方式分布的:

内存地址内存结构说明
高地址Args and env vars命令行参数和环境变量
堆栈(Stack)
未使用的内存空间
堆(Heap)
没有初始化的数据段(bss)运行时初始化为零
初始化了的数据段运行时通过程序源文件读取
低地址文本段(Text Segment)运行时通过程序源文件读取

>

  • 文本段(Text Segment):文本段包含了真实运行的代码,通常是可以共享的,所以许多情况下程序可以共享文本段来满足较低内存情况下的需求。这个段通常是标记为只读的,好让一个程序不能改变自己本身的结构。
  • 初始化了的数据段(Initialized Data Segment):这个数据段包含了一开始就被初始化了的全局变量。
  • 没有初始化的数据段(Uninitialized Data Segment):以前汇编程序中又称为bss(block started by symbol)。这个数据段包含了一系列没有被初始化的全局变量。所有在这里面的变量在执行前都被初始化为0或者NULL pointers(空指针)
  • 栈(Stack)是包含了一系列下一部分会讲到的数据帧,当一个新的数据帧要被添加到栈里面的时候(比如说当一个构造函数被调用的时候),会向下增长。
  • 堆(Heap):大部分动态内存,无论是C语言malloc()还是C++new之类的操作的变量都会从上被发放到程序中。C语言的库还会从中获取动态内存来填充personal workspace(自己的工作空间)。因为大量的内存需要被不断读写,所以整个会往上增长。

对于给定的Object文件或者executable(可执行)文件,你都可以查明程序每一个部分的大小(注意我们这里并不是在讨论内存分布而是在讨论一个最终会被执行的硬盘中的文件)。

给定Makefilehello_world-1.c

1   // hello_world-1.c
2   
3   #include <stdio.h>
4   
5   int main(void)
6   {
7      printf("hello world\n");
8   
9      return 0;
10  }

编译这个文件并单独的通过以下命令进行link

$ gcc -W -Wall -c hello_world-1.c
$ gcc -o hello_world-1  hello_world-1.o

你可以使用size命令来列出各个变量数据段的大小:

 $ size hello_world-1 hello_world-1.o 
 text   data   bss    dec   hex   filename
  916    256     4   1176   498   hello_world-1
   48      0     0     48    30   hello_world-1.o

数据段data由已经初始化和未初始化的数据段混合而成。dechex字段则表示整个文件在二进制十六进制下的大小。

你也可以使用objdump -hobjdump -x来得到object文件各部分的大小:

$ objdump -h hello_world-1.o 
hello_world-1.o:     file format elf32-i386
Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000023  00000000  00000000  00000034  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  00000000  00000000  00000058  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  00000058  2**2
                  ALLOC
  3 .rodata       0000000d  00000000  00000000  00000058  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .note.GNU-stack 00000000  00000000  00000000  00000065  2**0
                  CONTENTS, READONLY
  5 .comment      0000001b  00000000  00000000  00000065  2**0
                  CONTENTS, READONLY

练习:

  1. size命令并没有列出hello_worldhello_world.o数据段,你觉得这是为什么呢?
  2. hello_world-1.c文件中并没有定义全局变量。请解释为什么size命令列出的结果中,object文件的data段bss段的长度为0,而在可执行文件中却不为0。
  3. size命令和objdump命令给出了不同的text segment(文本段)大小。你能猜到差异从何而来吗?(提示:这个差异有多大呢?看看在源文件中是否有这个大小的东西。)
  4. 选做:读读关于object文件格式的相关知识

Stack Frames And The Stack——数据帧和栈

我们刚刚学习了关于程序的内存分布,在程序的内存分布中有一个被stack frames(数据帧)填满的数据段叫做stack(栈)。每一个数据帧都代表了一个function call(函数调用)。当一个函数被调用的时候,数据帧的数量增加然后整个栈空间就增长了。反过来,当一个函数返回了,数据帧的数量就减少了然后整个栈空间就缩小了。这一节中我们会来学习什么是数据帧,这一节会给出很详细解释,当然也会着重看看我们需要重点了解的部分。

一个程序被或多或少、互相调用的函数组成。每当一个函数被调用,一块内存被搁到一块儿放在一旁,这个内存块就是在里的数据帧。这一块内存空间有许多至关重要的信息,比如说:

  1. 为新调用的这个函数的变量提供存储空间。
  2. 当这个函数返回的时候应该返回的line number(行号)
  3. 这个函数所调用的参数。

每一个函数调用都会得到一个自己私有的数据帧。总的来说所有数据帧组成了一个call stack(调用栈)。这里提供一个hello_world-2.c来作为例子:

1   #include <stdio.h>
2   void first_function(void);
3   void second_function(int);
4   
5   int main(void)
6   {
7      printf("hello world\n");
8      first_function();
9      printf("goodbye goodbye\n");
10  
11     return 0;
12  }
13  
14  
15  void first_function(void)
16  {
17     int imidate = 3;
18     char broiled = 'c';
19     void *where_prohibited = NULL;
20  
21     second_function(imidate);
22     imidate = 10;
23  }
24  
25  
26  void second_function(int a)
27  {
28     int b = a;
29  }

当这个程序开始运行就会有一个数据帧,这个数据帧属于主函数main()。因为主函数并没有变量、参数和返回函数,所以这个数据帧没有什么观察价值。下面是主函数还没有调用first_function()的时候的内存分布:

内存分布
main()的数据帧

当程序调用first_function()的时候,未使用的栈空间内存被用来为first_function()创建一个新的数据帧。这个数据帧由四个东西组成,分别为一个int、一个char、一个void*和返回函数main()的所在行号。下面是刚刚调用first_function()时候的内存分布:

内存分布
main()的数据帧
first_function()的数据帧:【int】【char】【void*】【返回函数行号9

同样的,当我们调用second_function()的时候,未使用的栈空间内存被用来为second_function()创建一个新的数据帧。这个数据帧由三个东西组成,分别为:返回函数行号、一个int、一个int参数a。下面是刚刚调用second_function()时候的内存分布:

内存分布
main()的数据帧
first_function()的数据帧:【int】【char】【void*】【返回函数行号9
second_function()的数据帧:【int】【a】【返回函数行号22

second_function()返回时,它的数据帧会查明返回到哪里(first_function()22行),然后返回并且该数据帧出栈。这是刚刚返回了second_function()时候的内存分布:

内存分布
main()的数据帧
first_function()的数据帧:【int】【char】【void*】【返回函数行号9

first_function()返回时,它的数据帧会查明返回到哪里(main()9行),然后返回并且该数据帧出栈。这是刚刚返回了first_function()时候的内存分布:

内存分布
main()的数据帧

练习

  1. 假如一个程序调用了5个函数,那么里面有多少数据帧呢?
  2. 会不会有可能在中间的数据帧返回到了没有使用的内存空间中呢?如果有可能,那么这对于程序来说意味着什么呢?
  3. goto()函数能让一个在中部的数据帧出栈吗?答案是不能,为什么呢?
  4. loogjump()函数能让一个在中部的数据帧出栈吗?
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值