用 "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.
结束之前的指令结束可能没有区别
我发现 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.