old*_*mer 9 assembly gcc 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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
似乎正在发生的事情是处理器正在等待从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
Run Code Online (Sandbox Code Playgroud)
防止这个问题
注意,不限于拇指模式,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
Run Code Online (Sandbox Code Playgroud)
希望有人知道一个通用或手臂特定的选项来做一个像回归(pop {r4,lr}; bx lr在手臂模式中)的armv4t没有行李或者在pop pc之后立即将分支置于自己(似乎解决了问题管道不会混淆b作为无条件分支.
编辑
ldr pc,[something]
bx rn
Run Code Online (Sandbox Code Playgroud)
也导致预取.这不会落在-march = armv4t之下.gcc故意生成ldrls pc,[]; b某些地方用于switch语句,这很好.没有检查后端是否有其他ldr pc,[]指令生成.
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 从导致系统崩溃的地址进行推测性加载,而不是在它们变得非推测性时引发异常,这是非常不寻常的。
在这种情况下我们关闭了分支预测器
这可能就是为什么您总是会看到超出无条件分支(the pop)的推测执行,而不是很少见。
很好的侦探工作使用 abx返回,表明您的 CPU 在解码时检测到这种无条件分支,但不检查pca 中的位pop。:/
一般来说,分支预测必须在解码之前发生,以避免读取气泡。给定获取块的地址,预测下一个块获取地址。预测也是在指令级别而不是获取块级别生成的,以供核心的后续阶段使用(因为一个块中可以有多个分支指令,并且您需要知道采用哪一个)。
这就是一般理论。 分支预测不是 100%,因此您不能指望它来解决您的正确性问题。
x86 CPU 可能会出现性能问题,其中间接jmp [mem]or的默认预测jmp reg是下一条指令。如果推测执行启动的某些操作取消速度较慢(例如div在某些 CPU 上)或触发缓慢的推测内存访问或 TLB 未命中,则一旦确定正确路径,它可能会延迟执行。
因此,建议(通过优化手册)将ud2(非法指令)或int3(调试陷阱)或类似内容放在jmp reg. 或者更好的是,将跳转表目的地之一放在那里,这样“失败”在某些时候是正确的预测。(如果 BTB 没有预测,则下一条指令是它能做的唯一明智的事情。)
不过,x86 通常不会将代码与数据混合在一起,因此对于常见文字池的体系结构来说,这更可能是一个问题。(但是在间接分支或错误预测的正常分支之后,来自虚假地址的加载仍然可能发生。
例如,if(address_good) { call table[address](); }很容易错误预测并触发从错误地址进行推测性代码提取。但是,如果最终的物理地址范围被标记为不可缓存,则加载请求将在内存控制器中停止,直到知道它是非推测性的
返回指令是一种间接分支,但下一条指令预测不太可能有用。那么,也许bx lr因为投机性失败不太可能有用而停滞?
pop {pc}(又名LDMIA来自堆栈指针)在解码阶段没有被检测为分支(如果它没有专门检查该pc位),或者被视为通用间接分支。当然还有其他用例将ldintopc作为非返回分支,因此将其检测为可能的返回需要检查源寄存器编码以及位pc。
也许有一个特殊的(内部隐藏的)返回地址预测器堆栈,可以帮助bx lr每次与bl?配对时正确预测。x86 这样做是为了预测call/ret指示。
您是否测试过 ifpop {r4, pc}比pop {r4, lr}/更有效bx lr?如果bx lr进行特殊处理不仅仅是为了避免推测性执行垃圾,那么最好让 gcc 这样做,而不是让它用指令b或其他东西引导其文字池。