用“长NOP”填充可执行部分的原因是什么?

时间:2017-09-22 06:37:15

标签: gcc assembly x86 x86-64 micro-optimization

我发现x86-64程序(至少那些使用GCC编译的程序)默认情况下会在与16字节的倍数对齐的地址处启动,并且填充由NOP指令完成,其中尽可能多适合的前缀可以最佳地填充空间。例如,

  (...)
  447454:   c3                              retq   
  447455:   90                              nop
  447456:   66 2e 0f 1f 84 00 00 00 00 00   nopw   %cs:0x0(%rax,%rax,1)

0000000000447460 <__libc_csu_fini>:
  447460:   f3 c3                           repz retq 

使用常规herehere等常规NOP填充空间有什么好处?

1 个答案:

答案 0 :(得分:3)

没有缺点,为什么不呢?它使拆卸更容易为人类阅读,因为你没有大量的线分隔功能。

GCC(将C转换为汇编的实际编译器部分)使用相同的.p2align指令要求汇编器插入填充,无论它是在函数内部来对齐分支目标,还是在函数之间对齐函数入口分。

GCC可以发出.p2align 4,,0x90来要求汇编器在不执行NOP的情况下填充单字节NOP,但就像我说的那样,没有理由去做那个而不是{{1 (使用默认的填充选项填充到下一个.p2align 4边界)。

如果函数的结尾是间接分支(使用2^4或其他东西进行尾调用),则推测执行可能会遇到这些NOP指令。解码许多短NOP可能会溢出Intel SnB系列上的uop缓存。 (每32字节块超过3个缓存行,最多6个uop)。 (http://agner.org/optimize/ microarch pdf)。长NOP可能更好。

IDK Pentium4的跟踪缓存构建器如何表现;也许它也有用吗?同样,在确定NOP没有被执行之前,更少的NOP指令不太可能在CPU的前端触发任何奇怪的东西。

MSVC在函数IIRC之间填充jmp [rax],这将停止推测​​执行。这不是一个坏主意。

这是猜测;它可能不是性能的真正因素;如果它仍然在现代CPU上仍然很重要,那么所有编译器都可能避免功能之间的短NOP,但是正如你的一个链接所示,并非所有人都这样做。

某些CPU(如AMD K8 / K10和Bulldozer系列)标记L1I缓存中的指令长度。 Agner Fog表示,在K8 / K10上,从L2到L1I的带宽很低,并且猜测它可能来自添加额外的预解码信息。 IDK如果有很多小指令需要更长时间?它必须知道从哪里开始解码,因为指令的中间可以跨越缓存行边界。 IDK是如何运作的。

顺便说一句,这些指令可能被解码为包含普通int3的组的一部分,但我不认为在这种情况下有任何方法可以担心。

在某些CPU中,解码分两个阶段进行:首先,指令长度解码,找到包含最多4条指令的最多16字节的块(例如,在Intel P6系列/ Sandybridge系列上)。然后它将这些块提供给解码器。

ret进行正确的分支预测,即使ret之后LCP失速等令人讨厌的东西似乎没有受到伤害。

无论如何,我认为这种差异并不显着。 ret之后的解码NOP指令应在取消之前取消,因为RET是无条件分支。对于指令长度解码器是否发现许多单字节指令而不是某些前缀而不是16字节窗口结束之前的指令结束,我可能没有任何区别。