用 "long NOPs" 填充可执行部分的原因是什么?

What's the reason for padding executable sections with "long NOPs"?

我发现 x86-64 程序(至少是那些使用 GCC 编译的程序)的函数默认从与 16 字节的倍数对齐的地址开始,并且填充是由 NOP 指令和 尽可能多的前缀 以最佳地填充 space。例如,

  (...)
  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 

像观察到的 here or here 那样用常规 NOP 填充 space 有什么好处?

没有缺点,为什么不呢?它使反汇编更易于人类阅读,因为您没有大量的行来分隔函数。

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

GCC 可以发出 .p2align 4,,0x90 要求汇编程序在 NOP 不会被执行的情况下填充单字节 NOP,但就像我说的,没有理由这样做而不是 .p2align 4(填充到下一个 2^4 边界,默认选择填充符)。


如果函数的末尾是一个间接分支(使用 jmp [rax] 或类似的尾调用),推测执行可以 运行 进入这些 NOP 指令。解码许多短 NOP 可能会溢出 Intel SnB 系列上的 uop 缓存。 (超过 3 个高速缓存行,每 32 字节块最多 6 微指令)。 (http://agner.org/optimize/ 微架构 pdf)。长 NOP 可能会更好。

了解 Pentium4 的跟踪缓存构建器的行为;也许它也有用吗?同样,在 CPU 发现未执行 NOP 之前,更少的较长 NOP 指令不太可能在前端触发任何奇怪的事情。

MSVC 在函数之间用 int3 填充,IIRC,这将停止推测​​执行。这主意不错。

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

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


顺便说一句,这些指令 可能 被解码为包含正常 ret 的组的一部分,但我认为这两种方式都没有什么可担心的那种情况。

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

通过 ret 的正确分支预测,即使是 ret 之后 LCP 等令人讨厌的东西似乎也不会受到伤害。

无论如何,我认为这种差异并不显着。在 RET 之后解码的 NOP 指令应该在它们到达任何地方之前被取消,因为 RET 是一个无条件分支。指令长度解码器是否找到许多单字节指令与一些前缀但不是在 16 字节 window.

结束之前的指令结束可能没有区别