【问题标题】:What is the meaning of the data32 data32 nopw %cs:0x0(%rax,%rax,1) instruction in gcc inline asm?gcc inline asm中的data32 data32 nopw %cs:0x0(%rax,%rax,1)指令是什么意思?
【发布时间】:2015-07-04 12:02:18
【问题描述】:

在为 gcc 编译器的 -O2 优化运行一些测试时,我在函数的反汇编代码中观察到以下指令:

data32 data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)

这条指令有什么作用?

更详细地说,我试图了解编译器如何通过 O2 优化来优化无用的递归,如下所示:

int foo(void)
{
   return foo();
}
int main (void)
{
   return foo();
}

上面的代码在没有优化的情况下编译会导致堆栈溢出,但适用于O2优化的代码。

我认为使用 O2 它完全消除了函数 foo 的推送堆栈,但为什么需要 data32 data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)

0000000000400480 <foo>:
foo():
400480:       eb fe                   jmp    400480 <foo>
400482:       66 66 66 66 66 2e 0f    data32 data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)
400489:       1f 84 00 00 00 00 00

0000000000400490 <main>:
main():
400490:       eb fe                   jmp    400490 <main>

【问题讨论】:

  • 它似乎不是一条指令,而是(随机?)数据,反汇编程序仍然试图解析这些数据。 (66 是“数据大小”前缀。)顺便说一句,优化用尾部跳转替换了嵌套调用。 jmp 是函数 foo 中的唯一代码。
  • @Jongware 我认为管道优化需要它。
  • 就是把函数对齐到16个字节。参见例如agner.org/optimize/optimizing_assembly.pdf 中的 代码对齐 部分。作为评论发布,因为有人可能会解释为什么选择这些特定的填充值。至少我猜你会想要一些不会混淆反汇编程序的值,所以也许这就是它的全部。
  • @peterh:等一下……看看main!似乎最明显的优化是放弃整个函数foo。 (但我仍然觉得数据不是本应执行的代码的一部分。)
  • @Jongware 是的,但是尽管foo 需要编译并存在于二进制文件中,因为从C 的角度来看,它没有被声明为static。因此,不能保证有人不想从外部对象调用foo()。我不知道为什么在main() 之后没有发生类似的优化 - 也许它发生了,但 OP 没有在这里复制它。

标签: c gcc assembly optimization x86


【解决方案1】:

您会看到对 cpu 管道的 operand forwarding 优化。

虽然它是一个空循环,但 gcc 也尝试对此进行优化 :-)。

您正在运行的 CPU 具有 superscalar 架构。这意味着,它有一个管道,并且连续指令执行的不同阶段并行发生。例如,如果有一个

