种子班—SeedClass

我选择,我担当

【译】程序的内存分布

原文地址请见http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory,有改动,转载注明出处 。译者邮箱: chn.edward@gmail.com,同班同学看完请随便回复点啥

内存管理是操作系统的核心部分,对于编程和系统管理都是至关重要的。接下来的几篇文章中我会关注实际应用方面,但同时也不会回避其底层的实现机制。所讲述的概念都会是想通的,例子主要来自于Linux和Windows (32-bit x86)。第一篇描述程序如何在内存中分布。

多核系统中的每一个进程都在自己的内存沙盒中运行(memory sandbox),这个沙盒就是虚拟地址空间(virtual address space),在32-bit模式下,虚拟地址空间都是一个4GB大小的内存地址块。通过页面表(page table),这些虚拟地址被映射到物理地址。页面表由内核维护,被处理器访问。每个进程都有自己的一套页面表。一旦使能虚拟地址,包括内核在内的所有软件。因此,必须保留一部分的虚拟内存地址给内核。

这并不意味着内核需要使用那么多的物理内存,而是内核拥有那么多份额的地址空间,内核空间在页面表中独享特权指令权(ring 2甚至更低),因此当用户态程序想要访问内核空间内存时,就会触发页错误(page fault)。在Linux系统中,内核空间静态存在,在所有的进程中被映射到相同的物理区域。内核代码和数据总是可以被分配地址的,时刻准备着处理中断和系统调用。然而,随着进程的切换切换,用户态的内存映射总是在变化。

蓝色的区域代表被映射到物理内存的虚拟地址,白色的区域则是没有被映射的。在上图的例子中,Firefox使用了大量的内存,不同的区带对应不同内存段,如堆(heap),栈(stack)。需要注意的是这些内存地址范围和Intel处理器的分段没有关系。下图显示的是Linux的标准分段方法:

当计算机正常运行时,各个段的起始虚拟地址,对于每个进程都是完全一样的。这使得从远程攻破安全漏洞更加容易,攻击通常会要读取绝对的内存地址:栈上的地址,库函数的地址,或是其他。远程的攻击者往往武断地选择一个地址。如果虚拟地址分布的情况都是一模一样的,那这个选择就会恰好正确,攻击也就会成功。因此地址空间随机化就变得热门起来。Linux通过起始地址偏移随机化栈、内存段分配、和堆的空间。不幸的是32-bit的内存地址十分紧凑,随机化的空间很小。

进程空间中的最高部分段是栈空间,存储大部分编程语言中的变量和函数参数。调用一个方法或者函数的时候,就会有一个栈帧(stack frame)入栈,函数返回时该栈帧就会被销毁。可能是因为数据需要遵守严格的LIFO顺序(Last In First Out),这种简单的设计意味着跟踪栈内容不需要任何复杂的数据结构:一个简单的栈指针就足够了。入栈和出栈速度很快并且确定性很强,另外CPU倾向于将经常使用的变量存储在寄存器中,将活跃栈值存储在CPU寄存器中,就可以加速访问。进程中的每一条线程都会有自己的栈空间。

在栈中装入过多的数据可能会使映射给栈的地址枯竭,这时就会触发页错误,Linux下这个页错误会由expand_stack来处理,expand_stack随后调用acct_stack_growth()来检查扩栈举动是否合适。如果栈的大小小于RLIMIT_STACK(通常是8MB),栈通常情况下都会增长,程序继续平稳的运行,对它而言似乎什么都没发生过。这是栈获取其需要大小的通常办法。然而,一旦栈的大小达到最大值,就会造成栈溢出(stack overflow),这时程序会收到一个段错误(Segmentation Fault)。栈通过扩容满足需要,但在需求下降时却不会缩减尺寸,这就像我们的联邦预算一样,只增不减。

只有在动态栈增长的情况下,访问上图的白色区域才是合法的。任何其他的访问都会触发页错误,最后带来段错误。一些区域是只读的,因此向这些区域的写操作也会带来段错误。 栈空间之下,就是内存映射段(memory mapping segment),在这里内核将文件的内容直接映射到内存。任何应用程序都可以通过Linux系统调用mmap() (实现)或者是Windows下的CreateFileMapping()MapViewOfFile(),内存映射大大增加文件I/O速度,因此被使用在动态链接库的读取中。另外还可以创建一个匿名内存映射(anonymous memory mapping),该映射不对应任何文件,而是对应程序数据。在Linux中,如果你调用malloc()申请一块很大的内存,C库就会创建这样一个匿名内存映射,而不是使用堆内存。这里的‘大’,意味着大于MMAP_THRESHOLD个字节,默认值为128kB并且可以通过mallopt()更改。

