ARM预取解决方法

时间:2017-09-08 14:33:37

标签: gcc assembly arm armv6

我遇到一些情况,其中一些地址空间是敏感的,因为你没有人在那里回复该地址。

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

在这种情况下,处理器正在等待从pop(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

防止问题

请注意,不仅限于拇指模式,gcc也可以在弹出后使用文字池生成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

希望有人知道一个通​​用或手臂特定的选项来做一个armv4t像返回(例如pop {r4,lr}; bx lr在手臂模式下)没有行李或者在pop pc之后立即将一个分支放到自己身上(似乎为解决问题,管道不会将b作为无条件分支混淆。

修改

ldr pc,[something]
bx rn

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

1 个答案:

答案 0 :(得分:4)

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

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

我可以尝试解释为什么这个模糊不清,以至于编译器已经不能避免它了。

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

通常,大多数CPU设计都允许来自内存的推测性负载。但显然必须保护具有MMIO寄存器的存储区域。例如,在x86中,内存区域可以是WB(正常,可回写可缓存,允许推测加载)或UC(Uncacheable,无推测性加载)。更不用说写入组合直写......

您可能需要类似的东西来解决您的正确性问题,以阻止推测性执行从事实际爆炸的事情。 这包括由推测bx r0 触发的推测性指令获取。 (对不起,我不了解ARM,因此我无法建议 你是如何做到这一点的。  但这就是为什么它对大多数系统来说只是一个小的性能问题,即使它们有MMIO寄存器不能被推测性地读取。)

我认为设置允许CPU通过崩溃系统的地址进行推测性加载而不仅仅是在/如果它们变为非推测时引发异常,这是非常不寻常的

  

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

这可能就是为什么你总是总是看到超出无条件分支(pop)的推测性执行,而不是非常罕见。

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

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

这是通用理论。 分支预测不是100%,因此您无法依靠它来解决您的正确性问题。

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

所以建议(通过优化手册)在ud2之后放置int3(非法指令)或jmp reg(调试陷阱)或类似内容。或者更好的是,将其中一个跳转目的地放在那里&#34;直播&#34;在某些时候是正确的预测。 (如果BTB没有预测,那么下一条指令就是它唯一能做的事情。)

但是,x86通常不会将代码与数据混合在一起,因此对于文字池常见的体系结构来说,这更可能是一个问题。 (但是,在间接分支或错误预测的正常分支之后,来自伪造地址的负载仍然可以推测性地发生。

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

返回指令是一种间接分支,但下一指令预测有用的可能性较小。也许bx lr可能因为投机性堕落不太可能有用而停滞不前?

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

也许有一个特殊的(内部隐藏的)返回地址预测器堆栈,每次与bx lr配对时,有助于正确预测bl? x86执行此操作,以预测call / ret指令。

您是否测试过pop {r4, pc}是否比pop {r4, lr} / bx lr效率更高?如果专门处理bx lr不仅仅是避免推测执行垃圾,那么最好让gcc执行此操作,而不是让它使用b指令或其他内容引导其文字池。< / p>