try ai
科普
编辑
分享
反馈
  • 数据推测:推测执行的威力与风险

数据推测:推测执行的威力与风险

SciencePedia玻尔百科
核心要点
  • 推测执行是一种 CPU 优化技术,处理器通过猜测指令(如分支)的结果来避免停顿,从而显著提升性能。
  • 虽然在架构上无害,但错误的猜测会在处理器的内部微架构状态(如 CPU 缓存)中留下“足迹”。
  • 像 Spectre 和 Meltdown 这样的漏洞利用这些微架构足迹,制造出可能泄露敏感数据的侧信道攻击。
  • 防御推测执行攻击需要一个整体性的、多层次的方法,涉及硬件、操作系统和编译器领域的协同创新。

引言

现代计算建立在对速度的不懈追求之上。几十年来,处理器性能的惊人增长不仅得益于尺寸更小、速度更快,更在于其变得更智能、更具预测性。这种智能的核心在于一项强大的技术——推测执行,即处理器猜测程序的未来走向,以便提前工作、节省时间。尽管这一策略对于提升性能至关重要,但长久以来人们一直认为它是一个完美封闭的内部过程,对程序员而言是不可见的。然而,这一假设已被打破,揭示了我们硬件核心深处一个深刻的安全缺陷。本文将深入探讨推测这把双刃剑。我们首先将在 ​​原理与机制​​ 部分探索推测执行的基础,理解其必要性以及处理器如何安全地管理其猜测。然后,在 ​​应用与跨学科联系​​ 部分,我们将审视这种设计的惊人后果,剖析 Spectre 和 Meltdown 等漏洞,并探讨新一代硬件架构师、编译器设计者和安全研究人员正在构建的统一、多层次防御体系。

原理与机制

想象一下,你有一位速度极快但又有些魯莽的助手。你还没说完要从图书馆借哪本书,他已经冲了出去,因为他猜到了你想要的那一本。如果他猜对了,书会以创纪录的时间出现在你的桌上。如果他猜错了,他就得羞愧地把错的书还回去,再重新去取正确的书,浪费一点时间。现代计算机处理器就像这位过于热切的助手。它们不断尝试猜测你程序的未来执行路径,这个技巧被称为​​推测执行​​。这不仅仅是一个聪明的伎俩,它是几十年来计算领域实现巨大性能增长的基本原则。但正如任何强大的思想一样,其 brilliance 也伴随着深刻而微妙的复杂性。

对速度的需求:与停顿赛跑

要理解处理器为何要费心去猜测,我们必须首先认识它所对抗的敌人:​​流水线停顿​​。可以将现代处理器流水线想象成一条超高效的工厂装配线。一条指令,就像一辆正在制造的汽车,会经过一系列阶段:从内存中取出(取指)、解码其含义(解码)、执行必要的计算(执行)、根据需要访问内存(访存),最后保存其结果(写回)。在理想世界中,每个时钟周期都有一条新指令进入流水线,同时有一条完成的指令离开。工厂正在全产能运行。

不幸的是,现实世界是混乱的。流水线经常陷入停顿,这种情况被称为“停顿”。两个主要元凶是:

  1. ​​数据冒险​​:一条指令需要的数据,而前一条指令尚未完成计算。流水线必须等待。
  2. ​​控制冒险​​:程序走到了一个岔路口——一个条件分支(if 语句)。在条件被求值(这发生在流水线深处的执行阶段)之前,处理器不知道该走哪条路。如果工厂车间经理每次需要做决策时都必须停下整条装配线,那么生产效率将急剧下降。

如果没有推测执行,处理器在遇到分支时将不得不停顿其流水线的前端,等待几个周期,直到正确的路径确定下来。这段等待时间被称为​​分支解决距离​​。推测执行是解决这个问题的 audacious 方案:不要等待,猜就是了!处理器采用复杂精密的​​分支预测器​​对程序将要走的路径做出有根据的猜测,并立即开始从该预测路径上取指和执行指令。

