我有一种情况,其中一些地址空间是敏感的,因为你读它,你崩溃,因为没有人在那里响应该地址。
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。
作为人类,我们将流行音乐视为一个无条件的分支,但处理器不会一直通过管道。
预取和分支预测并不是什么新鲜事(在这种情况下,我们关闭了分支预测器),已经有几十年的历史了,并且不限于ARM,但是具有GPRPC的指令集的数量以及在某种程度上将其视为非特殊的指令的数量很少。
我正在寻找一个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也可以在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};例如arm模式下的bx lr),而不带行李,或者在pop pc之后立即将一个分支放入self(似乎解决了管道不会混淆b作为无条件分支的问题。
编辑
ldr pc,[something]
bx rn
也会导致预取。这不会属于-游行=armv4t。gcc故意生成ldrls pc,[]; b用于switch语句的某处,这很好。没有检查后端以查看是否有其他ldr pc,[]指令生成。
编辑
看起来ARM确实将此报告为勘误表(勘误表720247,推测式指令获取可以在内存映射中的任何位置进行),希望我在我们花一个月时间之前就知道…
https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html有一个-mpure-code
选项,它不会将常量放在代码段中。"此选项仅在使用MOVT指令为M-profile目标生成非pic代码时可用。"因此,它可能会使用一对mov即时指令而不是从常量池加载常量。
不过,这并不能完全解决你的问题,因为带有虚假寄存器内容的常规指令(在函数内的条件分支之后)的推测执行仍然可能触发对不可预测地址的访问。或者只是另一个函数的第一条指令可能是一个负载,所以落入另一个函数也不总是安全的。
我可以尝试阐明为什么这足够模糊,编译器还没有避免它。
通常,推测性执行故障不是问题的指令。CPU在变得非推测性之前不会真正接受故障。不正确的(或不存在的)分支预测会使CPU在找出正确的路径之前做一些缓慢的事情,但永远不应该有正确性问题。
通常,在大多数CPU设计中,内存中的推测负载是允许的。但是带有MMIO寄存器的内存区域显然必须受到保护。例如,在x86中,内存区域可以是WB(正常,可回写缓存,允许推测负载)或UC(不可缓存,不允许推测负载)。更不用说写组合写通了…
你可能需要类似的东西来解决你的正确性问题,以阻止推测性执行做一些实际会爆炸的事情。这包括由推测性bx r0
触发的推测性指令获取。(抱歉,我不知道ARM,所以我不能建议你如何做到这一点。但这就是为什么它对大多数系统来说只是一个次要的性能问题,即使它们有无法推测性读取的MMIO寄存器。)
我认为有一个设置让CPU从崩溃系统的地址进行推测性加载,而不仅仅是在它们变得非推测性时引发异常,这是非常不寻常的。
在这种情况下,我们关闭了分支预测器
这可能就是为什么您总是看到超出无条件分支(pop
)的推测执行,而不是很少看到。
很好的侦探工作,使用bx
返回,显示您的CPU在解码时检测到这种无条件分支,但不检查pop
中的pc
位。:/
一般来说,分支预测必须发生在解码之前,以避免获取气泡。给定一个fetch块的地址,预测下一个block-fetch地址。预测也是在指令级别而不是fetch-block级别生成的,供核心的后期使用(因为一个块中可以有多个分支指令,你需要知道取的是哪一个)。
这是一般理论。分支预测不是100%,所以你不能指望它来解决你的正确性问题。
x86 CPU可能会有性能问题,其中间接jmp[mem]
或jmp reg
的默认预测是下一条指令。如果推测性执行启动了取消速度较慢的内容(如某些CPU上的div
)或触发了缓慢的推测性内存访问或TLB,则一旦确定正确路径,它就会延迟执行。
因此,建议(通过优化手册)将ud2
(非法指令)或int3
(调试陷阱)或类似的放在jmp reg
之后。或者更好的是,将跳表目的地之一放在那里,这样“失败”有时是正确的预测。(如果BTB没有预测,下一步指令是它唯一能做的理智的事情。)
不过,x86通常不会将代码与数据混合在一起,因此对于文字池很常见的架构来说,这更有可能是一个问题。(但是在间接分支或错误预测的正常分支之后,来自虚假地址的负载仍然可能会推测性地发生。
例如if(address_good){call table[地址](); }
很容易错误预测并触发从错误地址获取推测性代码。但是如果最终的物理地址范围被标记为不可缓存,加载请求将在内存控制器中停止,直到它被知道是非推测性的
返回指令是一种间接分支,但下一条指令预测不太可能有用。所以也许bx lr
停顿是因为推测失败不太可能有用?
pop{pc}
(又名LDMIA
来自堆栈指针)要么在解码阶段未被检测为分支(如果它没有特别检查pc
位),要么被视为通用间接分支。ld
到pc
中肯定还有其他用例作为不返回分支,因此将其检测为可能的返回需要检查源寄存器编码以及pc
位。
也许有一个特殊的(内部隐藏的)返回地址预测器堆栈可以帮助每次正确预测bx lr
,当与bl
配对时?x86这样做,以预测调用
/ret
指令。
你测试过pop{r4, pc}
是否比pop{r4,lr}
/bx lr
更有效吗?如果bx lr
不仅仅是为了避免垃圾的推测执行而专门处理,让gcc来做这件事可能会更好,而不是让它用b
指令或其他东西来领导它的文字池。