理解 Meltdown 的关键在于理解现代 CPU 的一个核心设计理念:并行处理,为了提升性能,CPU 采用了“乱序执行”(Out-of-Order Execution, OOOE)技术。

我们可以把 CPU 想象成一个非常高效的厨师(执行单元),但他有一个奇怪的习惯:不等一道菜完全做完,他就会开始准备下一道菜的食材,因为他预判接下来你需要这些食材。
第一部分:背景知识 - 乱序执行
什么是乱序执行?
- 顺序执行:CPU 严格按照程序代码的顺序一条一条地执行指令,代码是 A -> B -> C,CPU 就先做 A,再做 B,最后做 C,这种方式简单但效率低下,因为指令之间可能存在等待(比如指令 A 需要从内存读取数据,这时 CPU 就会空闲等待)。
- 乱序执行:CPU 会分析指令流,找出没有先后依赖关系的指令,然后不按代码顺序,而是将那些可以立即执行的指令先拿出来处理,代码是
A -> B -> C,但 B 依赖于 A 的结果,而 C 不依赖任何东西,CPU 就可以先执行 C,然后再执行 A 和 B,这极大地提高了 CPU 的利用率,避免了等待,从而大幅提升了性能。
乱序执行的“副作用”
乱序执行虽然高效,但它带来一个关键问题:CPU 在执行“非法”指令时,它的工作状态(寄存器、缓存等)也会像执行合法指令一样被改变。
这个“非法”指令通常指的是“特权指令”(Privileged Instruction),这些指令只能由操作系统内核在最高权限级别(Ring 0)下执行,普通应用程序(在用户态,Ring 3)是无权执行的。
核心原则:当 CPU 试图执行一条特权指令时,硬件会检测到,并立即取消整个乱序执行的流程,丢弃所有已经执行但尚未提交的结果,并抛出一个异常(如 #GP),让操作系统来处理,这个过程叫做“管态切换”(Context Switch)或“陷阱”(Trap)。

第二部分:Meltdown 漏洞的攻击三部曲
Meltdown 的攻击者就是利用了乱序执行的这个“副作用”,攻击者是一个普通应用程序,它无法直接读取内核内存,但 Meltdown 通过一个精巧的三步法,绕过了这个限制。
触发“非法”读取,启动乱序执行
攻击者在自己的程序中,故意执行一条读取内核内存地址的指令。
// 假设 0xdeadbeef 是一个内核空间的地址 // 这条指令在用户态下是非法的 value = *(char *)0xdeadbeef;
当 CPU 的指令解码器看到这条指令时,它会识别出这是一个非法操作。在抛出异常之前,CPU 的乱序执行引擎已经启动了。
它会开始“预执行”这条指令:

- 它会尝试从地址
0xdeadbeef获取数据。 - 由于地址在内核空间,CPU 的内存管理单元会阻止这次访问。
- 在 MMU 拦截之前,数据已经被加载到了 CPU 的缓存中!
- 随后,CPU 的异常检测单元发现了这个非法操作,于是它丢弃了
value这个变量的结果,并准备触发一个异常。
关键点:虽然 value 的最终结果被丢弃了,但那个从内核地址读取的数据已经被悄悄地留在了 CPU 缓存里,这是一个“幽灵操作”,程序本身并不知道它发生过。
通过“侧信道”泄露缓存信息
攻击者知道了某个内核数据(我们称之为 Secret)已经被加载到了缓存中,他需要一种方法来“探测”这个数据是什么,这就是“侧信道攻击”的用武之地。
攻击者无法直接读取 Secret,但他可以观察 CPU 缓存的行为,他的方法是:
-
准备一个“探针”数组:在用户空间,创建一个很大的、可以放入 CPU 缓存的数组(大小为 256 字节)。
-
清空缓存:先访问这个数组的每一个元素,确保所有数据都在 L1 缓存中(这个过程叫“预热”)。
-
选择性访问:根据对
Secret的猜测,访问探针数组中的特定位置。// 假设 Secret 是一个内核地址的值,攻击者猜测它可能是 0, 1, 2, ..., 255 中的一个 // 攻击者会循环猜测,比如先猜 0 if (Secret == 0) { // 如果猜对了,步骤一中,Secret 的数据(0)会被加载到缓存 // 那么访问 probe_array[0] 就会很快,因为它在缓存里 access_time = measure_access_time(probe_array[0]); } else { // 如果猜错了,Secret 的数据不会加载到缓存 // 访问 probe_array[0] 就会慢,因为它需要从主内存读取 access_time = measure_access_time(probe_array[0]); } -
测量访问时间:通过高精度计时器,测量访问
probe_array[0]所需的时间。- 访问时间很短 -> 说明
Secret很可能是0。 - 访问时间很长 -> 说明
Secret不是0。
- 访问时间很短 -> 说明
通过这个方法,攻击者可以像“二分查找”一样,逐位、逐字节地“猜”出 Secret 的值,这个过程非常快,可以在毫秒级别内窃取大量敏感数据。
绕过 KASLR(地址空间布局随机化)
现代操作系统为了缓解这类攻击,使用了 KASLR 技术,它会随机化内核代码和数据在内存中的地址,让攻击者不知道 0xdeadbeef 这种具体地址对应什么。
但 Meltdown 依然可以绕过,攻击者不需要知道具体的内核地址,他可以:
- 泄露内核的基址(
_stext的地址)。 - 利用泄露的基址和公开的内核符号信息(可以从
/proc/kallsyms或调试接口获取),计算出任何内核函数或全局变量的精确地址。
一旦获得了这些地址,步骤一中的非法读取就可以精确地指向任何他想窃取的数据,如用户密码、密钥文件、或其他进程的内存。
第三部分:Meltdown 的本质与影响
本质
Meltdown 的本质是打破了 CPU 的“权限隔离”机制。 它利用了硬件设计(乱序执行)与软件(操作系统)之间的一个时间差,在软件意识到权限错误并采取行动(抛出异常)之前,硬件已经“不小心”地把高权限数据泄露到了低权限可以观察到的区域(缓存)。
影响
- 影响范围极广:几乎所有的 Intel CPU(2025年之前及之后的大部分型号)、部分 ARM 和 AMD CPU 都受到影响。
- 影响所有操作系统:Windows, Linux, macOS, iOS, Android 等所有运行在这些 CPU 上的操作系统都无法幸免。
- 后果严重:攻击者可以在同一台物理机上运行的任何用户程序(如浏览器、恶意网站脚本)中,窃取其他进程甚至整个内核的内存数据,导致敏感信息泄露。
缓解措施
Meltdown 的修复方案主要从软件层面入手,因为修改全球数以亿计的 CPU 硬件是不现实的。
-
KPTI (Kernel Page Table Isolation) - 内核页表隔离
- 原理:这是最核心的修复方案,它为每个进程(包括内核)维护两套独立的页表。
- 用户态页表:只包含用户空间的内存地址。
- 内核态页表:包含内核空间的内存地址。
- 执行:当 CPU 在用户态运行时,硬件页表寄存器指向用户态页表,当发生系统调用需要切换到内核态时,操作系统会立即切换硬件页表寄存器,指向内核态页表。
- 效果:这样,即使用户程序触发了乱序执行,试图访问内核地址,其地址翻译也会失败(因为用户态页表中没有这些映射),从而从源头上阻止了数据被加载到缓存。代价是每次系统调用都需要切换页表,带来了一定的性能开销(约 5%-30% 不等)。
- 原理:这是最核心的修复方案,它为每个进程(包括内核)维护两套独立的页表。
-
Retpoline (间接分支预测屏障)
- 原理:Meltdown 的变种 Spectre 漏洞利用了 CPU 的分支预测,Retpoline 是一种软件技术,通过特殊的代码序列“欺骗”分支预测器,使其做出错误的预测,从而阻止攻击者利用分支预测来泄露信息。
- 效果:主要用于防御 Spectre,但也是整个“幽灵”漏洞家族缓解方案的一部分。
| 特性 | 描述 |
