gcc inline asm 中 data32 data32 nopw %cs:0x0(%rax,%rax,1) 指令的含义是什么?

What is the meaning of the data32 data32 nopw %cs:0x0(%rax,%rax,1) instruction in gcc inline asm?

虽然 运行 对 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>

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

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

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

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

然后指令#2 的加载和解码可以在执行#1 时发生。

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

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

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

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

Here你可以从流水线中分支指令的处理中读到很多。

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

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

这仍然是未定义的行为,但这次没有观察到任何 "bad"。

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

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

回答标题中的问题,说明

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

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

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

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