内存管理是操作系统的核心;它对于程序员和系统管理员都很关键。在接下来的几篇文章里面我将对内存的关键技术做谈论,但是不会远离其本质。然而概念很普通,例子多半来自32位X86系统的LINUX和Window操作系统。这第一篇文章谈论程序在内存中如何存放。
在多任务操作系统中的每一个进程运行在他自己的内存地址空间中。这个地址空间就是虚拟地址空间,虚拟地址空间在32位模式下总是4GB大小的内存地址。这些虚拟地址用页表方式映射物理内存,页表由操作系统内核维护,由处理器访问。每个进程有自己的页表集合,但这里有个难以理解的地方。一旦虚拟地址(作者的意思也就是分页机制)开启,它应用与所有正在运行与机器上的软件,包括内核自身。这样一部分虚拟地址空间必须保留用于内核:
这并不意味着内核使用那么多物理内存,只是他运用那部分可用的地址空间去映射他实际希望的物理内存大小。内核空间在页表中标志为特权级代码(等级小于等于2),这样当用户模式程序访问他时就会触发一个访页错误。在LINUX中,内核空间一直处于当前状态并且在所有进程中映射到相同的物理内存。内核代码和数据在任何时候总是为中断服务和系统调用做好寻址的准备。相反,用于映射用户模式的地址空间部分将会在进程切换的时候发生改变。
蓝色区域代表已经映射物理内存的虚拟地址,白色区域为没映射部分。在上面的例子中,Firefox由于他的巨大的内存需求,已经使用了他的大部分虚拟地址空间。地址空间中不同的带对应内存段如堆、栈等等。需要注意的是这些段就是简单的内存地址范围,他和Intel汇编中的”段”不相干。下面是在LINUX进程中标准的段视图:
当计算安全时,如上面的段所示,对于机器中的几乎每一个进程的开始虚拟地址都相同。这使得很容易远程利用安全漏洞。一个漏洞,往往需要引用绝对内存位置:栈上的地址,库函数的地址,等等。远程攻击必须盲目地选择这个内存位置,正指望这所有的地址空间都是一样的。如果真是这样,那么太容易被攻击了。故而地址空间的随机化就变得通用了。LINUX以在栈、内存映射段和堆的起始地址加上偏移的方式随机化他们。不幸的是,32位地址空间很紧缺,留下很少的空间用来做随机化从而牵制了他的有效性。
进程地址空间中最上面的段为栈,很多语言中栈用于存储本地变量和函数参数。调用一个方法或函数时压入栈一个新的栈帧。当函数返回时,这个压入的栈帧被释放。这个简单的设个,可能是因为数据遵循严格的FIFO次序,这意味着再复杂的数据结构都无需跟踪栈内容——一个简单的栈顶指针将会做跟踪作用。这样入栈和出栈非常快速和准确。进一步,堆栈地区不断重用,往往在CPU缓存中持有活跃的栈内存,加快存取。进程中的每个线程获得他自己的栈。