在一个多任务操作系统的每个进程在其自己的沙箱的存储器执行。沙盒是一个虚拟地址空间(virtual address space)。
1 32位虚拟内存布局
在32下部模式虚拟地址空间始终是一个4GB内存地址块。这些虚拟地址的页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用。
每个进程拥有一套属于它自己的页表,可是另一个隐情。
仅仅要虚拟地址被使用,那么它就会作用于这台机器上执行的全部软件。包含内核本身。因此一部分虚拟地址必须保留给内核使用:
图 1
这并不意味着内核使用了那么多的物理内存,仅表示它可支配这么大的地址空间,可依据内核须要。将其映射到物理内存。
内核空间在页表中拥有较高的特权级(ring 2或下面)。因此仅仅要用户态的程序试图訪问这些页,就会导致一个页错误(page fault),用户程序不可訪问内核页。在Linux中,内核空间是持续存在的,而且在全部进程中都映射到相同的物理内存。内核代码和数据总是可寻址的。随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化:
图 2
图2中,蓝色区域表示映射到物理内存的虚拟地址,而白色区域表示未映射的部分。在上面的样例中,Firefox使用了相当多的虚拟地址空间。由于它是传说中的吃内存大户。
地址空间中的各个条带相应于不同的内存段(memory segment),如:堆、栈之类的。记住。这些段仅仅是简单的内存地址范围,与Intel处理器的段没有关系。
1.1 32位经典内存布局
图 3
32位经典内存布局,程序起始1GB地址为内核空间,接下来是向下增长的栈空间和由0×40000000向上增长的mmap地址。而堆地址是从底部開始,去除ELF、代码段、数据段、常量段之后的地址并向上增长。
可是这样的布局有几个问题,首先是easy遭受溢出攻击。其次是,堆地址空间仅仅有不到1G有木有?假设mmap内存比較少地址非常浪费有木有?所以后来就有了还有一种内存布局
1.2 32位默认内存布局
图 4
当计算机开心、安全、可爱、正常的运转时。差点儿每个进程的各个段的起始虚拟地址都与图4全然一致,这也给远程发掘程序安全漏洞打开了方便之门。一个发掘过程往往须要引用绝对内存地址:栈地址。库函数地址等。远程攻击者必须依赖地址空间布局的一致性,摸索着选择这些地址。假设让他们猜个正着,有人就会被整了。因此,地址空间的随机排布方式逐渐流行起来。
Linux通过对栈、内存映射段、堆的起始地址加上随机的偏移量来打乱布局。不幸的是。32位地址空间相当紧凑,给随机化所留下的空当不大,削弱了这样的技巧的效果。
栈
进程地址空间中最顶部的段是栈。大多数编程语言将之用于存储局部变量和函数參数。
调用一个方法或函数会将一个新的栈桢(stack frame)压入栈中。栈桢在函数返回时被清理。或许是由于数据严格的遵从LIFO的顺序,这个简单的设计意味着不必使用复杂的数据结构来追踪栈的内容,仅仅须要一个简单的指针指向栈的顶端就可以。
因此压栈(pushing)和退栈(popping)过程很迅速、准确。另外,持续的重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速訪问。进程中的每个线程都有属于自己的栈。
通过不断向栈中压入的数据,超出其容量就有会耗尽栈所相应的内存区域。这将触发一个页故障(page fault)。并被Linux的expand_stack()处理,它会调用acct_stack_growth()来检查是否还有合适的地方用于栈的增长。假设栈的大小低于RLIMIT_STACK(一般是8MB)。那么普通情况下栈会被加长。程序继续愉快的执行,感觉不到发生了什么事情。这是一种将栈扩展至所需大小的常规机制。然而,假设达到了最大的栈空间大小,就会栈溢出(stack overflow),程序收到一个段错误(Segmentation
Fault)。当映射了的栈区域扩展到所需的大小后。它就不会再收缩回去,即使栈不那么满了。这就好比联邦预算,它总是在增长的。
动态栈增长是唯一一种訪问未映射内存区域(图中白色区域)而被同意的情形。
其他不论什么对未映射内存区域的訪问都会触发页故障,从而导致段错误。一些被映射的区域是仅仅读的,因此企图写这些区域也会导致段错误。
内存映射段
在栈的下方。是我们的内存映射段。此处,内核将文件的内容直接映射到内存。不论什么应用程序都能够通过Linux的mmap()系统调用(实现)或Windows的CreateFileMapping() / MapViewOfFile()请求这样的映射。内存映射是一种方便高效的文件I/O方式。所以它被用于载入动态库。创建一个不正确应于不论什么文件的匿名内存映射也是可能的。此方法用于存放程序的数据。在Linux中。假设你通过malloc()请求一大块内存,C执行库将会创建这样一个匿名映射而不是使用堆内存。
‘大块’意味着比MMAP_THRESHOLD还大,缺省是128KB,能够通过mallopt()调整。
堆
说到堆,它是接下来的一块地址空间。
与栈一样。堆用于执行时内存分配。但不同点是,堆用于存储那些生存期与函数调用无关的数据。
大部分语言都提供了堆管理功能。因此,满足内存请求就成了语言执行时库及内核共同的任务。在C语言中,堆分配的接口是malloc()系列函数,而在具有垃圾收集功能的语言(如C#)中。此接口是newkeyword。
假设堆中有足够的空间来满足内存请求,它就能够被语言执行时库处理而不须要内核參与。否则,堆会被扩大,通过brk()系统调用(实现)来分配请求所需的内存块。堆管理是非常复杂的。须要精细的算法。应付我们程序中杂乱的分配模式,优化速度和内存使用效率。
处理一个堆请求所需的时间会大幅度的变动。实时系统通过特殊目的分配器来解决问题。堆也可能会变得零零碎碎,例如以下图所看到的:
图 5
BSS 数据段 代码段
最后。我们来看看最底部的内存段:BSS,数据段,代码段。在C语言中,BSS和数据段保存的都是静态(全局)变量的内容。差别在于BSS保存的是未被初始化的静态变量内容,它们的值不是直接在程序的源码中设定的。BSS内存区域是匿名的:它不映射到不论什么文件。
假设你写static int cntActiveUsers。则cntActiveUsers的内容就会保存在BSS中。
还有一方面。数据段保存在源码中已经初始化了的静态变量内容。这个内存区域不是匿名的。
它映射了一部分的程序二进制镜像,也就是源码中指定了初始值的静态变量。所以。假设你写static int cntWorkerBees = 10,则cntWorkerBees的内容就保存在数据段中了,并且初始值为10。虽然数据段映射了一个文件,但它是一个私有内存映射,这意味着更改此处的内存不会影响到被映射的文件。也必须如此。否则给全局变量赋值将会修改你硬盘上的二进制镜像,这是不可想象的。
下图中数据段的样例更加复杂,由于它用了一个指针。在此情况下,指针gonzo(4字节内存地址)本身的值保存在数据段中。而它所指向的实际字符串则不在这里。这个字符串保存在代码段中。代码段是仅仅读的,保存了你所有的代码外加零零碎碎的东西,比方字符串字面值。代码段将你的二进制文件也映射到了内存中。但对此区域的写操作都会使你的程序收到段错误。这有助于防范指针错误,尽管不像在C语言编程时就注意防范来得那么有效。下图展示了这些段以及我们样例中的变量:
图 6
你能够通过阅读文件/proc/pid_of_process/maps来检验一个Linux进程中的内存区域。记住一个段可能包括很多区域。
比方,每一个内存映射文件在mmap段中都有属于自己的区域,动态库拥有类似BSS和数据段的额外区域。
下一篇文章讲说明这些“区域”(area)的真正含义。有时人们提到“数据段”,指的就是所有的数据段+ BSS + 堆。
2 64位虚拟内存布局
64位系统的寻址空间比較大,所以仍然沿用了32位的经典布局,可是加上了随机的mmap起始地址,以防止溢出攻击。反正一时半会是用不了这么大的内存地址了。所以至少N多年不会变了。
首先, 眼下大部分的操作系统和应用程序并不须要16EB( 264 )如此巨大的地址空间, 实现64位长的地址仅仅会添加系统的复杂度和地址转换的成本, 带不来不论什么优点. 所以眼下的x86-64架构CPU都遵循AMD的Canonical form, 即仅仅有虚拟地址的最低48位才会在地址转换时被使用, 且不论什么虚拟地址的48位至63位必须与47位一致(sign extension). 也就是说, 总的虚拟地址空间为256TB( 248 ).
图 7
然后, 在这256TB的虚拟内存空间中, 0000000000000000 - 00007fffffffffff(128TB)为用户空间, ffff800000000000 - ffffffffffffffff(128TB)为内核空间. 这里须要注意的是, 内核空间中有非常多空洞, 越过第一个空洞后, ffff880000000000 - ffffc7ffffffffff(64TB)才是直接映射物理内存的区域, 也就是说默认的PAGE_OFFSET为ffff880000000000. 从这里我们也能够看出,
这么大的直接映射区域足够映射全部的物理内存, 所以眼下x86-64它不会在高内存架构存在, 那是,ZONE_HIGHMEM这个地区(参考部分).
版权声明:本文博客原创文章,博客,未经同意,不得转载。