如果预测正确,那就大获全胜。处理器成功地“隐藏”了分支决策的延迟,在原本会空闲的周期里完成了有用的工作。有效的分支惩罚变成了零。这个简单的猜测行为可以将一个缓慢、断断续續的流水线变成一条平稳流动的计算之河,显著增加每周期执行的指令数。

猜测的艺术:它是如何工作的

那么,这场高风险的猜测游戏究竟是如何运作的呢?它依赖于一个简单而强大的口头禅:​​推测,但要验证​​。处理器可以做任何它想做的猜测,只要它有一个万无一失的机制来检查其工作,并在猜错时清理任何混乱。

这个过程由几个关键的微架构机制管理:

  • ​​重排序缓冲区 (ROB)​​:这是处理器的临时草稿纸。当指令被推测性地、并可能以非原始程序顺序执行时,它们的结果不会直接写入“官方”寄存器。相反,它们被保存在 ROB 中。ROB 跟踪原始顺序,并确保指令按正确的原始程序顺序“提交”或“引退”——使其结果在架构上可见。

  • ​​验证​​:在某个时刻,真实的结果会变得可知。分支的实际方向被计算出来,或者来自内存加载的真实值到达。处理器将这个“地面实况”与其推测进行比较。

  • ​​清空与恢复​​:如果猜测错误——即​​预测错误​​——处理器必须付出代价。它宣布沿着错误路径所做的一切都无效。它从流水线和 ROB 中刷新所有推测性的、不正确的指令,将其状态重置到错误猜测的点,然后从正确的路径重新开始。整个清理过程称为​​清空​​ (squash)。

推测的决定是一场计算过的风险,是在你节省的延迟 (LLL) 和你为预测错误付出的惩罚 (MMM) 之间的权衡,并由犯错的概率 (1−p1-p1−p) 调节。只要期望的惩罚小于节省的延迟,即 (1−p)⋅ML(1-p) \cdot M L(1−p)⋅ML,推测就是一个净收益。而且处理器不仅对分支进行推测;它们甚至可以执行​​值推测​​,即在数据从内存加载之前就猜测其值,从而进一步隐藏延迟。

游戏规则:保持推测安全

这种推测性的“自由发挥”听起来很危险。是什么阻止了处理器陷入混乱?一套深深嵌入其设计中的严格规则确保了推測仍然是一个秘密的性能增强手段,永远不会改变程序的最终正确结果。

规则一:不得违反因果律

处理器不能“凭空”(OOTA) 创造信息。考虑一个程序,其中处理器 1 仅在看到 yyy 为 1 时才将 xxx 设置为 1,而处理器 2 仅在看到 xxx 为 1 时才将 yyy 设置为 1。它们是否都能推测性地猜测对方的值将为 1,写入自己的 1,然后在一个循环悖论中确认自己的猜测?答案是坚决的“不”。主流架构被设计为禁止这种违反因果律的循环。原因通常是​​真数据依赖​​:使用某个值的指令不能在产生该值的指令之前执行。这种固有的数据流约束防止了可能导致此类悖论的重排序,即使在松散内存模型中也是如此。推测可以猜测未来,但它不能创造一个没有根据的现实。

规则二:不得干扰外部世界

推测是 CPU 私有的内部事务。其影响在被证实为正确之前,绝不能为外部世界所见。想象一个推测性加载指令的目标是一个特殊的内存地址,该地址对应一个硬件设备,如网卡或工厂机器人控制器。从这个地址读取可能会产生真实世界的​​副作用​​,比如发送一个网络数据包。如果一个推测性的、错误路径上的读取可能触发这样的行为,后果可能是灾难性的。为了防止这种情况,处理器将标记为“设备”内存的区域视为非推测性的。外部总线上的实际访问被延迟,直到该指令不再是推测性的并准备好提交,从而确保没有任何错误路径上的指令会“触碰”到外部世界。

规则三:异常处理必须精确

