【问题标题】:Why does main initialize stack frame when there are no variables为什么没有变量时main初始化堆栈帧
【发布时间】:2019-01-01 01:23:04
【问题描述】:

为什么会有这样的代码:

#include "stdio.h"
int main(void) {
    puts("Hello, World!");
}

决定初始化一个堆栈帧?这是汇编代码:

.LC0:
        .string "Hello, World!"
main:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        mov     eax, 0
        pop     rbp
        ret

为什么编译器初始化一个堆栈帧只是为了以后销毁它,而没有使用它?这肯定不会在主函数外部造成任何错误,因为我从不使用堆栈,所以我不会导致任何错误。为什么会这样编译?

【问题讨论】:

  • 编译器设置的优化级别是什么?
  • 很可能是因为您编译时没有优化,请尝试使用-O4 标志(假设您使用 gcc 或 clang)
  • 如何编译最优化?
  • @Riolku 取决于您的编译器。使用 gcc 和 clang,-O3 应该可以解决问题。

标签: c assembly compiler-construction compiler-optimization


【解决方案1】:

在每个已编译函数中包含这些步骤是未优化的编译器的“基线”。它在拆卸时看起来很干净,并且有道理。但是,编译器可以优化输出以减少没有实际效果的代码的开销。您可以通过使用不同的优化级别进行编译来看到这一点。

你得到的就像this:

.LC0:
  .string "Hello, World!"
main:
  push rbp
  mov rbp, rsp
  mov edi, OFFSET FLAT:.LC0
  call puts
  mov eax, 0
  pop rbp
  ret

这是在 GCC 中编译的,没有优化。

添加标志 -O4 给出this 输出:

.LC0:
  .string "Hello, World!"
main:
  sub rsp, 8
  mov edi, OFFSET FLAT:.LC0
  call puts
  xor eax, eax
  add rsp, 8
  ret

您会注意到这仍然会移动堆栈指针,但它会跳过更改基指针,并避免与此相关的耗时的内存访问。

假定堆栈在 16 字节边界上对齐。随着返回地址被压入,这会在函数调用之前再减去 8 个字节以到达边界。

【讨论】:

  • @Riolku 这是为了按照 ABI 的要求将堆栈对齐到 16 个字节,以便调用 puts
  • @ThomasJager 为什么选择-O4 而不仅仅是-O3? AFAIK 任何大于 3 的数字都没有进一步的影响。
  • 如果godbolt.org/g/vEKY78 有任何迹象,他们肯定会在 4.4.7 之前加入。 -mpreferred-stack-boundary=2 将代码生成更改为不在仅调用单参数函数的非叶函数中保留尽可能多的堆栈,即使在 4.1.2 中也是如此。并且 4.4.7 可以正确处理 __attribute__((aligned(32)))(与 4.1 不同)。无论如何,这一切都是为了-m32;我不认为 -m64 做过少于 16 字节的对齐。
  • @PeterCordes 查看 GCC 发布历史 4.5.0 是在 4.4.7 发布前 2 年发布的。我敢打赌 4.4.7 包含对默认堆栈边界的更改
  • @Riolku:尾调用只能在void 函数中,如void foo(){puts("msg");},或者如果你在main 中使用return puts("msg");。否则,main 末尾的隐式 return 0 意味着不可能进行优化的尾调用。
【解决方案2】:

编译器以尽可能最简单的方式生成未优化的代码(或者至少是不会导致代码糟糕到优化器无法修复的最不复杂的方式)以保持代码简单并坚持单一职责原则(从某种意义上说,使代码更高效是优化器的工作)。

生成代码以初始化所有函数的堆栈比仅在必要时这样做更简单。由于优化器无论如何都能够删除不必要的代码(而且它在更多的情况下会这样做,而不是简单的“这个函数是否有任何局部变量?”检查会),生成不必要的代码不会有任何影响由于启用了优化(如果未启用,则预计生成的代码将包含低效率)。

如果我们确实添加了“这个函数有任何局部变量吗?”检查生成堆栈初始化代码的函数,我们将重新发明优化器已经执行的功能较弱的优化版本,因此我们将违反单一职责原则并增加复杂性编译器的一部分,否则可能相对简单(与优化器相反,优化器无论如何都充满了复杂的算法)。

【讨论】:

    【解决方案3】:

    堆栈帧使得在运行时检查调用堆栈成为可能。这很有用:

    正如其他人已经指出的那样,编译器可能会在更高的优化级别上省略堆栈帧。
    也可以看看: How do you get gcc's __builtin_frame_address to work with -O2?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-29
      • 2014-08-23
      • 2012-02-23
      • 2014-12-15
      • 2021-12-29
      • 1970-01-01
      相关资源
      最近更新 更多