为什么编译器会保留少量堆栈空间,而不保留整个数组大小?

时间:2018-07-25 15:52:43

标签: assembly x86-64 red-zone

以下代码

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

编译为:

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

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

2 个答案:

答案 0 :(得分:4)

在函数使用的堆栈区域下面,有一个128-byte red zone保留供程序使用。由于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的原因是它需要将数组放入堆栈空间中,而测试则不需要,因为它不调用任何函数。

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

答案 1 :(得分:1)

您使用的是x86-64 Linux,ABI包含一个红色区域(比RSP低128个字节)。 https://stackoverflow.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仅返回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

但是我们可以避免在叶函数中避免使用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都将数组的底部放在红色区域。请参阅Godbolt链接。

这通常是件好事:更多数组位于RSP的disp8范围内,因此对arr[0]的引用最多为arr[63,因此可以使用{{1 }}而不是[rsp+disp8]寻址模式。对于一个大数组不是超级有用,但是作为一种在堆栈上分配局部变量的通用算法,这很有意义。 (gcc不会一直到arr的红色区域的底部,但是clang会使用[rsp+disp32]而不是368,因此数组仍然是16字节对齐的。(IIRC,x86-64至少对于具有自动存储且大小大于等于16字节的阵列,System V ABI至少建议这样做。)