为什么CPU推测执行不会导致OOB程序崩溃?

luk*_*keg 2 memory security cpu cpu-architecture speculative-execution

这些问题源于阅读Spectre 攻击论文。如果我理解正确的话,攻击源于 CPU 启发式推测执行(错误)代码分支的可能性。考虑这个例子(C 语言):

int arr[42];
if (i < 42) {
    int j = arr[i];
}
Run Code Online (Sandbox Code Playgroud)

如果我正确理解了这篇论文,int j = arr[i]即使在i >= 42. 我的问题是 - 当我访问超出其范围的数组时,我的程序经常会崩溃(Linux 上的分段错误,Windows 上的“程序执行了非法操作”错误)。

为什么在数组越界访问的情况下推测执行不会导致程序崩溃?

Mar*_*oom 8

关键在于,在现代 CPU 中,动词“执行”的含义并不像您想象的那样。

执行指令是计算其输出和副作用(如果有)的行为。
但是,这不会改变程序状态。
乍一看这似乎很难理解,但实际上并不奇怪。

CPU有一个由所有寄存器组成的相当大的内部存储器,其中大部分存储器对程序员来说是可见的,这部分被称为架构状态
架构状态 (AS) 是 CPU 手册中记录的内容以及由指令序列(例如程序)更改的内容。

由于更改 AS 只能使用 ISA(手册)中给出的语义进行,并且 ISA 指定了串行语义(指令按照程序顺序一个接一个地完成),因此不允许并行性。
然而,现代 CPU 拥有大量可以独立完成工作的资源(称为执行单元)。

为了利用所有这些资源,CPU 的前端(负责从内存层次结构读取指令并将其馈送到执行单元的部分)能够在每个周期到达、解码和输出多个指令。
前端和后端(执行单元所在的位置)之间的边界不再真正处理指令(而是处理微指令),但这是 x86 CISC 的麻烦。

所以现在 CPU 一次被给予 4/6 uops 来“执行”,但如果 ISA 是串行的,除了对这些 uops 进行排队之外,它还能做什么?
好吧,前端的设计是为了让这些微指令不在 AS 上操作,而是在影子状态(SS,我的术语)上操作,它们的操作数被重命名,由 CPU 的大的不可见内存的一部分组成。
改变并行或乱序都可以,因为它不是 AS。
这就是执行:改变 SS。

真的值得吗?毕竟,AS 才是最重要的。
嗯,与执行相比,将 SS 转移到 AS 确实很快,所以这是值得的。
这是“重命名回来”(反转之前的重命名)的问题,称为指令的 退休。

事实上,退休的意义远不止于此。
由于执行不会影响 AS,因此副作用也不会影响它。
这些副作用包括异常,但推测性地处理异常太麻烦(需要协调大量资源),因此异常处理被推迟到退休。
这还有一个优点,就是在处理异常时拥有正确的 AS,并且只有在实际必须发生异常时才引发异常。

推测执行的要点是打赌,CPU打赌指令序列不会生成任何异常(包括页面错误),因此在大多数检查关闭的情况下执行它(我无法排除,某些检查无论如何都不会制造)从而获得很多优势。
当需要废弃这些指令时,将检查赌注,如果任何失败,则 SS 将被丢弃。

这就是为什么推测执行不会使程序崩溃。

Spectre 所依赖的事实是,推测性执行确实在某种意义上改变了 AS:缓存不会失效(再次出于性能原因,当赌注关闭时,SS 不会被复制到 AS 中)并且可能发生定时攻击。
这可以通过多种方式纠正,包括在从 TLB 读取时执行基本权限检查(毕竟仅使用权限 0 和 3,因此逻辑很简单)或向缓存行添加一个位以将其标记为推测的(被非推测代码视为无效)。