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



这些问题源于阅读幽灵攻击论文。如果我理解正确,攻击源于 CPU 启发式推测性地执行(错误的)代码分支的可能性。 考虑示例(在 C 中):

int arr[42];
if (i < 42) {
int j = arr[i];
}

如果我正确理解了这篇论文,即使i >= 42int j = arr[i]也可以(在某些情况下)推测性地执行。我的问题是 - 当我访问超出其边界的数组时,我的程序经常会崩溃(Linux 上的分段错误,Windows 上的"程序执行非法操作"错误)。

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

关键点是,在现代CPU中,动词执行并不意味着你认为它的意思。

执行指令是计算其输出和副作用(如果有)的行为。
但是,这不会更改程序状态。
乍一看这似乎很难理解,但实际上没有什么异国情调。

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

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

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

所以现在 CPU 一次有 4/6 uops 来"执行",但如果 ISA 是串行的,除了排队这些 uop 之外,它还能做什么?
好吧,前端是这样制作的,这些 uop 不会在 AS 上运行,而是在影子状态上运行(SS,我的术语在这里),它们的操作数被重命名,由 CPU 的大不可见内存的一部分组成。并行
或无序更改 很好,因为它不是 AS。
这就是执行:改变SS。

真的值得吗?毕竟,重要的是 AS。
好吧,与执行相比,将SS转移到AS确实很快,因此值得。
这是一个"重命名回来"(反转以前的重命名)的问题,它被称为停用指令。

实际上,退休的意义远不止于此。
由于执行不会影响 AS,因此副作用也不应影响它。
这些副作用包括异常,但推测性地处理异常过于繁琐(它需要协调大量资源),因此异常处理会延迟到停用。
这还具有在处理异常时具有正确的 AS,并且仅在实际必须引发异常时才引发异常的优点。

推测执行的重点是打赌,CPU 押注指令序列不会生成任何异常(包括页面错误),从而在大多数检查关闭的情况下执行它(我不能排除,在我的头顶上,无论如何都不会进行一些检查),从而获得很多优势。
当需要停用这些指令时,将检查投注,如果有任何失败,SS 将被丢弃。

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

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

最新更新