不清空 GHR,如何按历史长度分级阻断 BHI?

Arm 分支预测专利解析 · 05
上一篇:预测 taken 的分支实际 not-taken 时,前端如何快速恢复?

这篇文章聚焦分支预测器的安全与性能折中:权限提升后不粗暴清空 GHR 或 ITTAGE,而是根据每张预测表实际使用的历史长度,判断旧权限历史何时已被完全覆盖,再逐级恢复预测能力。

专利:US 11,861,368 B2,Re-enabling Use of Prediction Table After Execution State Switch
申请人/受让人:Arm Limited
申请号:17/752,060;申请日:2022-05-24;授权日:2024-01-02
原始材料:本地 PDF · Google Patents · 公开文本

1. 先把结论说准确

这项方案不是在 EL0 → EL1 时清空全局分支历史(GHR),也不是把 ITTAGE 的训练状态全部作废。它做的是更细的控制:

  1. 进入更高权限状态时,先禁止 依赖 GHR 的 polymorphic branch target prediction table 参与目标选择;
  2. 保留 GHR 和各张预测表里的内容;
  3. 统计切换后有多少个分支真正向 GHR 分配了新历史;
  4. 一张表使用最近 N 个历史项,就等计数达到 N 后重新启用;
  5. 短历史表先恢复,长历史表后恢复;
  6. 不依赖 GHR 的 base table,以及本实施例里的 taken/not-taken 方向预测,始终可以工作。

它隔离的不是“表里的 EL0 entry”,而是 EL0 留在 GHR 中、仍可能影响 EL1 lookup 的那一段输入

还有一个专利阅读上的重要边界:独立权利要求 1 并没有要求“必须不清空 GHR”。它保护的是“权限上升时禁用表,并在历史更新数达到该表的历史长度后重新启用”这一控制关系。不清空 GHR 是说明书强调的性能收益和优选实施方式,不是独立权利要求中的必要限定。

2. 图 3:这不是抽象的 enable bit,而是一条完整的 ITTAGE 选择通路

图 3:GHR、ITTAGE 多历史表、branch counter 与 enable 控制

图 3 给出了专利真正讨论的硬件结构,关键编号如下:

编号 结构 作用
100 branch history storage / GHR 按 FIFO 保存最近满足分配条件的分支属性
102 prediction control circuitry 处理 execution-state switch,产生各表 enable
104 branch counter 统计切换后造成 GHR 更新的分支数
120 Tbase 只用 PC 派生的 hash 查找,不依赖 GHR
110/112/114 T0/T1/.../T(M-1) 分别用越来越长的 GHR0/GHR1/... 与 PC 共同 hash
124/126/128 tag/context/target 每个预测 entry 的 tag、上下文标识和目标地址
144/146/148 table enable 控制对应 tagged table 的结果能否参与选择
140 prediction selection 在“已启用且命中”的表中选历史最长者
150 target output 最终的 polymorphic branch target prediction

2.1 Base table 与 tagged tables 的 lookup 输入不同

Tbase 的 lookup value 130 只由 PC 派生:

Tbase_index/tag = H(PC)

各 tagged table 则分别使用不同长度的全局历史:

T0_lookup       = H(PC, GHR0)
T1_lookup       = H(PC, GHR1)
...
T(M-1)_lookup   = H(PC, GHR(M-1))

len(GHR0) < len(GHR1) < ... < len(GHR(M-1))

因此,权限切换后不是整个 predictor 都失效。Tbase 不读取 GHR,原文明确允许它保持 enabled;真正暂时退出候选集的是 T0...T(M-1)

2.2 “Disable table”不是擦除 entry

图 3 中 enable 144/146/148 直接送进 prediction selection 140。说明书给出的选择优先级是:

在 enabled && hit 的 tagged tables 中:
    选择使用最长 history 的那张表

如果没有 tagged table 同时满足 enabled && hit:
    使用 Tbase 的命中结果

如果 Tbase 也 miss:
    可退回 BTB,或者按 not-taken 继续顺序取指

所以“禁用”更准确的含义是:表可以仍被读出,entry 也仍然存在,但它的结果不被 selection 140 接纳。 专利不要求为安全而逐项 invalidate,也不要求重训 ITTAGE。

3. 图 4:BHI 如何让 BR_X 命中 BR_Y 的 EL1 entry

图 4:攻击者操纵 GHR,使受保护的 BR_X 别名到 BR_Y 的 target entry

图 4 比“攻击者污染分支预测器”这个泛化说法具体得多。