如果一条推测性指令不僅在错误的路径上,而且本身就是无效的呢?例如,从一个非法内存地址进行推测性加载应该会导致页错误。如果 CPU 立即发出警报,程序可能会因为一条本不应执行的指令中的错误而崩溃。这将违反​​精确异常​​的保证。为了处理这个问题,CPU 使用了复杂的机制。一种方法是让推测性指令推迟其异常。一个导致错误的推测性加载 (ld.s) 不会使系统崩溃;相反,它会用一个特殊标记(如“非事物” Not-a-Thing, 或 NaT 位)来“毒化”其目标寄存器。编译器或硬件会在加载指令原本应该在的位置插入一条检查指令 (chk.s)。只有当这条检查指令在正确路径上执行时,它才会检查寄存器,发现“毒药”,并正确地引发异常。这确保了异常只有在它们发生在真实的执行路径上时才会被报告,这一原则也要求数据必须在验证后才能使用。

机器中的幽灵:意外的后果

几十年来,这些规则似乎筑起了一道完美的墙,将 CPU 内部混乱的推测世界与程序员所见的有序、可预测的世界隔离开来。一条被清空的指令就像一场梦——它从未发生过,也未留下任何痕迹。或者说,我们曾是这么认为的。像 Spectre 这样的漏洞被揭示出来,这一惊人的发现来自于这堵墙存在裂缝。关键在于一个微妙的区别:

  • ​​架构状态:​​ 这是机器的“官方”状态——你的寄存器和主内存的内容。这个状态是神圣不可侵犯的,处理器会竭尽全力确保在预测错误后能完美恢复它。

  • ​​微架构状态:​​ 这是处理器庞大、隐藏的内部状态。它包括各种缓存的内容、分支预测器的状态,以及管理内存请求的行填充缓冲区 (LFB) 等瞬态缓冲区的内容。回滚这种复杂的状态通常是不可行的。

危险就在于此:​​瞬态的推测执行会在微架构状态中留下足迹。​​

一个简单的例子是​​缓存污染​​。当错误路径上的推测性加载获取数据时,它们会用无用的信息填充缓存,可能会驱逐正确路径稍后需要的有用数据。当执行在正确路径上恢复时,它会遭受额外的缓存未命中。缓存的内容——一个微架构结构——已经被“从未发生过”的指令改变了。

当攻击者能够观察到这些足迹时,这种性能上的烦恼就变成了严重的安全漏洞。这就是推测执行侧信道攻击的本质。考虑以下 mirroring 了 Spectre 漏洞的场景:

  1. 攻击者诱骗 CPU 推测性地执行一段它本不应执行的代码。
  2. 在这种瞬态执行期间,一条加载指令访问存储在地址 AAA 的一个秘密值(例如密码)。这条指令位于错误路径上,最终将被清空。
  3. 秘密值从未被写入架构寄存器。然而,从地址 AAA 加载数据的行为将其从慢速主内存带入了快速的片上​​缓存​​或​​行填充缓冲区 (LFB)​​。这是微架构状态的一个变化。 4s 推测性代码被清空。从架构上看,似乎什么都没发生。但访问的幽灵依然存在:秘密数据现在位于缓存中。
  4. 攻击者现在可以对地址 AAA 的后续正常访问进行计时。如果访问速度极快,攻击者就知道数据是从缓存中提供的。如果速度很慢,则数据不在缓存中。通过精心设计哪些地址被推测性地访问,攻击者可以利用这种时间差异来逐位泄露秘密值。

那个曾承诺带来无限速度的 brilliant 技巧,在机器中制造了一个幽灵。一条被清空、官方上从未存在的指令,仍然可以从其微架构的混沌状态中伸出手,向外部世界低语系统的秘密。那个旨在让计算机更快的机制,无意中使它们变得脆弱,为寻求安全和高性能计算的持续探索开启了一个充满挑战的新篇章。

应用与跨学科联系

