未优化的 clang++ 代码在一个简单的 main() 中生成不需要的“movl $0, -4(%rbp)”

Posted

技术标签:

【中文标题】未优化的 clang++ 代码在一个简单的 main() 中生成不需要的“movl $0, -4(%rbp)”【英文标题】:Unoptimized clang++ code generates unneeded "movl $0, -4(%rbp)" in a trivial main() 【发布时间】:2018-03-15 12:58:12 【问题描述】:

我创建了一个最小的 C++ 程序:

int main() 
    return 1234;

并使用 clang++5.0 编译它,禁用优化(默认 -O0)。 The resulting assembly code is:

  pushq %rbp
  movq %rsp, %rbp
  movl $1234, %eax # imm = 0x4D2
  movl $0, -4(%rbp)
  popq %rbp
  retq

我理解大部分行,但我不理解“movl $0, -4(%rbp)”。似乎程序将一些局部变量初始化为 0。为什么?

什么编译器内部细节导致这个存储与源代码中的任何内容都不对应?

【问题讨论】:

因为您忘记启用优化。使用-O2 “为什么”部分的回答很简单,“因为你在编译时关闭了优化”。如果你禁止编译器优化奇怪的东西,编译器往往会生成奇怪的代码。 IDK,但我们可以将其缩小到 main 是“特殊的”。 godbolt.org/g/fjYKjH。对于具有不同名称的函数,clang 不会这样做。不幸的是,clang -fverbose-asm 没有像 gcc 那样详细地标记每一行,所以我们无法从中看出原因。也许如果我们查看 -O0 模式的 LLVM-IR 或其他编译器内部结构。 @NathanOliver:堆栈金丝雀没有意义。它没有读回它,它位于 RSP 下方的红色区域中。它看起来不像 gcc -fstack-protector-strong 所做的那样。这可能是一些隐藏变量,或者可能是由于clang处理main(void)main(int argc, char**argv)的方式之间存在某种差异而遗留下来的,main很特别。 这可能是因为显然 "到达 main() 函数的末尾等于返回 0" (source)。所以编译器只是针对这种情况进行设置,由于优化关闭,它没有删除该代码。 【参考方案1】:

TL;DR:在未优化的代码中,您的 CLANG++ 为 main 的返回值留出 4​​ 个字节,并根据 C++(包括 C++11)标准将其设置为零。它为不需要它的main 函数生成了代码。这是未优化的副作用。通常,未优化的编译器会生成它可能需要的代码,然后最终不再需要它,并且没有做任何事情来清理它。


因为您使用-O0 进行编译,所以对代码进行的优化非常少(-O0 可能会删除死代码等)。试图理解未优化代码中的工件通常是一种浪费的练习。未优化代码的结果是额外的加载和存储以及原始代码生成的其他工件。

在这种情况下,main 是特殊的,因为在 C99/C11 和 C++ 中,标准有效地表明,当到达main 的外部块时,默认返回值为 0。The C11 standard 表示:

5.1.2.2.3 程序终止

1 如果 main 函数的返回类型是与 int 兼容的类型,则从 初始调用main函数相当于调用exit函数的值 由 main 函数作为其参数返回;11) 到达终止 main 函数返回值 0。如果返回类型与 int 不兼容,则 返回宿主环境的终止状态未指定。

C++11 standard 说:

3.6.1 主要功能

5) main 中的 return 语句具有离开 main 函数的效果(自动销毁任何对象) 存储持续时间)并以返回值作为参数调用 std::exit。 如果控制到了尽头 main没有遇到return语句,效果就是执行

 return 0;

在 CLANG++ 版本中,您使用的是未优化的 64 位代码,默认情况下返回值 0 位于 dword ptr [rbp-4]

问题是您的测试代码有点过于琐碎,无法看到这个默认返回值是如何发挥作用的。这是一个更好的演示示例:

int main() 
    int a = 3;
    if (a > 3) return 5678;
    else if (a == 3) return 42;

此代码通过return 5678return 42; 有两个显式退出点,但函数末尾没有最终的return。如果达到,则默认返回0。如果我们检查godbolt output,我们会看到:

main:                                   # @main
        push    rbp
        mov     rbp, rsp
        mov     dword ptr [rbp - 4], 0        # Default return value of 0
        mov     dword ptr [rbp - 8], 3
        cmp     dword ptr [rbp - 8], 3        # Is a > 3
        jle     .LBB0_2
        mov     dword ptr [rbp - 4], 5678     # Set return value to 5678
        jmp     .LBB0_5                       # Go to common exit point .LBB0_5
.LBB0_2:
        cmp     dword ptr [rbp - 8], 3        # Is a == 3?
        jne     .LBB0_4
        mov     dword ptr [rbp - 4], 42       # Set return value to 42
        jmp     .LBB0_5                       # Go to common exit point .LBB0_5
.LBB0_4:
        jmp     .LBB0_5                       # Extraneous unoptimized jump artifact 
# This is common exit point of all the returns from `main`
.LBB0_5:
        mov     eax, dword ptr [rbp - 4]      # Use return value from memory
        pop     rbp
        ret

正如我们所看到的,编译器生成了一个通用的退出点,它从堆栈地址dword ptr [rbp-4] 设置返回值 (EAX)。在代码的开头,dword ptr [rbp-4] 被显式设置为 0。在更简单的情况下,未优化的代码仍会生成该指令但未被使用。

如果您使用选项 -ffreestanding 构建代码,您应该会看到 main 的默认返回值不再设置为 0。这是因为来自 main 的默认返回值 0 的要求适用于托管环境,而不是独立的环境。

【讨论】:

以上是关于未优化的 clang++ 代码在一个简单的 main() 中生成不需要的“movl $0, -4(%rbp)”的主要内容,如果未能解决你的问题,请参考以下文章

如何关闭llvm中的常量折叠优化

C++学习(三四八)CLang GCC

允许这种浮点优化吗?

为啥 clang 无法展开循环(即 gcc 展开)?

Clang 编译器阶段

编译器:gcc, clang, llvm