Chinaunix首页 | 论坛 | 博客
  • 博客访问: 353617
  • 博文数量: 83
  • 博客积分: 5322
  • 博客等级: 中校
  • 技术积分: 1057
  • 用 户 组: 普通用户
  • 注册时间: 2010-04-11 11:27
个人简介

爱生活,爱阅读

文章分类

全部博文(83)

文章存档

2015年(1)

2013年(1)

2012年(80)

2011年(1)

分类: LINUX

2012-09-06 20:36:03

剖析程序的内存布局

                                                                                                             by Gustavo Duarte

 

内存管理是操作系统的核心;它对于编程和系统管理都是及其重要的。在接下来的文章中,我将从实践的角度来观察内存,但并没有避开(shy away from)内部细节。由于观点是通用的,文中的例子多数来自于32x86架构的LinuxWindows操作系统。本文的第一部分描述程序的内存布局。

在一个多任务操作系统(multi-tasking OS)中,每一个进程运行在它自己的内存沙盒之中。该沙盒就是虚拟地址空间(virtual address space),在32位模式下,该内存块总是4G字节。这些虚拟地址通过页表(page tables)映射到物理内存,页表则由操作系统核心维护并供处理器参考(consult)。每个进程拥有自身的页表集(Each process has its own set of page tables)。一旦使能了虚拟地址,它将适用于机器中运行的所有软件,包括内核自身。因此,虚拟地址空间的一部分必须预留给内核。

这并不是说内核占用了那么多的物理内存,而是说内核拥有虚拟地址空间中那么大的部分,可以映射到任何期望的物理内存之中。内核空间(kernel space)在页表(page table)中标记为专用的特权代码( privileged code )(2环或更低),因此,一旦用户模式的程序(user-mode program)企图访问该内存,将会触发一个页错误(page fault)。在Linux中,内核空间在所有的进程中,总是存在且被映射到相同的物理内存上。内核代码、内核数据总是可寻址的(addressable,可设定地址的),时刻准备着处理中断与系统调用。相比之下,虚拟地址中的用户空间部分的内存映射,在进程切换过程中总是变化的。

SHAPE \* MERGEFORMAT

蓝色区域代表映射到物理内存的虚拟地址空间,而白色区域没有映射。在上面的例子中,火狐(Firefox)使用了更多的虚拟地址空间,这是由于它及其著名的内存饥饿(memory hunger)导致的。地址空间中的明显的分割相当于内存段(memory segment)中的堆(heap)、栈(stack)等等。记住,这些段仅仅是一个内存地址范围,与英特尔风格的分段(Intel-style segment)毫无关系。但无论如何,它是Linux进程的标准段布局。


当计算机运行的非常“开心”、安全、“可亲”(happy and safe and cuddly)的时候,上图中分段的起始虚拟地址,对于机器上几乎所有的进程而言都是精确相同( exactly the same )的。这使得远程的利用安全漏洞变得很容易。该漏洞利用经常需要引用绝对的内存位置:栈上的地址,库函数的地址等等。远程攻击必须摸索该位置,且依赖于地址空间是不争事实。当他们找到了该地址,也就获得了pwned。于是,地址空间随机化(address space randomization)变得很流行。Linux通过给栈(stack)、内存映射分段(memory mapping segment),以及堆(heap)增加偏移(offset)来实现随机化。不幸的是,32位的地址空间相当的紧凑,这为随机化留下了很小的空间并妨碍了其效率(randomization and )。

在进程地址空间中的最上面的分段是栈(stack),在大多数编程语言中,它用于存储本地变量(local variable)以及函数参数(function parameter)。调用一种方法或一个函数时,会将一个新的栈帧(stack frame)压入到栈顶。该栈帧在函数返回时销毁。这种简单的设计,可能是因为数据严格地遵从先进后出(LIFO)的顺序,也就是说,不需要复杂的数据结构来跟踪栈中的内容,仅仅用一个指向栈顶的指针即可。入栈(PUSH)和出栈(POP)非常快速和确定(Deterministic)。同样的,对栈区域的持续重利用通常是通过将活跃的栈内存保存在cpu缓存中来加速存取。进程中的每一个线程拥有其自己的栈空间。

将大于栈空间的数据压入栈顶时,耗尽(exhaust)映射的栈空间是可能的。这将触发一个页错误,该错误在Linux上通过expand_stack()处理,该函数转而调用来检查是否适合增加栈空间。如果栈大小小于RLIMIT_STACK(通常8兆字节),通常栈空间增大,程序则继续执行而没有察觉所发生的一切。这是通常的机制,即:按照需求调整栈的大小。然而,如果已经达到最大的栈空间大小,我们将会得到一个栈溢出(stack overflow),程序会收到栈错误。栈空间为了满足需求而扩展时,在段不需要那么大的空间的时候并不会缩小到原来的大小。就像联邦预算一样,只会增加。

