有时,术语“BTB”被统称为分支预测单元使用的所有缓冲区。然而,实际上有多个缓冲区,所有这些缓冲区都在每个周期中用于进行目标和方向预测。具体来说,BTB用于对直接分支进行预测,ITB(indirect target buffer)用于对除return以外的间接分支进行预测,RSB用于对return进行预测。ITB 也称为 IBTB 或间接目标阵列。所有这些术语都被不同的供应商和研究人员使用。通常,BTB 用于在其他缓冲区未命中时对各种分支指令进行初始预测。但后来预测器了解了更多关于分支的信息,其他缓冲区开始发挥作用。如果同一间接分支的多个动态实例都具有相同的目标,则也可以使用 BTB 代替 ITB。当同一个分支有多个目标并且专门为处理这些分支而设计时,ITB 会更加准确。看:分支预测和解释器的表现——不要相信民间传说。第一个实现独立 BTB 和 ITB 结构的 Intel 处理器是 Pentium M。所有后来的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,则当数据到达时,它可能会被错误地转发到当前正在管道中的任何新 uop,从而破坏微架构状态。所以需要有一种方法来确保在任何情况下都不会发生这种情况。通过简单地将所有有效的 LFB 条目标记为“已取消”,就可以取消未完成的加载请求和管道刷新上的推测性 RFO,这样数据就不会返回到管道。但是,数据可能仍会被提取并填充到一级或多级缓存中。LFB 中的请求由行对齐的物理地址标识。可以有其他可能的设计。很可能通过简单地将所有有效的 LFB 条目标记为“已取消”来取消未完成的加载请求和管道刷新上的推测 RFO,这样数据就不会返回到管道。但是,数据可能仍会被提取并填充到一级或多级缓存中。LFB 中的请求由行对齐的物理地址标识。可以有其他可能的设计。很可能通过简单地将所有有效的 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 仅在负载得到维修时才被释放,无论它们是否在错误预测的路径上。
在内层跳转后外层循环有3个载荷时,测得的值L1D_PEND_MISS.FB_FULL
约等于外层迭代次数。这是每个外部循环迭代的一个请求。这意味着当正确路径上的三个负载被发送到 L1D 时,来自错误预测路径的负载仍然占用 8 个 LFB 条目,从而导致第三个负载的 FB 满事件。这表明 LFB 中的负载仅在负载实际完成时才被解除涂层。
如果我在外循环中放入的负载少于两个,则基本上不会出现 FB full 事件。我注意到一件事:对于外部循环中超过三个负载的每一个额外负载,L1D_PEND_MISS.FB_FULL
增加了大约 20K 而不是预期的 10K。我认为正在发生的事情是,当第一次向 L1D 发出加载 uop 的加载请求并且所有 LFB 都在使用时,它会被拒绝。然后,当 LFB 可用时,加载缓冲区中的两个未决加载被发送到 L1D,一个将在 LFB 中分配,另一个将被拒绝。因此,每增加一次负载,我们就会得到两个 LFB 完整事件。但是,当外循环中有三个负载时,只有第三个会等待 LFB,因此每次外循环迭代都会得到一个事件。本质上,加载缓冲区无法区分一个 LFB 可用还是两个 LFB;它只知道至少有一个 LFB 是空闲的,因此它会尝试同时发送两个加载请求,因为有两个加载端口。