【问题标题】:Why stack grows by 16 bytes in this disassembly, when I only have one 4 byte local variable?当我只有一个 4 字节的局部变量时,为什么在这个反汇编中堆栈增长了 16 字节?
【发布时间】:2019-01-16 05:18:45
【问题描述】:

我无法理解为什么编译器选择以我编写的代码的方式偏移堆栈空间。

我正在玩弄 Godbolt 的 Compiler Explorer 以研究 C 调用约定,这时我想出了一个简单的代码,让我对它的选择感到困惑。

代码位于this link。我选择了 GCC 8.2 x86-64,但我的目标是 x86 处理器,这很重要。 Bellow 是 C 代码的转录和 Compiler Explorer 报告的生成程序集。

// C code
int testing(char a, int b, char c) {
    return 42;
}

int main() {
    int x = testing('0', 0, '7');

    return 0;
}
; Generated assembly
testing(char, int, char):
        push    ebp
        mov     ebp, esp
        sub     esp, 8
        mov     edx, DWORD PTR [ebp+8]
        mov     eax, DWORD PTR [ebp+16]
        mov     BYTE PTR [ebp-4], dl
        mov     BYTE PTR [ebp-8], al
        mov     eax, 42
        leave
        ret
main:
        push    ebp
        mov     ebp, esp
        sub     esp, 16
        push    55
        push    0
        push    48
        call    testing(char, int, char)
        add     esp, 12
        mov     DWORD PTR [ebp-4], eax
        mov     eax, 0
        leave
        ret

从现在开始看汇编列,据我了解,第15行负责为局部变量保留堆栈中的空间。问题是我只有一个本地 int 并且偏移量是 16 个字节而不是 4 个。这感觉像是在浪费空间。

这是否与单词对齐有关?但即使是这样,如果通用寄存器的大小是4字节,那么这种对齐不应该是4字节吗?

我看到的另一件奇怪的事情是关于testing 函数的本地chars 的位置。它们似乎在堆栈中每个占用 4 个字节,如第 7-8 行所示,但只有较低的字节被操作。为什么不每个只使用 1 个字节?

这些选择可能是出于好意,我真的很想了解它们的目的(或者是否没有目的)。或者,也许我只是感到困惑,并没有完全理解。

【问题讨论】:

  • 现在想起来,我相信chars的位置确实是因为字对齐。是吗?
  • 为什么不构建优化?
  • 有趣。即使您启用优化并将 args 更改为 volatile char a 等等 (godbolt.org/z/ul--bv),它们仍然会被复制到堆栈上的不同位置,而不是使用 arg 传递槽作为它们的永久位置。而且它们仍然保存在不同的双字中。此外,只有 char 参数被复制,除非您 b++ int。这看起来像是 gcc 错过了优化。标题问题是一个简单的重复问题,已被多次询问(i386 System V ABI 要求在 call 之前进行 16 字节堆栈对齐),但 char 打包有点有趣。跨度>
  • 是什么让你认为编译器不会浪费空间和时间,如果你不允许它优化? (-O0)。实际上,即使您允许它进行优化,是什么让您认为编译器将有足够的时间来计算完美的解决方案? (让我们忽略对于任何中等大小的源代码,甚至可能没有单一的“完美”解决方案)编译器的时间量非常短(秒/分钟),所以它只会划伤可能结果的总量,并且感谢编译器设计,即使是微小的划痕通常也会表现出色,但有点“浪费”......
  • 请将代码放入您的问题中。 Stack Overflow 希望您的所有问题都是独立的,以防外部网站出现故障。

标签: c assembly x86 calling-convention gcc8


【解决方案1】:

因此,正如 @PeterCordes 所述,通过 cmets,我可以确定堆栈增长问题是由于 i386 SystemV ABI 要求造成的。

字符对齐的原因可能是由于 GCC 的默认行为来提高速度,这可能是从 @Ped7g 的评论中推断出来的。虽然不确定,但对我来说这已经是一个足够好的答案了。

【讨论】:

  • 将字节放在单独的单词中对我来说似乎是一个错过的优化。 GCC 不会对本地人执行此操作,仅对 args 执行此操作,因此可能只是它们的一些剩余 gcc 内部属性最初作为填充到堆栈上的 dwords 的字节传递。由于-O0,或者如果您使用volatile,它只会将它们首先复制到函数自己的堆栈帧中。无论如何,当它们都在同一个高速缓存行中时,字节加载+存储在同一个双字中没有性能差异,英特尔或 AMD CPU 上的 AFAIK。见agner.org/optimize
  • 另见Can modern x86 hardware not store a single byte to memory?(实际上x86可以,其他一切也可以)。还有stackoverflow.com/tags/x86/info中的其他链接
【解决方案2】:

如今,以这种大小的倍数获取堆栈空间很常见,原因如下:

  • 缓存行通过在缓存中维护整个数据来支持这种行为。
  • 预分配临时空间,避免在 cpu 需要一些临时存储时使用推送和弹出指令。
  • 单独的 push 和 pop 指令会降低流水线的执行速度,因为它要求在执行下一条指令之前更新数据。这解耦了连续指令之间的数据依赖关系,让它们运行得更快。

出于这个原因,实际的编译器会指定要以这种方式设计的 ABI。

【讨论】:

  • Intel 自 Pentium-M 和 AMD 自 Bulldozer 以来都有一个“堆栈引擎”来跟踪 ESP/RSP 的更新,从而避免了来自多个 push/pop 指令的堆栈指针的数据依赖链。 (并且让它们成为单指令指令,这与早期的 P6 系列 CPU 不同)。这就是为什么 gcc 过去使用 mov 存储/加载来保存/恢复寄存器,以及将函数 args 写入堆栈(如果有的话),但是现在在为现代 CPU 进行调整时,它再次使用 push/pop。 What is the stack engine in the Sandybridge microarchitecture?
  • call 之前保持堆栈对齐可以让函数廉价地分配对齐的存储空间(例如,用于 SSE 向量)。在分配更多堆栈时,编译器通常选择始终以 16 字节堆栈对齐为目标。但实际上,他们没有将 push 用于 initialized 本地人,这实际上是一个错过的优化,他们将立即溢出。例如,请参阅我在 What C/C++ compiler can use push pop instructions for creating local variables, instead of just increasing esp once? 上的回答。但展开信息需要每个 ESP/RSP 更改的元数据。
猜你喜欢
  • 2017-01-06
  • 2011-11-29
  • 1970-01-01
  • 2023-01-13
  • 2020-07-21
  • 2016-05-29
  • 1970-01-01
  • 2015-09-29
  • 1970-01-01
相关资源
最近更新 更多