# 2.4 虚拟内存与保护
<!-- source: p0152 -->

附录 B 的 B.4 节介绍了虚拟内存的关键概念。回顾一下，虚拟内存允许将物理内存视为辅助存储器的缓存；辅助存储器可以是硬盘，也可以是固态存储。虚拟内存在这两级存储之间搬移页面，就像缓存在不同层级之间搬移块一样。同样，TLB 充当页表的缓存，从而不必每次地址转换都访问内存。虚拟内存还能隔离共享同一物理内存、但拥有不同虚拟地址空间的进程。继续阅读之前，请确保已经理解虚拟内存的这两项功能。

本节进一步关注共享同一处理器的进程之间的保护与隐私问题。安全和隐私是 2025 年信息技术领域最棘手的两项挑战。电子窃密事件频频见诸报道，其中经常涉及信用卡号码清单；人们普遍认为，还有更多事件没有公开。当然，这类问题源于程序错误，使网络攻击者能够访问本不应访问的数据。程序错误不可避免，而在今天复杂的软件系统中，它们出现得相当频繁。因此，研究人员和从业者都在寻找更好的方法，使计算系统更加安全。尽管信息保护并不局限于硬件，在我们看来，真正的安全和隐私很可能既需要系统软件创新，也需要计算机体系结构创新。

本节首先回顾体系结构如何借助虚拟内存实现进程之间的相互保护，然后介绍侧信道攻击如何使信息跨越保护边界泄露。

## 通过虚拟内存实施保护

以页面为单位的虚拟内存，以及缓存页表项的 TLB，是进程相互保护的主要机制。附录 B 的 B.4 和 B.5 节回顾了虚拟内存，其中详细介绍了 80x86 如何通过分段和分页实现保护。本节只作简要回顾；如果你觉得过于简略，请参阅附录 B 中上述两节。

多道程序设计使多个并发运行的程序共享一台计算机，由此产生了程序之间相互保护、共享资源的需求，以及**进程（process）**这一概念。打个比方，进程就是程序赖以呼吸的空气和生存的空间：它包含一个正在运行的程序，以及让程序能够继续运行所需的全部状态。在任意时刻，都必须能够从一个进程切换到另一个进程。这称为**进程切换（process switch）**或**上下文切换（context switch）**。

<!-- source: p0153 -->

操作系统与体系结构共同配合，让进程共享硬件而不相互干扰。为此，体系结构必须在运行用户进程时限制其可访问的内容，同时允许操作系统进程访问更多内容。体系结构至少必须做到以下几点：

1. 提供至少两种模式，用于表明当前运行的是用户进程还是操作系统进程。后者有时也称为**内核进程（kernel process）**或**管理进程（supervisor process）**。
2. 使处理器的一部分状态对用户进程可用，但不允许用户进程写入。这些状态包括用户／管理模式位、异常使能／禁止位，以及内存保护信息。不能让用户写入这些状态，因为如果用户能够自行取得管理权限、禁止异常或改变内存保护，操作系统就无法控制用户进程。
3. 提供让处理器从用户模式进入管理模式、以及反向返回的机制。前者通常通过**系统调用（system call）**完成：一条特殊指令把控制权转移到管理程序代码空间中的指定位置，保存系统调用处的 PC，并使处理器进入管理模式。返回用户模式则类似于子程序返回，同时恢复先前的用户／管理模式。
4. 提供限制内存访问的机制，以保护进程的内存状态，而不必在每次上下文切换时将进程换出到磁盘。

附录 A 介绍了若干内存保护方案，但迄今最常见的方案，是为每个虚拟内存页面添加保护限制。[^appendix-protection] 典型大小为 4 KiB、16 KiB 或更大的固定大小页面，经由页表从虚拟地址空间映射到物理地址空间。保护限制包含在每个**页表项（page table entry，PTE）**中，可以规定用户进程是否有权读取该页、写入该页，以及是否可以执行该页中的代码。此外，如果某页不在进程的页表中，进程既不能读它，也不能写它。由于只有操作系统能够更新页表，分页机制提供了完整的访问保护。

分页虚拟内存意味着，从逻辑上说，每次内存访问至少都要花费两倍时间：一次访问取得物理地址，第二次访问取得数据。这个代价实在太高。解决办法仍然依靠局部性原理：如果访问具有局部性，相应的**地址转换**也必然具有局部性。将地址转换保存在专用缓存中后，一次内存访问就很少需要再访问一次内存来完成地址转换。这种专门的地址转换缓存，传统上称为**地址转换后备缓冲区（translation lookaside buffer，TLB）**。

TLB 表项类似缓存表项：标签存放虚拟地址的一部分，数据部分存放物理页地址、保护字段、有效位，通常还包括使用位和脏位。操作系统若要修改这些位，先修改页表中的值，再使相应 TLB 表项失效。

<!-- source: p0154 -->