再来看(Heap),堆提供了运行时内存分配的空间,在这一点上和栈是相似的。但在另一点上,函数在堆中的分配的空间即使在函数退出时仍然存在,这和栈则是不同的。大部分的编程语言具有堆操作的功能,处理内存请求需要编程语言运行库(language runtime)和内核的共同作用。在C语言中,操作堆的接口是malloc()以及其他类似函数,而在一些有垃圾回收功能的语言,例如C#,接口则是new关键字。

如果堆中的内存空间足够满足内存请求,编程语言就可以处理一切,内核不需要干预。否则,堆空间需要通过系统调用brk() (实现)来为内存请求分配空间。堆管理很复杂,需要有精巧的算法来同时满足速度要求和内存使用效率要求。处理一个堆内存请求所需要的时间是不确定的,实时系统(Real-time system)通过特殊功能分配器(special-purpose allocators)来解决实时性的问题,堆空间变为片状分布,如下图所示。

最后,我们来到内存中最低的一段:BSS, Data以及Text segment段。BSS和Data都是用来存储C语言中的static(包括global)变量的。不同的是BSS存储未初始化的程序变量,BSS内存空间是匿名的,它不映射任何文件,如果在C语言里你写了static int cntActiveUsers,那么这个cntActiveUsers变量就会被放在BSS空间中。

Data空间则存储被代码初始化的static变量,并且该空间不是匿名的。它把程序二进制映像(binary image)中给static变量赋值中值的那一部分映射到了内存。所以,如果你写了static int cntWorkerBees = 10,cntWorkerBees的值就被存储在Data空间,并且初始值是10。尽管Data空间映射文件,这种映射却是私有内存映射(private memory mapping),私有内存映射意味着对内存的改变不会影响到被映射的文件。因此,对static变量的改变不会影响二进制影响本身,当然也不该影响!

下图的例子有些绕人,因为涉及指针。在图中,指针gonzo的值 – 4字节的内存地址 – 位于data段,而它指向的字符串则位于Text段,该段只读,除了代码指令之外还有字面字符串。该段在内存中映射二进制文件,但向该段写入则会导致段错误。这可以避免指针错误(但最好还是在C语言中自己避免)。下图显示的是上述的三个段,以及我们例子中的变量:

在Linux中,我们可以访问/proc/pid_of_process/maps来检查一个进程的内存区域情况。记住一个段可能包含有多个区域。 你可以利用nmobjdump命令来二进制映像的符号、地址、段,以及其他。最后,上述的虚拟内存分布在Linux中是”可变的”,这种分布方式作为默认分布方式已经有好几年了。它默认RLIMIT_STACK有一个值,否则,Linux就会退回到下图所示的经典分配方式:

 

posted by chnedward in tech and have Comments (5)

5 comments

  1. 评论 by tankery on 2011/03/22 at 下午 3:49

    我貌似经常看到段错误。。。哎,可惜没记录,忘记是什么情况下看到的了。。。。

  2. 评论 by Crazyman on 2011/03/22 at 下午 3:54

    昨天解析那个进程属性文件时就碰到了,原因是二维数组定义用了<code>char * xxx[]</code>;最后改成<code>char xxx[][]</code>就可以了。

  3. 评论 by yiran on 2011/03/22 at 下午 7:33

    宝光辛苦了……原文所在的网站貌似很给力啊!

  4. 评论 by chnedward on 2011/03/23 at 上午 9:41

    没错,文章写的很不错

  5. 评论 by 唐小叶 on 2011/03/25 at 上午 12:20

    大部分segment fault是由访问非法内存引起的吧?
    比如new一个数组,然后使用delete 而非 delete [],然后程序运行就会报出那个错误。而像这种错误出错的地方往往不是真正错的地方。

发表评论

电子邮件地址不会被公开。 必填项已被标记为 *

*

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Protected by WP Anti Spam