内核 160 中有两条 polymorphic branch:

  • BR_X(162)靠近从 EL0 进入内核的入口,因此周围带有投机屏障等保护;它的合法目标是 T_X0/T_X1
  • BR_Y(164)离入口较远,没有相同保护;它的 T_Y0/T_Y1 对 BR_Y 本身是合法目标,但如果从 BR_X 错误到达,就可能成为 gadget。

ITTAGE 中已有两条由 EL1 正常执行训练出的 entry:

entry 166: BR_X | context=EL1 | tag=0xF4 | target=T_X1
entry 168: BR_Y | context=EL1 | tag=0xBC | target=T_Y1

攻击者在 EL0 执行精心安排的分支序列,把 taken/not-taken、target 等分支属性写进 GHR 100,然后通过 supervisor call 进入 EL1。目标是让 BR_X 的 lookup 发生别名:

H(PC_X, malicious_GHR) == tag/index combination of BR_Y entry

于是 BR_X 没有使用自己的 T_X1,而是命中 entry 168,投机跳向 T_Y1。这个目标对 BR_Y 合法,却不是 BR_X 的合法后继。

3.1 为什么 context ID 拦不住

context 126 能阻止:

EL0 lookup -> EL1 entry
EL1 lookup -> EL0 entry

但图 4 的攻击是:

EL1 的 BR_X lookup -> EL1 的 BR_Y entry

entry 168 本来就是 EL1 训练的,context comparison 正常通过。被攻击者控制的是 H(PC, GHR) 的 GHR 部分,从而让一个 EL1 分支别名到另一个 EL1 分支。

这也是 BHI 与早期“直接跨权限训练 target entry”攻击的根本差别:攻击者不必把自己的 EL0 target 塞给内核,而是利用内核已有的合法 target,把内核代码以开发者没有预期的顺序串起来。

3.2 图 4 也说明了攻击限制

这不是任意地址跳转。攻击者仍需同时满足:

  • 找到可控的 EL0 分支历史序列;
  • 找到能够与 BR_X lookup 别名的内核 BR_Y entry;
  • T_Y0/T_Y1 中存在可利用 gadget;
  • 错误路径能在被纠正前留下 cache 等微架构痕迹。

专利说明书也承认这种攻击比早期 Spectre 变体更难,但指出它已在实践中被演示。

4. 为什么不采用四种更直接的方案

原文逐一讨论了替代方案,而不是笼统地说“清空性能差”。

4.1 用完整 PC 做精确 tag

如果 entry 保存足够精确的 PC tag,BR_X 就不会命中 BR_Y。但每个 entry 都会变宽,tag comparator 也会变宽,直接增加阵列面积、读出能耗和比较逻辑成本。专利的目标之一就是保留有别名可能、但成本较低的短 hash/partial tag 结构。

4.2 从 lookup hash 中删掉 GHR

这等价于只依赖类似 Tbase 的预测。代价不是一次性冷启动,而是长期丢失 path correlation:同一条间接分支在不同到达路径下可能有不同 target,去掉 GHR 后无法区分这些上下文。

4.3 权限上升时清空 tagged prediction tables

这样会丢掉 EL1 过去积累的 target 状态,而且逐项 invalidation 本身需要时间。内核重新热身的时间远长于一次短 syscall。

4.4 权限上升时清空 GHR

原文给出三个具体损失:

  1. 很多 supervisor call 在 EL1 只停留很短时间,回到 EL0 时原来的用户态历史仍可能有用;
  2. EL1 入口分支的方向有时与“从 EL0 的哪条路径发起 syscall”相关,保留 EL0 历史反而能提高 taken/not-taken 准确率;
  3. 原文称某些典型 workload 中 supervisor call 可能每几千个处理周期就发生一次,反复清空会持续伤害方向预测。

所以这项方案不是声称 EL0 历史“没有价值”,而是把它分成两类用途:

用于高风险 target-table lookup:暂时不可信
用于本实施例的 direction prediction:仍可使用

5. 控制逻辑:阈值必须等于“该表实际读取的历史长度”

权限上升时:

on switch(first_state, second_state):
    if privilege(second_state) > privilege(first_state):
        branch_counter = 0
        disable T0, T1, ..., T(M-1)

        keep GHR unchanged
        keep Tbase enabled
        keep branch-direction prediction enabled   // 本实施例

之后,每当一个分支满足 GHR allocation 条件并造成 GHR 更新:

branch_counter += 1

T0_enable       = branch_counter >= len(GHR0)
T1_enable       = branch_counter >= len(GHR1)
...
T(M-1)_enable   = branch_counter >= len(GHR(M-1))

这个阈值不是经验性的安全等待时间,而是一个覆盖证明。假设表 Ti 只读取最近 Ni 个有效历史项,GHR 又按 FIFO 更新,则:

