【问题标题】:ARM prefetch workaroundARM 预取解决方法
【发布时间】:2018-02-17 12:32:42
【问题描述】:

我有一种情况,其中某些地址空间很敏感,因为您读取它会崩溃,因为那里没有人响应该地址。

pop {r3,pc}
bx r0

   0:   e8bd8008    pop {r3, pc}
   4:   e12fff10    bx  r0

   8:   bd08        pop {r3, pc}
   a:   4700        bx  r0

bx 不是由编译器作为指令创建的,而是一个 32 位常量的结果,该常量不适合作为单个指令中的立即数,因此设置了 pc 相对负载。这基本上是文字池。它恰好有类似于 bx 的位。

可以轻松编写测试程序来生成问题。

unsigned int more_fun ( unsigned int );
unsigned int fun ( void )
{
    return(more_fun(0x12344700)+1);
}

00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   4802        ldr r0, [pc, #8]    ; (c <fun+0xc>)
   4:   f7ff fffe   bl  0 <more_fun>
   8:   3001        adds    r0, #1
   a:   bd10        pop {r4, pc}
   c:   12344700    eorsne  r4, r4, #0, 14

在这种情况下,处理器正在等待从弹出(ldm)返回的数据移动到下一条指令 bx r0,并在 r0 中的地址处开始预取。哪个挂了 ARM。

作为人类,我们将 pop 视为无条件分支,但处理器不会,它一直通过管道。

预取和分支预测并不是什么新鲜事(在这种情况下我们关闭了分支预测器),已有数十年历史,并且不仅限于 ARM,而是将 PC 作为 GPR 的指令集的数量以及在某种程度上处理的指令它作为非特殊的很少。

我正在寻找一个 gcc 命令行选项来防止这种情况。我无法想象我们是第一个看到这个的人。

我当然可以这样做

-march=armv4t


00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   4803        ldr r0, [pc, #12]   ; (10 <fun+0x10>)
   4:   f7ff fffe   bl  0 <more_fun>
   8:   3001        adds    r0, #1
   a:   bc10        pop {r4}
   c:   bc02        pop {r1}
   e:   4708        bx  r1
  10:   12344700    eorsne  r4, r4, #0, 14

预防问题

注意,不限于 thumb 模式,gcc 也可以在 pop 之后使用文字池生成类似这样的 arm 代码。

unsigned int more_fun ( unsigned int );
unsigned int fun ( void )
{
    return(more_fun(0xe12fff10)+1);
}

00000000 <fun>:
   0:   e92d4010    push    {r4, lr}
   4:   e59f0008    ldr r0, [pc, #8]    ; 14 <fun+0x14>
   8:   ebfffffe    bl  0 <more_fun>
   c:   e2800001    add r0, r0, #1
  10:   e8bd8010    pop {r4, pc}
  14:   e12fff10    bx  r0

希望有人知道通用或特定于 arm 的选项来执行 armv4t 之类的返回(例如 pop {r4,lr}; bx lr 在 arm 模式下)没有行李或在 pop pc 之后立即将分支放到 self (似乎为了解决问题,管道不会混淆 b 作为无条件分支。

编辑

ldr pc,[something]
bx rn

也会导致预取。这不会属于-march = armv4t。 gcc 故意生成 ldrls pc,[]; b 某处用于 switch 语句,这很好。没有检查后端是否有其他 ldr pc,[] 指令生成。

编辑

看起来 ARM 确实将此报告为勘误表 (erratum 720247, "Speculative Instruction fetches can be made anywhere in the memory map"),希望我在我们花了一个月的时间之前就知道这一点...

【问题讨论】:

  • "(避免弹出 {pc}" - 我猜括号应该关闭吗?即用 nops 填充对你来说很好。丢失不是 100% 清楚")",但你为什么不喜欢填充没有多大意义。想想看,超级智能的编译器只有在数据中出现意外分支指令的情况下才会填充,否则数据可能会跟随而没有额外的填充。 (抱歉,我不知道 gcc 是否包含任何可以帮助您的内容)
  • 我想知道的是:ARM通常没有不可缓存内存的概念吗?如果 SoC 尝试预加载未连接的地址,那么告诉它可以缓存哪些区域的表肯定有问题。
  • @Ped7g 重新写了这个问题(再次)。我尚未确定例如基于寄存器的 ldr(bhd) 指令是否会启动最终挂起的读取。到目前为止,在 pop 解决问题之后,可能会使用分支到自我(分支到与分支相同的地址)的其他指令,而不必使用自定义 gnu 工具链。同样做 gcc 已经做的 armv4t 的事情,在用电脑返回时,会工作得很好,它不会对 bx 感到困惑。
  • @fuz 缓存和指令获取是两个不同的东西,指令获取可以去任何地址(在这种情况下,我认为它读取 4 个字或 8 个字,围绕地址对齐题)。缓存/mmu 不会阻止提取,我认为 mmu 没有指令/数据控制,并且无论如何都不会工作,因为您从 .text 进行提取和数据访问(如果没有其他内容,则为文字池)。
  • 由芯片设计人员确定 amba/axi 总线连接到什么以及它们如何响应,以及由设计人员决定覆盖了多少地址空间等。 ..在我们的例子中,手臂是更大设计的一小部分,手臂的整个地址空间是可编程的,就像 pcie 一样,我们可以改变各种大小的空间块来指向芯片的其余部分,但就像AXI,芯片的其他部分使用不会超时的总线(根据设计),如果程序员遇到没有目标响应的空间。

标签: assembly gcc arm armv6 speculative-execution


【解决方案1】:

https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html 有一个-mpure-code 选项,它不会将常量放在代码段中。 “此选项仅在使用 MOVT 指令为 M-profile 目标生成非 pic 代码时可用。”所以它可能会使用一对 mov-immediate 指令而不是从常量池加载常量。

但这并不能完全解决您的问题,因为带有虚假寄存器内容的常规指令(在函数内的条件分支之后)的推测执行仍可能触发对不可预测地址的访问。或者只是另一个函数的第一条指令可能是负载,因此进入另一个函数也并不总是安全的。


我可以尝试解释一下为什么这很模糊以至于编译器还没有避免它。

通常,推测性执行出错的指令不是问题。 CPU 在变为非推测性之前实际上不会承担故障。不正确(或不存在)的分支预测可能会使 CPU 在找出正确路径之前做一些缓慢的事情,但绝不应该存在正确性问题。

通常,大多数 CPU 设计都允许从内存进行推测加载。但是带有 MMIO 寄存器的内存区域显然必须受到保护。例如,在 x86 中,内存区域可以是 WB(正常,可回写缓存,允许推测加载)或 UC(不可缓存,无推测加载)。更别提写合写透了……

您可能需要类似的东西来解决您的正确性问题,以阻止投机执行做一些实际会爆炸的事情。 这包括由推测性bx r0 触发的推测性指令提取。 (抱歉,我不了解 ARM,所以我无法建议您如何这样做。 但这就是为什么对于大多数系统来说这只是一个很小的性能问题,即使它们具有无法推测性读取的 MMIO 寄存器。)

我认为让 CPU 从导致系统崩溃的地址进行推测性加载而不是仅仅在当/如果它们变为非推测性时引发异常的设置是非常不寻常的。


在这种情况下,我们关闭了分支预测器

这可能就是为什么您总是看到超出无条件分支(pop)的推测执行,而不是很少见的原因。

使用bx 返回的侦探工作很好,表明您的 CPU 在解码时检测到这种无条件分支,但没有检查pop 中的pc 位。 :/

一般来说,分支预测必须在解码之前进行,以避免获取气泡。给定一个获取块的地址,预测下一个块获取地址。预测也是在指令级别而不是 fetch-block 级别生成的,供核心的后续阶段使用(因为一个块中可以有多个分支指令,您需要知道采用哪一个)。

这是一般理论。 分支预测不是 100%,所以你不能指望它来解决你的正确性问题。


x86 CPU 可能存在性能问题,其中间接jmp [mem]jmp reg 的默认预测是下一条指令。如果推测性执行启动了一些取消速度很慢的事情(例如某些 CPU 上的div)或触发了缓慢的推测性内存访问或 TLB 未命中,则一旦确定正确路径,它就会延迟执行。

因此建议(通过优化手册)将ud2(非法指令)或int3(调试陷阱)或类似的放在jmp reg 之后。或者更好的是,将跳转表目的地之一放在那里,这样“失败”在某些时候是一个正确的预测。 (如果 BTB 没有预测,那么下一条指令是它唯一能做的理智的事情。)

不过,x86 通常不会将代码与数据混合在一起,因此对于字面量池很常见的架构来说,这更可能是一个问题。 (但来自虚假地址的加载仍然可能在间接分支或错误预测的正常分支之后发生推测性地发生。

例如if(address_good) { call table[address](); } 很容易错误预测并触发从错误地址获取推测性代码。但是如果最终的物理地址范围被标记为不可缓存,加载请求将在内存控制器中停止,直到知道它是非推测性的


返回指令是一种间接分支,但下一条指令预测不太可能有用。那么bx lr 可能会因为投机失败不太可能有用而停滞不前?

pop {pc}(又名来自堆栈指针的LDMIA)要么在解码阶段未被检测为分支(如果它没有专门检查pc 位),要么被视为通用间接分支。 ldpc 作为非返回分支当然还有其他用例,因此将其检测为可能返回需要检查源寄存器编码以及 pc 位。

当与bl配对时,也许有一个特殊的(内部隐藏的)返回地址预测器堆栈可以帮助每次正确预测bx lr? x86 这样做是为了预测call/ret 指令。


您是否测试过pop {r4, pc} 是否比pop {r4, lr} / bx lr 更有效?如果bx lr 被特别处理不仅仅是为了避免垃圾的推测性执行,那么让 gcc 这样做可能会更好,而不是让它用 b 指令或其他东西引导它的文字池。

【讨论】:

猜你喜欢
  • 2011-07-04
  • 1970-01-01
  • 1970-01-01
  • 2018-06-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-21
  • 2023-03-13
  • 2012-10-11
相关资源
最近更新 更多