当该表项从页表重新装入时，TLB 就取得这些位的准确副本。处理 TLB 未命中的办法，是从页表读取地址信息。随着地址空间不断扩大，页表结构变得更加复杂，处理器引入了专用硬件来处理 TLB 未命中。其方式类似缓存未命中处理，但更加复杂。附录 L 讨论更高级的地址转换技术。

假定计算机忠实遵守页面的访问限制，并正确地将虚拟地址映射到物理地址，似乎事情就解决了。然而，报纸头条却告诉我们并非如此。

问题没有解决，是因为我们不仅依赖硬件正确，还依赖操作系统正确。今天的操作系统由数千万行代码组成。缺陷通常以每千行代码的数量来衡量，因此投入使用的操作系统中存在数千个缺陷。操作系统缺陷形成的漏洞经常被人利用。不过，下一小节将说明，即便存储系统在功能上正确实现了隔离，仍然可能泄露受保护的信息。第 6 章将介绍虚拟机和 enclave 等技术，使程序即便面对有意作恶的操作系统，也能保护高度敏感的数据。遗憾的是，下面介绍的侧信道攻击有时还能绕过这些更强的保护机制。

## 针对存储系统的侧信道攻击

存储侧信道攻击的共同做法，是扰动存储系统中的某种状态，再利用处理器提供的计时器或性能计数器监测性能，观察扰动的效果。攻击之所以奏效，是因为多个独立进程共享了某些不属于指令集状态的硬件，例如缓存；这些硬件因而没有受到虚拟内存和操作系统通常用于隔离进程的那些机制的保护。

考虑下面这段简单代码：

```text
if (x <= 0) {访问 P}
else {访问 Q}
```

假设要利用侧信道攻击窥探这段代码，判断 x 是否小于或等于 0。过程分为三步：

1. **准备缓存（prime）**：窥探者将自己的信息写入那些将缓存 if-then-else 两条分支代码的缓存行。
2. 让受害者的代码运行一次，这会使 then 分支或 else 分支的代码发生一次未命中。
3. **探测缓存（probe）**：通过访问 P 和 Q，测量访问是命中还是未命中。可以使用精确计时器，也可以使用记录缓存未命中次数的性能计数器。如果 x≤0，就知道原来对应 P 的缓存行已被替换；窥探者再次访问这些缓存行时，就会发生未命中。知道哪些缓存行命中、哪些未命中，就能推断受害者走了哪条执行路径，从而泄露 x 的值的信息。

<!-- source: p0155 -->

这个简单例子用不同代码路径区分了我们想知道的关键事实，即 x≤0 是否成立；但从程序行为中推断数据值或执行路径的方法还有很多。为了详细了解另一个进程的内存内容，窥探者可能要重复执行许多次 prime–probe 序列，不过这些操作能够很快完成。此类攻击的关键是：即使虚拟内存系统禁止窥探者直接访问受害者的地址空间，二者仍然共享缓存。

要使 prime–probe 攻击奏效，需要知道受害者代码在内存中的位置，还需要确保受害者代码在准备与探测步骤之间运行，中间没有其他操作。只要执行了其他代码，缓存就可能被扰动，从而大幅削弱攻击效果。窥探者与受害者之间的切换本身需要开销，也就是一次进程切换，因此也可能扰动缓存、降低攻击效果。如果二者在同一多核处理器的不同核上运行，并共享 LLC，攻击就会更容易实施，也更有效。下一章还会说明，多线程会进一步增强此类攻击，因为它让窥探者与受害者更加紧密地共享硬件资源。

这类侧信道攻击有若干缓解办法，可以显著降低信息泄露给窥探者的概率。只要共享任何资源，就几乎不可能阻止所有侧信道攻击；但可以大幅降低泄露速率，使窥探受害者在实际中变得不可行。以下措施能显著提高实施侧信道攻击的难度：

- **随机分配页面。** 让攻击者难以判断某段代码会出现在缓存的什么位置。这对采用物理地址的缓存有效，对采用虚拟地址的缓存则无效。遗憾的是，某些操作系统代码可能固定在特定物理地址。另一种做法是，在使用高度敏感的信息之前，先将其复制到随机位置。关键在于切断缓存行为与已知代码或数据之间的联系。
- **扰乱计时。** 含有高度敏感信息的关键代码段，可以在执行期间插入短暂的随机延迟。20 世纪 70 年代，人们就曾用类似办法防御最早的一批侧信道密码攻击。
- **在上下文切换时清空缓存，消除隐含的共享。** 遗憾的是，这一办法必须覆盖 LLC，代价相当高。
- **划分资源。** 如果共享发生在 LLC，可以将其划分为多个区域，分别提供给同时活动的进程。这可能带来显著的性能代价，尤其是对于内存占用较大的应用。大多数面向服务器环境的处理器支持缓存划分。

<!-- source: p0156 -->

下一章将说明，引入推测执行和多线程会极大地提高侧信道攻击的带宽，可能达到原来的 100 倍，因此必须采用硬件或软件对策。

[^appendix-protection]: **译注：** 此处原文写“附录 A”，本节前文关于虚拟内存及保护机制的引用则指向附录 B 的 B.4 和 B.5 节。
