Anatomy of a Program in Memoryhtml
在一个多任务OS中,每一个进程都运行在它本身的内存沙箱中。这个沙箱就是虚拟地址空间,在32位下就是一块容量为4GB的内存地址。内核将这些虚拟地址按页表(page table)映射为物理内存,并交由CPU访问。每一个进程有本身的页表集,但有一点要注意。虚拟地址一旦被启用,就会应用到机器上全部运行的程序上,也包括内核本身。所以虚拟地址空间必须为内核预留一部分(不然就没办法和内核交互了):
linux
给内核预留那么多空间,并非说内核真的使用了那么多物理内存,只是内核能够这部分虚拟地址自由的映射到某片物理内存上(而不用和其它虚拟地址遵循相同的规则)。内核空间在页表中被标记为专属的特权代码(ring 2或更低),用户态的程序访问它时就会产生页错误(page fault)。Linux的内核空间在全部进程中都映射到相同的物理内存上。内核代码和数据老是可寻址的(老是能够找到的),任意时刻均可处理中断和系统调用。做为对比,当进程切换时,用户态地址空间的映射就会发生改变:
算法
蓝色区域表示映射到物理内存的虚拟地址空间,而白色区域则表示未映射的空间。在这个例子中,Firefox惊人的内存需求让它使用的虚拟地址远远超过了其自身的地址空间。内存地址空间是按堆、栈这样的内存段进行管理的。要记住内存段就是简单的一个内存地址范围,并且与Intel风格的段没有任何关系。下面是一个Linux进程的标准内存段布局:
编程
若是计算过程轻松愉快、准确无误,那么上图显示的内存段起始虚拟地址在几乎每一个进程中都是同样的。这致使了远程利用安全漏洞变得很是容易。一次漏洞探测一般须要引用内存的绝对地址:一个栈上地址,一个库函数的地址,等等。远程攻击者只能盲目的选择这样的地址,期望地址空间都是同样的。若是这种状况真发生了,用户就悲剧了。所以地址空间随机化变的很流行。Linux会给栈、mmap段、和堆的起始地址一个随机偏离。不幸的是,32位地址空间实在是太紧凑了,只给随机化很小的空间,妨碍了它的效果。ubuntu
进程地址空间的最上面是栈,栈里保存了局部变量,以及大多数编程语言中的函数参数。一次方法或函数调用就会向栈增长一个栈帧(stack frame)。当函数返回时栈帧就会被销毁。由于数据遵循严格的“后入先出”顺序,这种简单的设计意味着不须要复杂的数据结构就能追踪到栈的上下文——栈顶的一个指针就搞定。栈的Push和pop操做所以变得快速且肯定。同时,栈区域的稳定重用也有助于栈内存在CPU缓存中保持活跃,加快了访问速度。进程中的每一个线程都有本身的栈。c#
若是推入栈的数据过多,可能会耗尽栈映射的地址区域。这会致使一次页错误,Linux将其处理为一次expand_stack()调用,其实是调用acct_stack_growth()检查当前是否能够增长栈大小。若是栈大小小于RLIMIT_STACK
(一般8MB)就能够继续增加,程序会正常继续,不会察觉到什么。这是栈大小调整的默认处理。可是,若是栈大小达到了上限,就会发生栈溢出,程序会接到一次段错误(Segmentation Fault)。相对的,当栈变小时,不会缩减栈大小。这就像联邦预算,只增不减(笑)。缓存
在访问到未映射内存区(上图中的白色部分)时,只有动态栈增加多是合法的。其它方式访问到未映射区域时都会引起一次页错误,进而致使段错误。一些映射区是只读的,对它们的写操做也会致使段错误。安全
在栈的下面就是mmap段,内核在这里将文件内容映射为内存。任何应用均可以经过Linux下的mmap()或Windows下的CreateFileMapping()/MapViewOfFile()申请一片mmap区域。mmap是一种高效便捷的文件I/O方式,被用于加载动态连接库。咱们一样能够建立一块与文件没有关系的mmap区,用来存放程序的数据。在Linux中,若是你经过malloc()申请一大块内存,glibc会返回一块匿名的mmap内存块,而不是用堆内存。“大”意思是比MMAP_THRESHOLD大,一般是128KB,能够调用mallopt()修改。数据结构
接下来咱们开始说堆。堆提供了运行时的内存分配,这点和栈相似;数据的生命期与执行分配的函数生命期不一致,这点与栈不一样。大多数语言都提供了堆的管理。所以知足内存请求须要语言的运行环境和内核协做完成。C语言中分配堆的接口是malloc()族函数,而在有gc的语言(例如C#)其接口则是关键字new。app
若是堆内存足够,语言运行时环境就能够处理内存请求,不须要内核介入,或者能够经过brk()系统调用去增大堆内存。堆的管理很复杂,在面对程序杂乱的分配方式时,须要使用在速度和内存使用率上努力作平衡的微妙算法。知足一次堆请求的时间差别能够很是大。实时系统须要特殊用途的分配器来处理这个问题。使用时堆也会变的很碎片化,见下图:
最后,咱们看一下最下面的内存段:BSS、data、代码段。在C里面BSS和data段都是存储静态变量(全局)的数据。区别是BSS段存储没有初始化的静态变量,即在代码中没有初始值的静态变量。BSS区是匿名的:不映射自任何文件。若是代码中有static int cntActiveUsers;
,那么cntActiveUsers就在BSS段。
而data段则存放代码中显示初始化了的静态变量。这块内存区不是匿名的,它映射自程序镜像中包含对应静态变量的文件。这种映射是私有映射,即内存中的变化不会反映到文件中。若是不这样,改变一个全局变量的值就会修改你磁盘中的镜像文件,这就太荒谬了!
下图中的data示例有些取巧,使用了一个指针。指针gonzo的内容——4字节的地址——就存在data段。而它指向的字符串则不是,字符串存放在text段。text段是只读的,存放字符串字面值等不会执行的代码。text段也会映射程序文件,但对它的写操做会引起段错误。这会避免一些指针bug,但不像第一时间在C代码中避免指针bug那么有效。下图显示了这些段和示例变量:
你能够读/proc/pid_of_process/maps
来检查一个Linux进程的内存区。一个内存段可能包含多个内存区。例如,每一个mmap映射的文件都会在mmap段有一个单独的内存区,而动态库还会有在BSS和data段的内存区。下篇文章会剖析“内存区”意味着什么。有时人们也会用“data段”代指整个data + BSS + 堆。
你能够用nm和objdump命令检查镜像文件中的符号、它们的地址、所属的段,等等。最后,上面说的虚拟地址布局是Linux的“灵活”布局,也是近年来Linux的默认布局。它假设RLIMIT_STACK
有值。不然Linux会回到下图的“经典”布局: