Protection and Examples of Virtual Memory
并发运行的多个程序共享一台计算机的多道程序的发明导致了对程序间保护和共享的新需求。 这些需求与当今计算机中的虚拟内存密切相关,因此我们将在此处连同两个虚拟内存示例一起讨论该主题。
多道程序设计引出了进程的概念。 比喻地,进程是程序的呼吸空气和生存空间——也就是说,一个正在运行的程序加上继续运行它所需的任何状态。 分时是多道程序设计的一种变体,它与多个交互式用户共享处理器和内存。同时,给人一种所有用户都有自己的电脑的错觉。 因此,在任何时刻都必须可以从一个进程切换到另一个进程。 这种交换称为进程切换或上下文切换。
一个进程无论是从头到尾连续执行,还是被重复中断并与其他进程切换,都必须正确运行。 维护正确进程行为的责任由计算机和操作系统的设计者共同承担。 计算机设计者必须确保进程状态的处理器部分可以被保存和恢复。 操作系统设计者必须保证进程不会干扰彼此的计算。
保护一个过程的最安全方法是从另一个过程中保护当前信息将当前信息复制到磁盘。但是,对于时间共享环境,将需要几秒钟的时间。
这个问题是通过操作系统对主内存进行分区来解决的,以便几个不同的进程同时在内存中拥有它们的状态。 这种划分意味着操作系统设计者需要计算机设计者的帮助来提供保护,以便一个进程不能修改另一个进程。 除了保护之外,计算机还提供进程之间的代码和数据共享,以允许进程之间通信或通过减少相同信息的副本数量来节省内存。
Protecting Processes
进程可以通过拥有自己的页表来相互保护,每个页表指向不同的内存页。 显然,必须防止用户程序修改它们的页表,否则保护将被绕过。
保护可以升级,这取决于计算机设计者或购买者的担忧。 添加到处理器保护结构中的环将内存访问保护从两个级别(用户和内核)扩展到更多级别。 就像最高机密、机密、机密和非机密的军事分类系统一样,安全级别的同心环允许最受信任的人访问任何东西,第二个最受信任的人可以访问除最内层之外的一切,依此类推。 “平民”程序最不可信,因此访问范围也最有限。 对于哪些内存片段可以包含代码——执行保护——甚至级别之间的入口点,也可能存在限制。 使用环的 Intel 80x86 保护结构将在本节后面介绍。 目前尚不清楚环是否是对用户模式和内核模式的简单系统的实践改进。
随着设计师的担忧升级为恐惧,这些简单的限制可能还不够。 限制内部密室中给定程序的自由需要一个新的分类系统。 这个系统的类比不是军事模型,而是钥匙和锁:除非有钥匙,否则程序无法解锁对数据的访问。为了使这些密钥或功能有用,硬件和操作系统必须能够明确地将它们从一个程序传递到另一个程序,而不允许程序本身伪造它们。 如果要缩短检查密钥的时间,则此类检查需要大量硬件支持。
多年来,80x86 架构已经尝试了其中的几种替代方案。由于向后兼容性是该体系结构的指导方针之一,该体系结构的最新版本包括其在虚拟内存中的所有实验。 我们将在这里讨论两个选项:首先是较旧的分段地址空间,然后是较新的平面 64 位地址空间。
A Segmented Virtual Memory Example: Protection in the Intel Pentium
最初的 8086 使用段来寻址,但它没有提供虚拟内存或保护。 段有基址寄存器,但没有绑定寄存器,也没有访问检查,并且在可以加载段寄存器之前,相应的段必须在物理内存中。 英特尔对虚拟内存和保护的奉献在 8086 的后继产品中显而易见,扩展了一些字段以支持更大的地址。 这个保护方案是精心设计的,许多细节都经过精心设计,试图避免安全漏洞。 我们将其称为 IA-32。接下来的几页重点介绍了一些英特尔保护措施; 如果你觉得阅读困难,想象一下实施它们的难度!
第一个增强是将传统的两级保护模型加倍:IA-32 有四级保护。 最内层 (0) 对应于传统的内核模式,最外层(3)是最低特权模式。IA-32 的每个级别都有单独的堆栈,以避免级别之间的安全漏洞。 还有类似于传统页表的数据结构,其中包含段的物理地址,以及要对转换地址进行的检查列表。
英特尔设计师并没有就此止步。 IA-32 划分地址空间,允许操作系统和用户访问整个空间。 IA-32 用户可以在此空间中调用操作系统例程,甚至可以将参数传递给它,同时保持完全保护。 这个安全调用不是一个微不足道的动作,因为操作系统的堆栈与用户的堆栈不同。 此外,IA-32 允许操作系统为传递给它的参数维护被调用例程的保护级别。 通过不允许用户进程要求操作系统间接访问它本身无法访问的内容,可以防止这种潜在的保护漏洞。 (此类安全漏洞称为特洛伊木马。)
英特尔设计人员遵循尽可能少信任操作系统的原则,同时支持共享和保护。 作为使用这种受保护共享的一个示例,假设工资单程序编写支票并更新有关总工资和福利支付的年初至今信息。 因此,我们希望让程序能够读取工资和年初至今信息并修改年初至今信息而不是工资。 我们很快就会看到支持这些功能的机制。 在本小节的其余部分,我们将着眼于 IA-32 保护的大局并检查其动机。
Adding Bounds Checking and Memory Mapping
增强英特尔处理器的第一步是获得分段寻址以检查边界并提供基础。 IA-32 中的段寄存器包含一个指向称为描述符表的虚拟内存数据结构的索引,而不是基地址。 描述符表扮演传统页表的角色。在 IA-32 上,页表条目的等价物是段描述符。 它包含在 PTE 中找到的字段:
■ 当前位——相当于 PTE 有效位,用于指示这是一个有效的转换
■ Base field——相当于一个页框地址,包含段的第一个字节的物理地址
■ 访问位——类似于某些架构中的参考位或使用位,有助于替换算法
■ 属性字段—指定使用此段的操作的有效操作和保护级别
还有一个限制字段,在分页系统中找不到,它建立了该段的有效偏移量的上限。 图 B.26 显示了 IA-32 段描述符的例子。除了这种分段寻址之外,IA-32 还提供了一个可选的寻呼系统。 32 位地址的上半部分选择段描述符,中间部分是描述符选择的页表的索引。 下节介绍不依赖分页的保护系统。
我们现在可以展示如何调用这里提到的工资程序来更新年初至今的信息,而不允许它更新工资。 可以为程序提供一个描述符来描述可写字段清除的信息,这意味着它可以读取但不能写入数据。 然后可以提供一个仅写入年初至今信息的可信程序。 它被赋予一个具有可写字段集的描述符(图 B.26)。 工资单程序使用具有一致字段集的代码段描述符调用可信代码。 此设置意味着被调用程序采用被调用代码的权限级别,而不是调用者的权限级别。 因此,工资程序可以读取工资并调用受信任的程序来更新年初至今的总数,但工资程序不能修改工资。 如果该系统中存在特洛伊木马,那么它必须位于可信代码中才能有效,其唯一工作是更新年初至今信息。 这种保护方式的论点是限制漏洞的范围可以增强安全性。
Adding Safe Calls from User to OS Gates and Inheriting Protection Level for Parameters
允许用户进入操作系统是一个大胆的步骤。 那么,硬件设计人员如何在不信任操作系统或任何其他代码的情况下增加安全系统的机会? IA-32 方法是限制用户可以输入一段代码的位置,将参数安全地放置在适当的堆栈上,并确保用户参数不会获得被调用代码的保护级别。
为了限制进入其他人的代码,IA-32 提供了一个特殊的段描述符或调用门,由属性字段中的一个位标识。 与其他描述符不同,调用门是内存中对象的完整物理地址; 处理器提供的偏移被忽略。 如前所述,它们的目的是防止用户随意跳转到受保护或更特权的代码段。 在我们的编程示例中,这意味着工资单程序可以调用可信代码的唯一位置是在正确的边界处。 需要此限制才能使符合要求的段按预期工作。
如果调用方和被调用方“相互怀疑”,以至于双方都不信任对方,会发生什么? 解决方法是在图 B.26 底部描述符的字数字段中找到。当调用指令调用调用门描述符时,描述符将描述符中指定的字数从本地堆栈复制到对应于调用门描述符的堆栈中。 本段的水平。 这种复制允许用户通过首先将参数推送到本地堆栈来传递参数。 然后硬件将它们安全地传输到正确的堆栈上。 从调用门返回会将参数从两个堆栈中弹出并将任何返回值复制到正确的堆栈中。 请注意,此模型与当前在寄存器中传递参数的做法不兼容。
这种方案仍然存在让操作系统使用用户地址作为参数传递的潜在漏洞,操作系统的安全级别,而不是用户的级别。 IA-32 通过以下方式解决了这个问题,每个处理器段寄存器中的 2 位专用于请求的保护级别。 当操作系统例程被调用时,它可以执行一条指令,在所有地址参数中设置这个 2 位字段,并具有调用该例程的用户的保护级别。 因此,当这些地址参数加载到段寄存器中时,它们会将请求的保护级别设置为适当的值。 然后 IA-32 硬件使用请求的保护级别来防止任何愚蠢行为:如果它具有比请求的特权级别更高的保护级别,则不能使用这些参数从系统例程访问任何段。
A Paged Virtual Memory Example: The 64-Bit Opteron Memory Management
AMD 工程师发现上一节中描述的精心保护模型的用途很少。 流行的模型是由 80386 引入的扁平 32 位地址空间,它将段寄存器的所有基值设置为零。 因此,AMD 在 64 位模式下省去了多个段。 它假定段基数为零并忽略限制字段。 页面大小为 4 KiB、2 MiB 和 4 MiB。
AMD64 架构的 64 位虚拟地址映射到 52 位物理地址,尽管实现可以实现更少的位以简化硬件。 例如,Opteron 使用 48 位虚拟地址和 40 位物理地址。 AMD64 要求虚拟地址的高 16 位只是低 48 位的符号扩展,它称之为规范形式。
64 位地址空间的页表大小令人震惊。 因此,AMD64 使用多级分层页表来映射地址空间以保持大小合理。 级别数取决于虚拟地址空间的大小。图 B.27 显示了 Opteron 的 48 位虚拟地址的四级转换。
每个页表的偏移量来自四个 9 位字段。 地址转换首先将第一个偏移量添加到页映射级别 4 基址寄存器,然后从此位置读取内存以获取下一级页表的基址。 下一个地址偏移量依次添加到这个新获取的地址上,并再次访问内存以确定第三个页表的基址。 它以同样的方式再次发生。 最后一个地址字段被添加到这个最终基地址,并使用这个总和读取内存以(最终)获得被引用页面的物理地址。 该地址与 12 位页面偏移量连接以获得完整的物理地址。 请注意,Opteron 架构中的页表适合单个 4 KiB 页面。
Opteron 在这些页表中的每一个中使用 64 位条目。 前 12 位保留供将来使用,接下来的 52 位包含物理页帧号,最后 12 位提供保护和使用信息。 尽管页表级别之间的字段有所不同,但以下是基本字段:
■ Presence - 表示该页面存在于内存中。
■ 读/写—说明页面是只读还是读写。
■ 用户/主管——说明用户是否可以访问该页面,或者是否仅限于上三个权限级别。
■ 脏-表示页面是否已被修改。
■ 已访问— 说明自该位上次清除后页面是否已被读取或写入。
■ 页面大小——说明最后一个级别是用于 4 KiB 页面还是 4 MiB 页面; 如果是后者,那么皓龙只使用三层而不是四层页面。
■ 不执行—在80386 保护方案中未找到,添加此位是为了防止代码在某些页面中执行。
■ 页面级缓存禁用—说明页面是否可以被缓存。
■ 页级直写— 说明页面是否允许数据缓存回写或直写。
因为在 TLB 未命中时 Opteron 通常会通过四个级别的表,所以有三个潜在的地方可以检查保护限制。 Opteron 只服从底层 PTE,检查其他的只是为了确保有效位被设置。
由于条目长 8 个字节,每个页表有 512 个条目,而 Opteron 有 4 KiB 页,页表正好是一页长。 四个级别字段中的每一个都是 9 位长,页偏移量是 12 位。 此推导留下 64%(4$9 + 12) 或 16 位进行符号扩展以确保规范地址。
虽然我们已经解释了合法地址的翻译,但是是什么阻止了用户创建非法地址翻译并进行恶作剧? 页表本身受到保护,不会被用户程序写入。 因此,用户可以尝试任何虚拟地址,但操作系统通过控制页表条目来控制访问哪些物理内存。 进程之间的内存共享是通过让每个地址空间中的页表条目指向相同的物理内存页面来实现的。
Opteron 采用四个 TLB 来减少地址转换时间,两个用于指令访问,两个用于数据访问。 与多级缓存一样,Opteron 通过拥有两个更大的 L2 TLB 来减少 TLB 未命中:一个用于指令,另一个用于数据。 图 B.28 描述了数据 TLB。
Summary: Protection on the 32-Bit Intel Pentium Versus the 64-Bit AMD Opteron
Opteron 中的内存管理是当今大多数台式机或服务器计算机的典型特征,依靠页面级地址转换和操作系统的正确操作来为共享计算机的多个进程提供安全性。尽管作为替代方案出现,英特尔已经跟随 AMD 的步伐并采用了 AMD64 架构。 因此,AMD 和 Intel 都支持 80x86 的 64 位扩展; 然而,出于兼容性原因,两者都支持精心设计的分段保护方案。
如果分段保护模型看起来比 AMD64 模型更难构建,那是因为它确实如此。 这项工作对工程师来说一定特别令人沮丧,因为很少有客户使用精心设计的保护机制。 此外,保护模型与类 UNIX 系统的简单分页保护不匹配的事实意味着它只会被专门为这台计算机编写操作系统的人使用,而这种情况尚未发生。