窥见了定义推测执行的预测与纠正的复杂舞蹈后,我们或许会感到一种敬畏之情。我们制造出的机器能够凝视程序的未来,执行一条尚不存在的路径,并在猜测错误时无缝地倒转时间——所有这一切都在几十亿分之一秒内完成。这种能力是工程学的胜利,是现代计算速度的引擎。但是,当这个强大的、能穿越时间的处理器大脑被欺骗时,会发生什么?它那些幽灵般的、瞬态的思想会带来什么后果?

这就是我们旅程中一个引人入胜的转折点,从性能设计的纯净领域进入计算机安全的混乱、对抗性世界,并从那里走向一个跨越计算每一层的统一努力,从硅晶片到编译器最抽象的表示。推测执行应用的故事,是一个关于意外后果以及为应对这些后果而兴起的美丽、多学科科学的故事。

机器中的幽灵:Spectre 与 Meltdown

几十年来,硬件和软件之间的契约很简单:处理器保证,最终,它将按照书面规定,按顺序执行程序的指令。引擎盖下发生的狂野乱序执行是硬件自己的事,是其微架构的秘密。但事实证明,即使是短暂的、瞬态的思想——在预测错误的路径上执行并随后从架构历史中抹去的指令——也会留下微弱的足迹。它们可以 subtly 改变 CPU 缓存的状态,而一个聪明的攻击者可以测量这些改变的时间,从而读懂处理器的思想。

这一发现催生了两类漏洞,它们经常被混淆,但在方法上有着根本的不同,我们可以通过一系列典型行为来区分它们。

第一种是 ​​Meltdown​​,它更直接,在某些方面也更令人震惊。它利用了处理器本身的竞争条件,即读取一个禁止访问的内存地址的请求——比如用户程序试图从操作系统内核读取一个秘密——被推测性地满足了。在短暂的瞬间,数据被获取并转发给依赖的指令,然后处理器的特权检查电路才发出警报。处理器很快会发现自己的错误,引发一个故障,并清空非法的操作,这样秘密值就不会污染程序的架构状态,比如被写入寄存器。但为时已晚。那些看到秘密的瞬态指令可能已经用它来访问一个特定的缓存行,在内存层次结构中留下了一个攻击者可以检测到的“热点”。因此,Meltdown 是对硬件自身特权边界执行的一种攻击,是一次短暂的越狱。

​​Spectre​​ 则是另一种完全不同的野兽。它更微妙,更普遍,在许多方面也更深刻。Spectre 不破坏规则;它诱骗处理器错误地应用规则。攻击者“训练”处理器的分支预测器犯错。例如,通过反复使用有效输入调用一个函数,攻击者教会 CPU 预测某个安全检查——比如数组索引的边界检查——将会通过。然后,攻击者提供一个恶意输入,一个越界索引。CPU 遵循它的训练,推测性地冲过安全检查,执行在恶意偏移量处访问内存的代码。这种越界访问在架构上是合法的,因为它没有像 Meltdown 那样跨越特权边界,但它访问了程序逻辑设计要保护的内存部分。就像一个幽灵,CPU 瞬态地遵循了程序员禁止的路径,而它的幽灵行为可以被用来揭示秘密。Spectre 通过利用处理器的预测性来颠覆程序自身的逻辑。

瞬息念头的物理学

人们很容易认为这些瞬态执行是无所不能的,但它们和其他事物一样,受到相同的物理和信息流定律的约束。一次攻击是一场与时间的赛跑。一个恶意瞬态代码片段的整个序列,从最初的预测错误到最终影响缓存的操作,必须在“推测窗口”——即 CPU 的引退单元发现预测错误并清空不正确路径之前的几纳秒内——完成。

考虑一个需要一系列依赖操作的攻击,比如따라一个指针找到一个地址,然后使用该地址获取另一个值。第一次加载必须完成并传递其结果,第二次加载才能开始。每一步都需要时间,无论是对于一级缓存命中 (tL1t_{L1}tL1​) 的几个周期,还是对于 DRAM 访问 (tDRAMt_{DRAM}tDRAM​) 的数百个周期。如果推测窗口 WWW 比这个依赖链的延迟要短,攻击就根本无法成功。CPU 在恶意的“包袱”抖出来之前就纠正了它的路径。这意味着,要使一个复杂的瞬态执行成功,它不仅要在逻辑上巧妙,而且在微架构层面也必须足够快,以赢得与处理器自身纠错机制的赛跑。