counter < Ni:Ti 的 lookup window 里仍可能包含 EL0 history
counter >= Ni:Ti 的整个 lookup window 都是在切换后分配的 history

因此重新启用条件与 cycle 数、普通指令数、取指 block 数都无关。即使 EL1 已执行了很多 ALU/load/store,只要它们没有向 GHR 分配历史,就不能把旧的 EL0 history 挤出窗口。

6. 图 5:专利给出的 5/10/20 分支阈值

图 5:counter=6 时,只有 5-history 表 T0 可以重新启用

图 5 的例子有三张表:

lookup history 长度 ELx branch count = 6 时的状态
T0(110) 5 branches enabled
T1(112) 10 branches disabled
T2(114) 20 branches disabled

图中 GHR 尾部已经写入 6 个 EL1 历史项。因此 T0 所读取的最近 5 项必然全部来自 EL1;T1/T2 的窗口仍会向左覆盖到 EL0 区域。

图中的“6 branches”表示计数器 104 已统计到 6 个造成 branch history storage 更新的分支

7. 图 6:真正保留的性能来自两条始终 enabled 的路径

图 6:方向预测和 Tbase 始终启用,T0/T1/T2 依次恢复

图 6 把权限切换后的状态分成三类:

7.1 Branch direction predictor 44:持续 enabled

专利实施例认为,只要 target prediction 已被保护,方向预测错误通常只会在软件本来允许的两条路径之间选错:顺序下一条,或该分支的合法 target。它不像错误 target 那样把 BR_X 送到 BR_Y 的 gadget。

这是一个带前提的结论,不应写成“方向预测永远没有 BHI 风险”。说明书后文明确说,同样的计数/重启技术也可以用于其他依赖 GHR 的预测器,包括 TAGE。图 6 表达的是这一个 target-prediction 实施例中的风险划分。

7.2 Tbase 120:持续 enabled

Tbase 只由 PC 派生 lookup value,不读取被 EL0 注入的 GHR,所以无需等待覆盖。Tagged tables 暂时不可用时,ITTAGE 仍有一个低精度 fallback,而不是完全停止 target prediction。

7.3 T0、T1、T2:按历史长度依次恢复

切换瞬间全部 disabled;之后 T0 最先跨过阈值,T1 次之,T2 最后。selection 140 每个阶段都能使用“当前安全且命中的最长历史表”,性能不是在最长历史完全安全后一次性恢复,而是阶梯式恢复。

8. GHR 和 counter 到底统计什么

说明书对 GHR 100 的定义比“一个 taken/not-taken 位串”更宽:

  • 它保存最近 N 个满足 allocation 条件的分支,而不一定保存所有分支;
  • allocation 条件可以按 branch type、alignment 等筛选;
  • 每项属性可以是 taken/not-taken、target address、二者组合,权利要求 14 还覆盖 branch type;
  • 实现可以是 circular buffer,也可以是 shift register,但语义是 FIFO;
  • 表的 lookup window 可能从 circular buffer 尾部回绕。

说明书还允许 GHR 为了前端时效性而先写入预测的分支属性,并在 misprediction 时冲掉错误分支及其之后的历史。因此,不能擅自把 claim 中的“branches causing an update”翻译成“已退休分支数”。更准确的实现要求是:counter 必须与当前有效的 GHR allocation 语义一致。

由此还能推出一个必要的实现约束:如果误预测恢复会撤销若干 GHR 项,那么相应计数状态也必须避免把已撤销的历史当成“已覆盖”。否则 counter 可能提前达到阈值,而 lookup window 中实际上仍残留 EL0 历史。专利没有单独展开 counter checkpoint/rollback 电路,但其“窗口全部由切换后历史组成”的安全条件要求二者保持一致。

9. 如果很快返回 EL0,为什么可以立即把表打开

授权后的独立权利要求 1 特别加入了这一条件:如果在 counter 尚未达到阈值时就从 EL1 返回原来的 EL0,prediction control circuitry 重新启用第一预测表。

原因不是 GHR 已经“洗干净”,恰恰相反——GHR 中可能仍保留大量切换前的 EL0 历史。但处理器已经回到原来的低权限状态,不再存在“低权限历史影响更高权限 victim lookup”这一方向的隔离需求,而且旧 EL0 历史对恢复用户态预测很有价值。

可以把完整状态机写成:

EL0 -> EL1:
    counter = 0
    disable GHR-based target tables

stay in EL1:
    for each valid GHR allocation:
        counter++
        enable each table whose history-length threshold is met

EL1 -> original EL0 before all thresholds are met:
    re-enable remaining tables immediately

10. 权限切换不只等于 EL0 → EL1

