Почему компилятор резервирует немного стекового пространства, но не весь размер массива?

Следующий код

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 байтов... Почему это так?

2 ответа

Решение

Ниже области стека, используемой функцией, находится 128-байтовая красная зона, зарезервированная для использования программой. поскольку main не вызывает никакой другой функции, ему не нужно перемещать указатель стека больше, чем нужно, хотя в данном случае это не имеет значения. Я только достаточно вычитает из rsp чтобы убедиться, что массив защищен красной зоной.

Вы можете увидеть разницу, добавив вызов функции main

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

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

Это дает:

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, потому что ей нужно, чтобы массив находился в своем стековом пространстве, но тестировать не нужно, потому что она не вызывает никаких функций.

Дополнительное использование элементов массива существенно не меняет вывод, но оно было добавлено, чтобы прояснить, что это не делает вид, что эти элементы не существуют.

Вы работаете в Linux x86-64, где ABI включает красную зону (128 байт ниже RSP). https://stackru.com/tags/red-zone/info.

Таким образом, массив идет от нижней части красной зоны до вершины того, что зарезервировано для gcc. Компилировать с -mno-red-zone чтобы увидеть другой код-ген.

Кроме того, ваш компилятор использует RSP, а не ESP. ESP - это младшие 32 бита RSP, а x86-64 обычно имеет RSP за пределами младших 32 битов, поэтому он потерпит крах, если вы урежете RSP до 32 бит.


На проводнике компилятора Godbolt я получаю это от 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 или какого-либо другого компилятора действительно загружается извне красной зоны при этом неопределенном поведении (чтение неинициализированного элемента массива)? лязг просто компилирует 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 вместо встроенного асма).

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.

В целом это хорошо: большая часть массива находится в пределах диапазона disp8 от RSP, так что ссылка на arr[0] вплоть до arr[63 или так можно использовать [rsp+disp8] вместо [rsp+disp32] режимы адресации. Не очень полезно для одного большого массива, но в качестве общего алгоритма размещения локальных объектов в стеке имеет смысл. (gcc не доходит до нижней части красной зоны для arr, но clang делает, используя sub rsp, 360 вместо 368, так что массив все еще выровнен по 16 байтов. (IIRC, x86-64 System V ABI, по крайней мере, рекомендует это для массивов с автоматическим хранением с размером>= 16 байт.)

Другие вопросы по тегам