mov eax, ebx ;(#1)
mov ecx, edx ;(#2)

那么指令 #2 的加载和解码可能已经在 #1 执行时发生了。

在分支的情况下,流水线有很大的问题需要解决,即使它们是无条件的。

例如,当jmp 正在解码时,下一条指令已经预取到流水线中。但是jmp 改变了下一条指令的位置。在这种情况下,管道需要清空和重新填充,并且会丢失很多有价值的 cpu 周期。

在这种情况下,如果管道中充满了无操作,看起来这个空循环会运行得更快,尽管它永远不会被执行。它实际上是对 x86 管道的一些不常见特性的优化。

早期的 dec alphas 甚至可以从这些东西中产生段错误,并且空循环中必须有很多无操作。 x86 只会更慢。这是因为它们必须与 intel 8086 兼容。

Here你可以从管道中分支指令的处理中读到很多。

【讨论】:

  • 对于条件分支我同意,但无条件分支通常在非常早的阶段解码(如果可能)并导致从跳转目标的地址而不是线性地址获取代码。此外,一些架构延迟了分支。即使采用分支以避免流水线停顿,它们也会执行分支的紧随其后的指令。 MIPS 就是一个典型的例子,A​​lphas 的问题也很可能由此产生。
  • 还有一点:超标量架构不是管道的要求。反过来说:是的。但是,几乎所有 CPU 架构都可以(并且实际上)使用管道,包括一些 70 年代后期的 8/16 位。超标量(粗略)是指在相同执行阶段同时拥有多条指令的能力。
  • @Olaf 非常感谢您的信息!顺便说一句,是否有一个超标量但没有管道的 CPU,或者这只是理论上的可能性?我认为,连续指令的并发执行需要依赖分析,这应该发生在执行之前
  • 操作数转发与此无关。此外,几乎所有现代 x86 CPU 都可以在每次迭代 1 个周期时运行短循环,这是一种特殊情况,即使正常的分支吞吐量小于每个时钟一个周期也是如此。最近,CPU 甚至具有循环缓冲区,可在重复重新运行同一块时优化解码。英特尔因为 Nehalem 根本不重新解码,而是从提供问题阶段 (LSD) 的队列中回收解码后的微指令。见agner.org/optimize
  • nop 跟在 jmp 之后并不是使循环运行得快的必要条件。这恰好是 GNU 汇编器用于 .p2align 指令的内容,而 gcc 不会在填充从未执行的情况下覆盖它。一些编译器使用int3 或类似的错误指令在函数之间填充,但当然仍然使用nop 来对齐函数内循环的顶部。 任何不停止解码器的东西都可以,因为nop 甚至不会发出问题(除了可能在分支预测检测到 jmp 之前在某些 CPU 上出现一次)
【解决方案2】:

要回答标题中的问题,说明

data32 data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)

是一个 14 字节的 NOP(无操作)指令,用于填充 foo 函数和 main 函数之间的间隙以保持 16 字节对齐。

x86 架构有大量不同大小的 NOP 指令,可用于将填充插入到可执行段中,这样如果 CPU 最终执行它们,它们将不起作用。然后Intel optimization manual 包含有关可用作填充的不同长度的推荐 NOP 编码的信息。

在这种特定情况下,它是完全不相关的,因为 NOP 永远不会被执行(甚至在无条件跳转之后被解码),因此编译器可以填充它想要的任何随机垃圾。

【讨论】:

  • 它将被推测解码。如果它的解码速度很慢,它可能会减慢 jmp 的解码速度,因为它们可能是一起解码的。 (大多数现代 CPU 一次最多解码 4 条指令,最高可达 16B(过于简单,请参阅agner.org/optimize 了解详细信息)。)
  • 这应该是公认的答案。 @PeterCordes - 我实际上并不清楚指令通常会被解码。分支目标预测器必须很早就通知前端在jmp 处有一个分支,并且它不能在解码这些指令的路径上走得太远。几乎可以肯定,它不会将解码器留在 IDQ 中(因为这将不得不撤消),而且它似乎很可能永远不会进入预解码器和解码器之间的缓冲区。也许甚至预解码器也不解码字节。
  • 我不确定该链的哪个部分会减速,我记得至少有一次建议放置 ud2 或在代码的开头不是真正的代码告诉解码器“这里有龙”。
  • @BeeOnRope:我认为 x86 代码块会影响解码器,并且可能会导致停顿,即使稍后丢弃无条件分支后的所有内容的解码结果也是如此。我的猜测是任何并行解码器都可以设置丢弃剩余信号,并为ud2int *jmpcall 等设置。同意他们不会被添加到 IDQ 中。但是关于分支预测究竟在哪里进入它的好问题。也许一旦core2正确检测到jmp,它就会重复jmp指令并向解码器提供4个jmp的块?早期的 P6 没有循环缓冲区。
  • @BeeOnRope:对。真正的版本是一个以jmp 结尾的非空循环,后跟垃圾(其中退出条件是通常不采用的jcc)。前向分支是一个有趣的甚至更普遍的情况。
【解决方案3】:

函数 foo() 是一个没有终止的无限递归。如果不进行优化,gcc 会生成正常的子程序调用,其中至少包括返回地址的堆栈。由于堆栈是有限的,这会造成堆栈溢出,即_undefined_behaviour_。

如果优化,gcc 检测到 foo() 根本不需要堆栈帧(没有参数或局部变量)。它还检测到, foo() 立即返回给调用者(也将是 foo())。这称为尾链:在函数末尾的函数调用(即显式/隐式返回)被转换为跳转到该函数,因此不需要堆栈。

这仍然是未定义的行为,但这次没有观察到“坏”。

请记住:未定义包括致命行为和预期行为(但这只是偶然)。具有不同优化级别的行为不同的代码应该始终是错误的。 有一个例外:时间。这不受 C 语言标准(大多数其他语言都没有)的约束。

正如其他人所说,data32 ... 非常确定填充以获得 16 字节对齐,这可能是内部指令总线和/或缓存线的大小。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-12-12
    • 1970-01-01
    • 2020-02-16
    • 2017-10-14
    • 2017-05-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多