Почему компилятор резервирует немного стекового пространства, но не весь размер массива?
Следующий код
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 байт.)