图 2 和说明书把 execution state 定义为 exception level 与 security state 的组合。示例包括:

  • EL0 application → EL1 OS;
  • EL1 guest OS → EL2 hypervisor;
  • normal world → secure world(如 S-EL0/S-EL1/EL3 monitor);
  • 其他“第一状态到更高权限第二状态”的切换。

所以 EL0/EL1 是讲解 BHI 最直观的例子,不是权利要求唯一覆盖的状态对。

11. 权利要求到底保护了哪些层次

把授权文本拆开看,会比一句“Arm 防 BHI 专利”更清楚。

独立权利要求 1:最小控制闭环

核心组合是:

  1. branch history storage;
  2. 第一预测表至少基于最近 N 个历史项查找;
  3. 从第一状态切到更高权限的第二状态时禁用该表;
  4. 自切换后造成 history update 的分支数达到 N 时重新启用;
  5. 若未达到 N 就返回第一状态,也重新启用。

这里既没有限定必须是 ITTAGE,也没有限定 history 只能存 taken/not-taken,更没有把“不清空 GHR”写成 claim element。

从属权利要求 3–6:多表与 tagged-geometric 渐进恢复

  • 第二张表使用更长 history,因此更晚恢复;
  • 多张 tagged-geometric table 中优先采用“已启用、命中且历史最长”的表;
  • 按 history 长度递增顺序逐张恢复。

这部分才对应文章标题中的“分级”。

权利要求 7–9:第一类预测与第二类预测分开处理

  • 第一类:branch target address prediction,可被暂时禁用;
  • 第二类:taken/not-taken direction prediction,即使读取切换前历史也可保持 enabled。

图 6 就是这组权利要求的可视化。

权利要求 10–16:counter、hash、FIFO 与 polymorphic target

这些从属项覆盖:

  • reset 后的 branch counter;
  • hash(PC, history portion) lookup;
  • FIFO global history;
  • history property 可以依赖 direction、target 或 branch type;
  • 一条 branch 在不同 history 下对应多个 target 的 polymorphic prediction。

权利要求 17–18:不只保护分支预测器

第一类预测还可以是:

  • data/instruction prefetch prediction;
  • data/instruction value prediction。

只要预测表的 lookup 依赖某段 branch history,同样可以在权限上升后按对应历史长度禁用和恢复。ITTAGE 是最具体、最容易理解的实施例,不是保护范围的终点。

12. 从微架构实现角度看,真正需要落地的信号

如果把专利翻译成 RTL 级控制,至少要有以下信息:

privilege_up_switch
return_to_first_state
ghr_allocate
ghr_recovery / valid-history correction
branch_count_since_switch
history_length[T0...T(M-1)]
table_enable[T0...T(M-1)]

一个简化写法是:

if (privilege_up_switch) begin
    branch_count <= '0;
    table_enable <= '0;
end else if (return_to_first_state) begin
    table_enable <= '1;
end else if (ghr_allocate) begin
    branch_count <= branch_count + 1'b1;
    for (int i = 0; i < NUM_TABLES; i++) begin
        if (branch_count + 1'b1 >= HISTORY_LENGTH[i])
            table_enable[i] <= 1'b1;
    end
end

这段只是帮助理解,不是专利给出的 RTL。真实实现还要处理 counter 饱和、同周期 state switch 与 history allocation 的优先级、预测历史回滚、多个状态域以及不同表是否共享 folded history 等问题。

13. 最容易误读的五点

  1. 不是清空 ITTAGE。 entry 仍在,只是结果暂时不能进入 selection。
  2. 不是等待固定 cycle。 等的是足以覆盖该表 lookup window 的有效 GHR allocation 数。
  3. context tag 仍有用,但它解决的是另一类跨 context entry 污染。 BHI 的 BR_X/BR_Y 都是 EL1 entry。
  4. 方向预测保持开启是具体实施例的风险选择,不是“所有方向预测绝对安全”的普遍定理。
  5. 独立权利要求比 ITTAGE 更宽。 多表渐进恢复、target/direction 分离、polymorphic target 都是后续从属层次。

14. 最终理解

这项专利的关键不是“把攻击者留下的历史删除”,而是为每个依赖历史的预测资源建立一个精确的安全时刻:

只有当该资源实际读取的整个历史窗口
都已经由更高权限状态自己的新历史覆盖时,
它才重新获得参与预测的资格。

因此,安全边界跟着每张表的 history length 走:5-history 表等 5 次有效更新,10-history 表等 10 次,20-history 表等 20 次。与此同时,GHR、已训练的 target entries、base predictor 和方向预测的可用状态尽量保留下来。

这就是它相对“清空一切”的价值:不是少做一次 flush,而是把 BHI 防御从全局破坏,缩小成 按 predictor、按 table、按历史窗口长度控制可信度