【问题标题】:Stack allocation, why the extra space?堆栈分配,为什么会有多余的空间?
【发布时间】:2012-04-09 08:38:15
【问题描述】:

为了更好地掌握调用约定和堆栈的处理方式,我进行了一些尝试,但我不明白为什么 main 在设置堆栈时分配了三个额外的双字(<main+0>)。它既不与 8 个字节对齐,也不与 16 个字节对齐,所以据我所知,这不是原因。如我所见,main 需要 12 个字节用于 func 和返回值的两个参数。

我错过了什么?

该程序是在 x86 架构上使用“gcc -ggdb”编译的 C 代码。

编辑:我从 gcc 中删除了 -O0 标志,它对输出没有任何影响。

(gdb) disas main
Dump of assembler code for function main:
    0x080483d1 <+0>:    sub    esp,0x18
    0x080483d4 <+3>:    mov    DWORD PTR [esp+0x4],0x7
    0x080483dc <+11>:   mov    DWORD PTR [esp],0x3
    0x080483e3 <+18>:   call   0x80483b4 <func>
    0x080483e8 <+23>:   mov    DWORD PTR [esp+0x14],eax
    0x080483ec <+27>:   add    esp,0x18
    0x080483ef <+30>:   ret    
End of assembler dump.

编辑:当然我应该发布 C 代码:

int func(int a, int b) {
    int c = 9;
    return a + b + c;
}

void main() {
    int x;
    x = func(3, 7);
}

平台是 Arch Linux i686。

【问题讨论】:

  • 贴出 C 代码可能会有所帮助
  • 该平台也很有用,因为您正在询问调用约定。例如,Mac OS X 要求堆栈在 16 字节边界上保持对齐。
  • 最好假设当您禁用优化时,您最终会看到未优化的代码。
  • @HansPassant 当然可以,但在这种特殊情况下没有区别。我没有它重新编译,反汇编是一样的。
  • 嗯,这不可能。优化后你应该只剩下ret。该代码没有副作用,因此可以完全消除。

标签: gcc assembly x86 stack memory-alignment


【解决方案1】:

当你进入函数时,函数的参数(包括但不限于main)已经在堆栈中。您在函数内分配的空间用于局部变量。对于具有简单返回类型的函数,例如int,返回值通常在寄存器中(eax,在 x86 上使用典型的 32 位编译器)。

例如,如果main 是这样的:

int main(int argc, char **argv) { 
   char a[35];

   return 0;
}

...当我们进入 main 为 a 腾出空间时,我们希望看到至少 35 个字节分配在堆栈上。假设是 32 位实现,通常会向上舍入到 4 的下一个倍数(在本例中为 36),以保持堆栈的 32 位对齐。我们不希望看到为返回值分配任何空间。 argcargv 将在堆栈上,但在输入 main 之前它们已经在堆栈上,因此 main 无需为它们分配空间。

在上述情况下,在为 a 分配空间后,a 通常会从 [esp-36] 开始,argv 会在 [esp-44]argc 会在 [esp-48](或者这两个可能会被颠倒——取决于参数是从左到右还是从右到左)。如果您想知道我为什么跳过[esp-40],那将是返回地址。

编辑:这是进入函数时的堆栈图,以及设置堆栈框架后:

编辑 2:根据您更新的问题,您所拥有的内容有些迂回,但并不是特别难以理解。进入main 后,它不仅为main 的本地变量分配空间,还为您传递给从main 调用的函数的参数分配空间。

这至少占了分配的部分额外空间(尽管不一定是全部)。

【讨论】:

  • 但是 main() 只有三个本地整数,12 个字节。那为什么要分配 24 个字节呢?
  • @spektre:剩下的就是填充,将堆栈对齐到 16 个字节。尝试修改 func() 以使用更少/更多参数。您会注意到main() 的堆栈区域以 16 字节的增量变化(使用更多参数,您最终会看到 sub esp,0x28,然后使用更多参数,它将更改为 sub esp,0x38,...)。跨度>
  • @ninjalj 对齐是我想到的第一件事,但它并没有加起来,因为我假设 esp 在输入 main() 时已经对齐。尽管现在我想起来,如果是的话,对齐就永远没有必要了。那为什么一开始就不对齐呢?在进入main() 之前运行的代码肯定会使用堆栈,或者该代码是否严格位于另一个上下文中?
【解决方案2】:

这是对齐。出于某种原因,我假设 esp 从一开始就会对齐,但显然不是。

gcc 默认将堆栈帧对齐为 16 个字节,这就是发生的情况。

【讨论】:

  • 堆栈在call 指令之前对齐了 16 个,因此 args 是 16B 对齐的。如果堆栈在进入main 之前没有已知的对齐方式,gcc 将发出使用and esp, -16 来对齐它的代码。