我已经阅读了有关Spectre v2的一些文章,显然您会得到非技术性的解释。彼得·科德斯(Peter Cordes)对explanation有更深入的了解,但并未完全解决一些细节。注意:我从未进行过Spectre v2攻击,因此没有实际经验。我只读过有关该理论的文章。
我对Spectre v2的理解是,您对实例if (input < data.size)
进行了间接分支错误预测。如果间接目标数组(我不太确定其细节,即为什么与BTB结构分开)(在解码时针对间接分支的RIP重新检查)不包含预测,则它将插入新的跳转RIP(分支执行最终将插入分支的目标RIP),但是目前它尚不知道跳转的目标RIP,因此任何形式的静态预测都将不起作用。我的理解是,它总是会预测不使用新的间接分支,并且当端口6最终确定跳转目标RIP并进行预测时,它将使用BOB进行回滚并使用正确的跳转地址更新ITA,然后更新本地和全局分支历史记录寄存器和饱和计数器相应地。
黑客需要训练饱和计数器,以始终预测所采取的措施,我想,他们是通过在if(input < data.size)
设置为确实小于input
的循环中多次运行data.size
来做到的(因此会捕获错误) ),并在循环的最后一次迭代中,使input
大于data.size
(例如1000);间接分支将被预测为采用,并且将跳转到发生高速缓存加载的if语句的主体。
如果if语句包含secret = data[1000]
(包含秘密数据的特定内存地址(数据[1000])的目标是从内存加载到缓存),则它将以推测方式分配给加载缓冲区。先前的间接分支仍在分支执行单元中,并等待完成。
我相信前提是在错误预测前刷新装载缓冲区之前,需要执行装载(为行填充缓冲区分配)。如果已经为它分配了行填充缓冲区,则无法执行任何操作。有意义的是,没有取消行填充缓冲区分配的机制,因为在将行填充缓冲区返回到加载缓冲区之后再存储到高速缓存之前,必须先填充行填充缓冲区。这可能会导致行填充缓冲区变得饱和,因为不是在需要时进行分配(将其保留在此处以提高其他负载到同一地址的速度,而是在没有其他可用行缓冲区时进行分配)。在接收到将不会发生刷新的信号之前,它将无法取消分配,这意味着它必须暂停执行上一个分支,而不是立即使行填充缓冲区可用于其他逻辑核心的存储。这种信号传递机制可能难以实施,并且可能没有引起他们的注意(Spectre思维),并且如果分支执行花费足够的时间来挂起行填充缓冲区以引起性能影响,则还会引入延迟。在循环的最后一次迭代之前,有意从缓存(data.size
)中清除了CLFLUSH
,这意味着分支执行可能需要多达100个周期。
我希望我的想法是正确的,但我不确定100%。如果有人要添加或更正任何内容,请这样做。
最佳答案
有时,术语“ BTB”被统称为指分支预测单元使用的所有缓冲区。但是,实际上有多个缓冲区,每个周期都使用所有缓冲区来进行目标和方向预测。特别是,BTB用于直接分支的预测,ITB(间接目标缓冲区)用于除了收益以外的间接分支的预测,而RSB用于收益的预测。 ITB也称为IBTB或间接目标阵列。所有这些术语都由不同的供应商和研究人员使用。通常,当其他缓冲区未命中时,BTB用于对各种分支指令进行初始预测。但是后来,预测变量了解了有关分支的更多信息,其他缓冲区开始起作用。如果同一间接分支的多个动态实例具有相同的目标,则也可以使用BTB代替ITB。当同一个分支有多个目标时,ITB的准确性要高得多,并且专门用于处理此类分支。请参阅:Branch prediction and the performance of interpreters — Don't trust folklore。奔腾M是第一个实现单独的BTB和ITB结构的英特尔处理器。所有以后的Intel Core处理器都具有专用的ITB。
Spectre V1漏洞利用程序是基于使用攻击者程序训练BTB的,因此,当受害者执行别名相同BTB条目的分支时,处理器就会被诱使以推测方式执行指令(称为小工具)以泄漏信息。 Spectre V2攻击类似,但基于训练ITB。这里的关键区别在于,在V1中,处理器错误地预测了分支的方向,而在V2中,处理器错误地预测了分支的目标(并且在条件间接分支的情况下,也错误地预测了分支的方向,因为我们希望它被采取)。在解释,JIT编译或利用动态多态性的程序中,可能有许多间接分支(除了返回)。特定的间接分支可能永远不会到达某个位置,但是通过对预测变量进行训练,可以使它跳转到我们想要的任何位置。正是出于这个原因,V2非常强大。无论小工具在何处,以及程序的故意控制流程是什么,您都可以选择一个间接分支之一,并使其推测性地跳转到小工具。
请注意,通常,静态直接分支目标的线性地址在程序的整个生命周期中都保持不变。只有一种情况可能不是这种情况:动态代码修改。因此,至少在理论上,可以基于直接分支的目标错误预测来开发Spectre漏洞利用。
关于LFB的回收,我真的不明白您在说什么。当未命中L1D的负载请求将数据接收到LFB中时,数据将立即转发到管道的旁路互连。必须有一种方法来确定哪个负载uop已请求此数据。返回的数据必须使用加载的uop ID进行标记。 RS中等待数据的微指令的源表示为负载的微指令ID。此外,保存负载uop的ROB条目需要标记为已完成,以便可以撤消,并且在SnB之前的版本中,需要将返回的数据写入ROB。如果在管道刷新时未取消LFB中未完成的加载请求,并且如果加载uop ID被其他uop重用,则在数据到达时,它可能会错误地转发到管道中当前存在的任何新uoop,从而破坏了微体系结构状态。因此,需要一种方法来确保在任何情况下都不会发生这种情况。通过简单地将所有有效LFB条目标记为“已取消”,取消管道刷新上的未完成的负载请求和推测性RFO很有可能,只是为了避免将数据返回到管道中。但是,仍可能会提取数据并将其填充到一层或多层缓存中。 LFB中的请求由行对齐的物理地址标识。可能还有其他可能的设计。
我决定进行实验以确定LFB在Haswell上何时重新分配的确切时间。下面是它的工作原理:
Outer Loop (10K iterations):
Inner Loop (100 iterations):
10 load instructions to different cache lines most of which miss the L2.
LFENCE.
A sequence of IMULs to delay the resolution of the jump by 18 cycles.
Jump to inner.
3 load instructions to different cache lines.
LFENCE.
Jump to outer.
为此,需要关闭超线程和两个L1预取器,以确保我们拥有L1的所有10个LFB。
LFENCE
指令可确保在正确预测的路径上执行时,我们不会用完LFB。此处的关键思想是内部跳转每次外部迭代都会被错误预测一次,因此在错误预测路径上最多可以在LFB中分配10个内部迭代负载。请注意,LFENCE
阻止分配来自后续迭代的负载。几个周期后,将解决内部分支并发生错误预测。清除了管道,并恢复了前端,以获取并执行外循环中的加载指令。有两种可能的结果:
已经为错误路径上的负载分配的LFB将作为管道清除操作的一部分立即释放,并可供其他负载使用。在这种情况下,不会因LFB不可用而导致停顿(使用
L1D_PEND_MISS.FB_FULL
进行计数)。LFB仅在负载得到服务时才释放,无论它们是否在错误的路径上。
当内部跳转之后外部循环中有三个负载时,
L1D_PEND_MISS.FB_FULL
的测量值大约等于外部迭代的次数。每个外循环迭代只有一个请求。这意味着,当正确路径上的三个负载发送给L1D时,来自错误路径的负载仍将占据8个LFB条目,从而导致第三个负载的FB满事件。这表明LFB中的负载仅在负载实际完成时才进行脱层处理。如果在外部循环中放置的负载少于两个,则基本上不会出现FB满事件。我注意到一件事:外循环中每增加三个负载,
L1D_PEND_MISS.FB_FULL
就会增加约20K,而不是预期的10K。我认为正在发生的事情是,当首次向L1D发出加载uop的加载请求并且所有LFB都在使用时,它被拒绝了。然后,当LFB变得可用时,会将加载缓冲区中的两个未决加载发送到L1D,一个将在LFB中分配,另一个将被拒绝。因此,每增加一个负载,我们就会得到两个LFB完整事件。但是,当外循环中有三个负载时,只有第三个负载在等待LFB,因此每次外循环迭代都会得到一个事件。本质上,加载缓冲区无法区分是拥有一个LFB还是两个LFB。只能知道至少有一个LFB是空闲的,因此由于有两个加载端口,它尝试同时发送两个加载请求。