多层次防御:凝聚的新共识

推测执行攻击的发现给整个计算机科学界带来了冲击波。它揭示了安全不仅仅是软件的属性,而是整个计算栈的一个涌现属性。因此,应对措施也同样是整体性的,在硬件设计、操作系统和编译器之间创造了一种美妙的相互作用。

编译器的巧思

也许在思想上最优雅的防御来自编译器和编程语言的世界。既然 Spectre 攻击利用了控制依赖(如 if 语句)的预测错误,那么我们是否可以转换代码,使其根本没有控制依赖?

想象一下易受攻击的代码:if (index limit) { access(array[index]); }。分支预测器可能会被欺骗。一个稳健的软件缓解措施,通常由一个具有安全意识的编译器实现,是将其转换为数据依赖。例如,可以编写无分支代码来钳制索引:safe_index = min(index, limit - 1); access(array[safe_index]);。推测性处理器无法打破真正的数据依赖。它 jednostavno 必须 等待 min 操作的结果,然后才能计算内存访问的地址。没有可以被愚弄的预测。通过用一个铁板钉钉的数据依赖替换一个可猜测的分支,程序员或编译器可以迫使 CPU 的推测引擎正确行事。

这种新发现的安全意识深入到编译器设计的核心。优化编译器建立在“as-if”规则之上:它们可以以任何方式转换程序,只要最终的可观察行为相同。但在一个有侧信道的世界里,什么是“可观察的”?一个安全检查,比如防止缓冲区溢出的栈金絲雀,几乎总是通过。一个激进的编译器可能会看到这一点,并“优化”掉这个检查,尤其是在推测路径上。为了防止这种情况,编译器设计者现在必须将这些安全检查正式建模为神圣的、有副作用的操作,即使它们看起来是多余的,也不能被重排序或移除。这导致了编译器中间表示中的新形式主义,确保一个检查不仅在架构上得到尊重,而且在瞬态执行中也同样如此 ([@problem_ID:3625609], [@problem_ID:3660412])。

为未来设计更智能的硬件

虽然软件补丁和编译器的英雄主义至关重要,但最终的解决方案可能在于设计更智能的硬件。与其将所有数据一视同仁,不如让处理器知道哪些数据是不可信的?这就是​​硬件污点跟踪​​背后的思想。

想象一下,任何从不可信来源——比如一个网络数据包——读取的数据都被标记上一滴隐喻的染料。这就是它的“污点”。硬件被设计成让这种污点扩散:任何由污点值计算出的值本身也变得带有污点。关键步骤不仅是将这种污点传播到数据,还要传播到用于内存操作的地址。

有了这个能力,CPU 内部的内存消歧逻辑可以变得更加智能。当它看到一个加载指令的地址没有被污染时,它知道这次访问很可能是良性的,可以使用其正常的、激进的推测。但当它看到一个加载指令的地址被污染了——意味着它受到了不可信输入的影响——它就知道与之前的存储发生恶意别名的风险很高。作为响应,它可以降低其激进性,等待所有先前的存储地址都已知后才发出加载指令。这是一个细粒度的、有针对性的解决方案。它只在需要谨慎的地方才谨慎行事,为绝大多数操作保留了性能,同时加固了系统以抵御攻击。这不仅仅是一个补丁;它是硬件理解其处理信息方式的一次原则性演进。

从一个让计算机更快的简单技巧开始,推测执行迫使我们重新思考我们设计的最基本 fundamentos。它揭示了编译器逻辑、操作系统规则和处理器瞬息念头的物理现实之间隐藏的统一性。机器中的幽灵,以其自身奇怪的方式,让我们成为了更好、更全面的工程师。