编写程序时,对内存的维护非常重要,所有的程序都依赖于其操作的内存,可以说快速高效的程序和糟糕出错的程序之间的差距可能主要就在于是否进行正确的内存管理。
类似于其他主流操作系统,ios上也提供了两种类型的内存分配:一种是基于栈的内存分配,另一种是基于堆的内存分配。基于栈的内存分配通常由编译器处理,因为栈中填充的通常都是程序的自动变量;而动态内存分配一般都在堆上进行。
在最开始的几节还提到过,对于每一个进程来说,其独自享有一个私有的地址空间,这个地址空间可以通过LC_SEGMENT命令填充可执行文件以及各种库的代码。整个地址空间分为保护段、代码段、dyld段、数据段等。不同进程的内存地址空间相互独立,互不影响。
顺便相关的还有ASLR技术:地址空间布局随机化。进程在自己的私有的虚拟地址空间中启动,按照传统的方式,进程每一次启动都采用固定的可预见的方式。然后这也意味着某个给定程序再某个给定架构上的进程初始虚拟内存镜像都是基本一致的。更加严重的问题是,即使是在进程正常运行的生命周期中,大部分内存分配的操作都是按照同样的方式进行的,因此使得内存中的地址分布具有非常强的可预测性。这就给黑客提供了很大的施展空间。黑客可以可靠的判断要重写那些指针,判断注入代码应该在内存的什么位置,从而通过缓存区溢出等技术,重写内存中的函数指针,将程序的执行路径转到自己的代码。ASLR是一种避免类似攻击的有效保护。进程每一次启动时,地址空间都会被简单地随机化:进行整体的地址偏移,而不是搅乱。通过内核将整个进程的内存空间“平移”某个随机数,进程的基本内存布局如程序文本、数据、库等相对位置仍然是一样的,但其具体的地址都不同了,可以比较有效的阻挡黑客对地址的猜测。
上面提到的堆和栈、地址空间和ASLR等技术,背后都隐含着一个概念:虚拟内存。基本所有的现代操作系统都有虚拟内存的概念,相对的就有物理内存的概念。物理内存就是实打实的存放数据的硬件,虚拟内存是在物理内存上的一层与具体硬件无关的抽象。显而易见,用户态不可能也不应该和物理内存层打交道。
Mach的虚拟内存子系统可以说和其要管理的虚拟内存一样复杂和充满了各种细节,下面简单介绍一下物理内存层和虚拟内存层这两个层次。
尽管我们写程序时不关注物理内存层,但任何内存地址最终还是要翻译为物理地址。机器的RAM实际上是虚拟内存中开的窗口,允许程序访问虚拟内存中有限的而且通常是不连续的区域。对Mach来说,物理内存层最重要的一个概念为pmap。pmap从设计上对物理内存提供了一个统一的接口,屏蔽了架构相关的区别,这对于支持像X86架构或者ARM架构非常重要。pmap在逻辑上由两个子层构成:一是机器无关层,提供了一组基本上和机器无关的API。这些API只要求机器支持基本的虚拟内存分页概念。二是机器相关层,将pmap绑定到一个具体的实现,处理底层架构的各种细节。这一层主要是各种和硬件相关的宏定义,如页表项宏、位掩码、寄存器等等。从面向对象的角度看,机器无关层类似pmap提供的接口,而机器相关层则是pmap的实现。只要接口不变,那么上层逻辑可以完全不用考虑具体的实现细节。因此,pmap的实现细节对于其上层的系统来说是透明的,这样可以最大化实现可移植性,缺点就是损失一部分性能。
虚拟内存层完全以一种机器无关的方式来管理内存,这一层有几种关键的抽象:一是vm_map,表示任务地址空间的一个或多个虚拟内存区域。每一个区域都由一个独立的条目vm_map_entry表示,这些条目由一个双向链表vm_map_links维护。二是 vm_map_entry,这是关键的数据结构,每一个该结构都表示了虚拟内存中的一块连续的区域,每一个这样的区域都可以通过指定的访问保护权限进行保护。任务之间可以共享区域。它通常指向一个vm_object,也可以指向一个嵌套的vm_map,即子映射。三是vm_object,用于将vm_map_entry和实际支撑的内存关联起来。这个数据结构包含一个vm_page的链表,还包含一个用于访问正确分页器的Mach端口,通过这个分页器进行页面的获取或清理操作。四是 vm_page,真正表示完整的或部分的vm_object,并含有各种页状态。这些数据结构加上其状态管理,再加上换页算法,基本就是虚拟内存系统的主要内容。这些实现非常复杂,没法在这里展开了。
总之,我们平常所说的各种内存概念基本都是在用户态的针对虚拟内存层的。操作系统帮我们屏蔽了复杂的硬件实现。