【问题标题】:AMD64 -- nopw assembly instruction?AMD64 -- nopw 汇编指令?
【发布时间】:2011-06-15 11:07:03
【问题描述】:

在此编译器输出中,我试图了解 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 的填充函数并不特定于无限循环。这是一组具有一系列 NOP 长度的函数,on the Godbolt compiler explorer.

【问题讨论】:

  • 我们只是在这里查看随机垃圾填充吗?
  • 也许吧!我真的不知道!这就是这一切的美妙之处!哇。不过,真的,我从链接中得到处理器将加载一个块作为速度优化的一条指令,尽管感谢jmp,它没有。我才明白它的意思。我知道 0x90 是什么,但我不知道 66 .. .. 是怎么回事,或者为什么它是 72 位长。
  • 这不是这里的原因,但您可能会发现My, what strange NOPs you have! - The Old New Thing 是一个有趣的阅读。

标签: c gcc assembly x86 machine-code


【解决方案1】:

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

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

0x0f 0x1f 是一个 2 字节操作码,用于接收 ModRM 字节的 NOP

0x84ModRM byte,在这种情况下,它表示使用 5 个以上字节的寻址模式。

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

AMD K8 decoders in Agner Fog's microarch pdf:

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


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

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

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

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

【讨论】:

  • Re the ModRM byte 含义...ref.x86asm.net/coder64.html#x0F1F 列出了用于 Hintable NOP 的 ModRM 字节,并参考了以下内容: 1. 参见美国专利 5,701,442 2. sandpile.org -- IA- 32 架构——操作码组。我没有检查这些,但以防万一。
  • 这是一个 NOP,所以 mod/rm 字节不会任何事情。它是指令的一部分,以允许解码器可以快速解码的方式允许大范围的指令长度。在某些 CPU 上解码许多前缀很慢,因此仅重复 66 操作数大小前缀 5 次比使用 SIB + disp32 的寻址模式编码的 mod/rm 要糟糕得多。
  • 所以我可以理解你为什么要将下一个函数的边界推到 16 字节,但为什么必须用有效代码填充它?如果函数在地址 0x000d 处结束(例如,这是 retq 的最后一个字节),则需要 2 个字节的填充才能到达 0x0010。为什么不直接放零呢?在 retq 之后什么都不会执行。我唯一能猜到的就是反汇编程序可以解析它,但是为什么不直接使用 0x90 作为一个字节的 nop 呢?如果填充永远不会被执行,为什么它必须有效地使用内存?
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • i 为我提供了一种在失败时检查堆栈大小的便捷方法。就我有限的知识而言,Gdb 没有“堆栈的打印大小”键。更有趣的是,一旦优化级别提高,编译器会删除它的增量。该程序是故意“疯狂”的。
    • 我的意思是编译器优化了它——因为你从来没有读过 i。
    • 不过,问题不在于这个。问题的关键是为什么nop(此处为nopw)以这种方式出现。标准nop 是 0x90 并且只是重复。将 i 作为未使用的变量放入其中是有目的的,并且在外部有用,即使它没有在代码中触及。
    【解决方案4】:

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

    【讨论】:

    • 它不仅仅是一个分支延迟指令。它也用作填充。尝试使用 objdump -d 关闭 C 程序。在ret指令之后你会发现很多这样的指令
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-27
    • 2011-11-22
    • 2016-04-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多