我仍然很难弄清楚 Meltdown 如何使用推测执行。论文中的示例(与我之前提到的相同)仅使用 IMO OoO - @Name in a comment
Meltdown 基于 Intel CPU 乐观地推测负载不会出现故障,并且如果出现故障的负载到达负载端口,则它是早期错误预测分支的结果。因此,加载 uop 被标记,因此如果它达到退休状态就会出错,但执行继续推测性地使用页表条目表示您不允许从用户空间读取的数据。
它不会在负载执行时触发代价高昂的异常恢复,而是一直等到它确实达到退休,因为这是机器处理分支未命中 -> 坏负载情况的一种廉价方式。在硬件中,管道更容易保持管道,除非您需要它停止/停止以确保正确性。例如根本没有页表条目的加载,因此 TLB 未命中,必须等待。但是即使在 TLB hit 上等待(对于具有阻止使用它的权限的条目)也会增加复杂性。通常,只有在页面遍历失败(找不到虚拟地址的条目)之后,或者在其命中的 TLB 条目的权限失败的加载或存储退出时才会引发页面错误。
在现代 OoO 流水线 CPU 中,所有指令在退役之前都被视为推测性的。只有在退休时,指令才会变成非投机性的。 Out-of-Order 机器并不真正知道或关心它是在推测已预测但尚未执行的分支的一侧,还是推测过去可能出现故障的负载。 “推测”加载不会出错或 ALU 指令不会引发异常happens even in CPUs that aren't really considered speculative,但完全无序执行会将其变成另一种推测。
我不太担心“投机执行”的确切定义,以及什么重要/什么不重要。我对现代无序设计的实际工作方式更感兴趣,而且在管道结束之前甚至不尝试区分投机和非投机实际上更简单。这个答案甚至没有尝试通过推测性指令获取(基于分支预测)而不是执行来解决更简单的有序管道,或者在这之间的任何地方和full-blown Tomasulo's algorithm with a ROB + scheduler 与 OoO exec + 有序退休以获取精确异常。
例如,只有在退休之后,存储才能从存储缓冲区提交到 L1d 缓存,而不是之前。并且为了吸收短脉冲和缓存未命中,它也不必作为退休的一部分发生。因此,唯一的非投机性无序事情之一是将存储提交到 L1d;就架构状态而言,它们肯定已经发生,因此即使发生中断/异常,它们也必须完成。
fault-if-reaching-retirement 机制是避免在分支错误预测的阴影下进行昂贵工作的好方法。如果异常触发,它还会为 CPU 提供正确的架构状态(寄存器值等)。无论您是否让 OoO 机器在检测到异常的点之外继续处理指令,您都需要这样做。
分支未命中是特殊的:有缓冲区记录分支上的微架构状态(如寄存器分配),因此分支恢复可以回滚到那个状态而不是刷新管道并从最后一个已知良好的退休状态重新启动。分支确实错误地预测了实际代码中的相当数量。其他例外情况非常罕见。
现代高性能 CPU 可以在分支未命中之前保持(无序)执行微指令,同时丢弃该点之后的微指令和执行结果。快速恢复比丢弃并从可能远远落后于发现错误预测点的退休状态重新启动所有内容要便宜得多。
例如在一个循环中,处理循环计数器的指令可能会远远领先于循环体的其余部分,并在最后很快检测到错误预测以重定向前端并且可能不会损失太多实际吞吐量,特别是如果瓶颈是依赖链的延迟或者不是 uop 吞吐量。
这种优化的恢复机制仅用于分支(因为状态快照缓冲区是有限的),这就是为什么分支未命中与完整管道刷新相比相对便宜的原因。 (例如在 Intel 上,内存排序机器清除,性能计数器 machine_clears.memory_ordering: What are the latency and throughput costs of producer-consumer sharing of a memory location between hyper-siblings versus non-hyper siblings?)
不过,例外情况并非闻所未闻;页面错误确实发生在正常的操作过程中。例如存储到只读页面会触发写时复制。加载或存储到未映射的页面会触发页面输入或处理延迟映射。但是,即使在频繁分配新内存的进程中,通常也会在每个页面错误之间运行数千到数百万条指令。 (在 1GHz CPU 上每微秒或毫秒 1 个)。在不映射新内存的代码中,您可以走得更远,没有例外。在没有 I/O 的纯数字运算中,主要只是偶尔的计时器中断。
但无论如何,在您确定真正会触发异常之前,您不希望触发管道刷新或任何昂贵的事情。并且你确定你有 right 例外。例如可能较早的错误加载的加载地址没有尽快准备好,因此第一个执行的错误加载不是程序顺序中的第一个。等到退休是获得精确例外的一种廉价方法。处理这种情况的额外晶体管成本低廉,并且让通常的有序退休机器准确地确定哪个异常触发速度快。
在标记为报废时出错的指令之后执行指令执行的无用工作会消耗一点功率,并且不值得阻止,因为异常非常罕见。
这解释了为什么首先设计易受 Meltdown 影响的硬件是有意义的。显然,继续这样做是不安全的,因为 Meltdown 已经想到了。
廉价修复 Meltdown
我们不需要在错误加载后阻止推测执行;我们只需要确保它实际上不使用敏感数据。问题不是投机成功的负载,Meltdown 基于以下指令,使用该数据产生依赖于数据的微架构效果。 (例如,根据数据触摸缓存行)。
因此,如果加载端口将加载的数据屏蔽为零或其他内容,并设置故障时退休标志,执行将继续,但无法获得有关秘密数据的任何信息。这应该需要大约 1 个额外的关键路径门延迟,这在加载端口中可能是可能的,而不会限制时钟速度或增加额外的延迟周期。 (1 个时钟周期足以让逻辑通过流水线级中的许多 AND/OR 门传播,例如完整的 64 位加法器)。
相关:我在 Why are AMD processors not/less vulnerable to Meltdown and Spectre? 中为 Meltdown 的硬件修复建议了相同的机制。