访问没有映射的内存区域是栈动态增长的唯一情景,如上面的白色区域,这种情况是有效的。任何其他的访问未映射的内存将会触发一个页错误(triggers a page fault),该错误会产生段错误(Segmentation Fault)。一些映射的区域是只读的,因此,对该区域进行写入同样会导致段错误。

在栈的下面是内存映射段(memory mapping segment)。在这里,内核将文件的内容直接映射到内存。任何应用均可以通过Linuxmmap()(Windows中的CreateFileMapping()/MapViewOfFile())系统调用来进行映射。内存映射是一种方便且高效的处理文件I/O的方式,所以它通常用于加载动态库。也可以创建一个匿名的内存映射(anonymous memory mapping ),该映射不与任何文件同步,而用于替代程序的数据。在Linux下,如果你通过malloc()申请一大块内存,C库将创建一个匿名的映射来取代使用堆内存。“Large”意味着比MMAP_THRESHOLD 字节大,默认为128k字节,可通过mallopt()调整。

说到堆(heap),就进入到了地址空间的下一个部分。如同栈(stack),堆(heap)提供了运行时的内存分配;但与栈(stack)不同的是,申请的内存比执行内存申请的函数的寿命更长。大多数的语言为程序提供了堆管理。满足内存请求(satisfying memory request)是介于编程语言运行时与内核之间的共同事物(a joint affair)。在C语言中,获取堆内存的接口是malloc()及其变种,而在垃圾回收语言中,像c#,接口是关键字new

如果堆中有足够的空间来满足内存请求,那么,语言运行时就可以处理而不需要内核参与。否则堆通过系统调用brk()进行扩展来获取请求块。堆管理是复杂的,它需要复杂的、争取快速的、高效的使用内存的算法来面对程序中混乱的分配模式。处理一个堆请求的时间是变化的。实时系统(Real-time system)拥有特殊目的的分配器( )以处理该问题。堆也会变成碎片,如下所示:

SHAPE \* MERGEFORMAT

最后,我们来到了内存的最低分段:BSS,数据,代码段。代码段和BSS区存储C语言中的静态、全局变量。不同之处是:BSS保存了未初始化的静态变量,其值没有在源代码中由程序员设置。BSS内存区域是匿名的:它不映射任何文件。如果你声明了一个静态整形变量static int cntWorkerBees,那么该变量就位于BSS段中。

另一方面。数据段(data segment)则保存了源代码中初始化过的静态变量。该内存区域不是匿名的(is not anonymous)。它映射了程序中的二进制映像,该镜像包含了在源代码中初始化后的静态变量值。如果你声明了静态变量: = 10cntWorkerBees就存储在数据段中并初始化为10。尽管数据段映射了文件,它是一个私有的内存映射(private memory mapping),这意味着对内存的更新不会映像到该内存映射的文件。这必须为事实,否则对全局变量的赋值将会改变你的硬盘上的二进制映像。这是不可思议的!

在下面示意图中的数据是复杂的,因为使用了一个指针。这样,gonzo指针的内容-----一个4字节的内存地址-存储在数据段(data segment)中。然而,它指向的字符串并不是存储在数据段。而是存储在文本段(代码段text segment),该段是只读的,并且存储了所有的代码以及各种字符串。文本段(text segment)将二进制文件映射进内存,但是向该区域写入会产生段错误(segmentation fault)。这将防止指针缺陷,尽管没有一开始就避免使用C语言同样有效。下面是一个示意图,该图展示了这些段以及我们例子中的变量:


你可以通过读取文件/proc/pid_of_process/maps来检查Linux进程的内存区域。注意:一个段可能包含很多区域。例如,每个映射了文件的内存通常都在mmap段中有其自身的区域,并且动态库拥有与bss及数据段相似的额外区域。下面的内容将澄清“区域()area”到底是什么。同样的,有时人们把数据段(data segment)叫做“data+bss+heap

你可以使用nm以及objdump命令来显示二进制镜像的符号、地址、分段等等。最后,上述虚拟地址空间的布局是Linux下的“灵活的”布局,这已经默认了一些年了。它假定我们有RLIMIT_STACK的值。当不是这种情况时,Linux将退回到下面显示的经典的(classic)布局:


这就是虚拟地址空间的布局。下一节将讨论内核如何跟踪这些内存区域。一开始,我们将查看内存映射,如何读取与写入大量文件,以及内存使用数据的意义。







阅读(1692) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~