为什么编译器保留一点堆栈 space 而不是整个数组大小?

Why does the compiler reserve a little stack space but not the whole array size?

下面的代码

int main() {
  int arr[120];
  return arr[0];
}

编译成这样:

  sub     rsp, 360
  mov     eax, DWORD PTR [rsp-480]
  add     rsp, 360
  ret

知道整数是 4 个字节,数组的大小是 120,数组应该占用 480 个字节,但从 ESP 中只减去 360 个字节...这是为什么?

在一个函数使用的栈区下面,有一个是预留给程序使用的。由于 main 不调用其他函数,因此不需要将堆栈指针移动超过它需要的位置,尽管在这种情况下这无关紧要。它只是从rsp中减去足够的,以确保数组被红色区域保护。

您可以通过添加对 main

的函数调用来查看差异
int test() {
  int arr[120];
  return arr[0]+arr[119];
}

int main() {
  int arr[120];
  test();
  return arr[0]+arr[119];
}

This gives:

test:
  push rbp
  mov rbp, rsp
  sub rsp, 360
  mov edx, DWORD PTR [rbp-480]
  mov eax, DWORD PTR [rbp-4]
  add eax, edx
  leave
  ret
main:
  push rbp
  mov rbp, rsp
  sub rsp, 480
  mov eax, 0
  call test
  mov edx, DWORD PTR [rbp-480]
  mov eax, DWORD PTR [rbp-4]
  add eax, edx
  leave
  ret

您可以看到 main 函数减去 480,因为它需要数组在其堆栈中 space,但测试不需要,因为它不调用任何函数.

数组元素的额外使用不会显着改变输出,但添加它是为了清楚表明它并不是假装这些元素不存在。

您使用的是 x86-64 Linux,其中 ABI 包含一个红色区域(RSP 以下 128 字节)。 https://whosebug.com/tags/red-zone/info.

因此数组从红色区域的底部向上接近 gcc 保留的顶部。使用 -mno-red-zone 编译以查看不同的代码生成。

此外,您的编译器使用的是 RSP,而不是 ESP。 ESP 是 RSP 的低 32 位,而 x86-64 通常在低 32 位之外具有 RSP,因此如果将 RSP 截断为 32 位,它会崩溃。


Godbolt compiler explorer 上,我从 gcc -O3(使用 gcc 6.3、7.3 和 8.1)得到这个:

main:
    sub     rsp, 368
    mov     eax, DWORD PTR [rsp-120]   # -128, not -480 which would be outside the red-zone
    add     rsp, 368
    ret

您是否伪造了您的 asm 输出,或者某些其他版本的 gcc 或某些其他编译器是否真的从红区外加载了这种未定义的行为(读取未初始化的数组元素)? clang 只是将它编译为 ret,而 ICC 只是 returns 0 而没有加载任何东西。 (未定义的行为是不是很好玩?)


int ext(int*);
int foo() {
  int arr[120];     // can't use the red-zone because of later non-inline function call
  ext(arr);
  return arr[0];
}
   # gcc.  clang and ICC are similar.
    sub     rsp, 488
    mov     rdi, rsp
    call    ext
    mov     eax, DWORD PTR [rsp]
    add     rsp, 488
    ret

但是我们可以在不让编译器优化掉 store/reload 的情况下避免叶函数​​中的 UB。 (我们也许可以只使用 volatile 而不是内联 asm)。

int bar() {
  int arr[120];
  asm("nop # operand was %0" :"=m" (arr[0]) );   // tell the compiler we write arr[0]
  return arr[0];
}

# gcc output
bar:
    sub     rsp, 368
    nop # operand was DWORD PTR [rsp-120]
    mov     eax, DWORD PTR [rsp-120]
    add     rsp, 368
    ret

注意编译器假设我们写了arr[0],而不是arr[1..119].

但不管怎么说,gcc/clang/ICC都把数组底部放在了红区。见神箭 link.

总的来说这是一件好事:数组的更多部分在 RSP 的 disp8 范围内,因此参考 arr[0]arr[63 左右可以使用 [rsp+disp8] 而不是 [rsp+disp32] 寻址模式。对于一个大数组来说不是很有用,但作为一种在堆栈上分配局部变量的通用算法,它是完全有意义的。 (gcc 并没有一直走到 arr 的红色区域底部,但是 clang 做到了,使用 sub rsp, 360 而不是 368 所以数组仍然是 16 字节对齐的。(IIRC,x86-64 System V ABI 至少建议将其用于自动存储大小 >= 16 字节的数组。)