21

在此编译器输出中,我试图了解nopw指令的机器代码编码是如何工作的:

00000000004004d0 <main>:
  4004d0:       eb fe                   jmp    4004d0 <main>
  4004d2:       66 66 66 66 66 2e 0f    nopw   %cs:0x0(%rax,%rax,1)
  4004d9:       1f 84 00 00 00 00 00

在http://john.freml.in/amd64-nopl有一些关于“nopw”的讨论。谁能解释 4004d2-4004e0 的含义?从操作码列表看,代码似乎66 ..是多字节扩展。我觉得我可能会比我在这里得到更好的答案,除非我尝试探索操作码列表几个小时。


该 asm 输出来自 C 中的以下(疯狂)代码,该代码优化为一个简单的无限循环:

long i = 0;

main() {
    recurse();
}

recurse() {
    i++;
    recurse();
}

当使用 编译时gcc -O2,编译器识别出无限递归并将其变成无限循环;事实上,它做得很好,以至于它实际上在main()没有调用recurse()函数的情况下循环。


编者注:使用 NOP 填充函数并不特定于无限循环。这是 Godbolt 编译器资源管理器上具有一系列 NOP 长度的函数集。

4

4 回答 4

25

0x66字节是“操作数大小覆盖”前缀。拥有一个以上就等于拥有一个。

0x2e是 64 位模式下的“空前缀”(否则它是 CS: 段覆盖 - 这就是它出现在程序集助记符中的原因)。

0x0f 0x1f是采用 ModRM 字节的 NOP 的 2 字节操作码

0x84ModRM 字节,在这种情况下编码为使用 5 个以上字节的寻址模式。

一些 CPU 对具有许多前缀(例如超过三个)的指令进行解码的速度很慢,因此指定 SIB + disp32 的 ModRM 字节是一种比多五个前缀字节更好地使用额外 5 个字节的方法。

Agner Fog 的 microarch pdf 中的 AMD K8 解码器

每个指令解码器可以在每个时钟周期处理三个前缀。这意味着可以在同一个时钟周期内对具有三个前缀的三个指令进行解码。具有 4 - 6 个前缀的指令需要额外的时钟周期来解码。


本质上,这些字节是一条长的 NOP 指令,无论如何都不会被执行。它在那里确保下一个函数在 16 字节边界上对齐,因为编译器发出了一个.p2align 4指令,所以汇编器用 NOP 填充。 gcc 对于 x86 的默认设置是
-falign-functions=16
. 对于将要执行的 NOP,long-NOP 的最佳选择取决于微架构。对于在许多前缀上阻塞的微架构,例如 Intel Silvermont 或 AMD K8,两个具有 3 个前缀的 NOP 可能解码得更快。

该问题链接到的博客文章 ( http://john.freml.in/amd64-nopl ) 解释了为什么编译器使用复杂的单个 NOP 指令而不是一堆单字节 0x90 NOP 指令。

您可以在 AMD 的技术参考文档中找到有关指令编码的详细信息:

主要在《AMD64架构程序员手册第三卷:通用和系统指令》中。我确信英特尔的 x64 架构技术参考资料将包含相同的信息(甚至可能更容易理解)。

于 2011-01-25T21:43:36.650 回答
3

汇编器(而不是编译器)使用它可以找到的最长 NOP 指令将代码填充到下一个对齐边界。这就是你所看到的。

于 2011-01-25T23:23:28.737 回答
0

我猜这只是分支延迟指令。

于 2011-01-25T20:20:51.207 回答
-3

我相信 nopw 是垃圾 - i 从未在您的程序中读取,因此无需增加它。

于 2011-01-25T21:28:37.483 回答