Как alloca() взаимодействовала с другими распределениями стека?
Начнем с простого примера распределения стека:
void f() {
int a, b;
...
}
Если я правильно понимаю. Тогда адрес и имеет фиксированное смещение от базы стека, а именно регистра.ebp
. Вот как компилятор найдет их, если они нам понадобятся впоследствии.
Но рассмотрим следующий код.
void f(int n) {
int a;
alloca(n);
int b;
...
}
Если компилятор не выполняет никакой оптимизации, стек будетa->n->b
. Теперь смещение зависит от . Что тогда сделал компилятор?
Подражание Как alloca() работает на уровне памяти?. Я попробовал следующий код:
#include <stdio.h>
#include <alloca.h>
void foo(int n)
{
int a;
int *b = alloca(n * sizeof(int));
int c;
printf("&a=%p, b=%p, &c=%p\n", (void *)&a, (void *)b, (void *)&c);
}
int main()
{
foo(5);
return 0;
}
Результат:&a=0x7fffbab59d68, b=0x7fffbab59d30, &c=0x7fffbab59d6c
. На этот раз адресa
и выглядит соседским. Компилятор сделал какое-то изменение порядка? И если мы не позволим компилятору изменить порядок, как компилятор найдет адресc
?
------------Некоторые обновления------------
Хорошо, если вы верите, что компиляторы действительно имеют значение, давайте попробуем x86-64 gcc 13.2, я немного изменил код.
#include <alloca.h>
void alloca_test(int n) {
int a;
int* ptr = (int *) alloca(n);
int b;
a++;
b++;
ptr[0]++;
}
и вот сборка:
alloca_test(int):
push rbp
mov rbp, rsp
sub rsp, 48
mov DWORD PTR [rbp-36], edi
mov DWORD PTR [rbp-4], 0
mov eax, DWORD PTR [rbp-36]
cdqe
lea rdx, [rax+8]
mov eax, 16
sub rax, 1
add rax, rdx
mov ecx, 16
mov edx, 0
div rcx
imul rax, rax, 16
sub rsp, rax
mov rax, rsp
add rax, 15
shr rax, 4
sal rax, 4
mov QWORD PTR [rbp-16], rax
mov DWORD PTR [rbp-20], 0
add DWORD PTR [rbp-4], 1 <--a++
add DWORD PTR [rbp-20], 1 <--b++
mov rax, QWORD PTR [rbp-16]
mov eax, DWORD PTR [rax]
lea edx, [rax+1]
mov rax, QWORD PTR [rbp-16]
mov DWORD PTR [rax], edx
nop
leave
ret
Здесьb
имеет адрес[rbp-20]
, которые не меняются относительноn
1 ответ
На самом деле, для действительно неоптимизирующего компилятора все было бы наоборот.
Давайте представим себе наивный компилятор, созданный в стиле 1970-х годов, поскольку язык C изначально был разработан для реализации такими компиляторами. И давайте пока проигнорируем этот призыв. Когда компилятор анализирует функцию, каждый раз, когда он встречает определение локальной переменной [*], он назначает ей слот стека относительно указателя кадра: soat, at и т. д., и использует его для обращения к переменной. Когда он завершает анализ, он видит все локальные переменные и может вычислить общий объем необходимого стека и поэтому вставить соответствующую константу в код пролога, который корректирует указатель стека (например,
Что касается , то он не был частью исходного языка C. Скорее, это был, по сути, «крутой хак», который кто-то обнаружил, и который можно реализовать способом, полностью ортогональным описанному выше процессу. Мы можем описать эту идею следующим образом в терминах x86. (Первоначальная реализация была бы для PDP-11 или VAX или чего-то подобного, но идея та же.) Поскольку все локальные переменные адресуются относительно , то не имеет значения, будет ли указатель стека уменьшаться дальше во время выполнение функции; компилятор никогда не ссылается на . А очистка стека в эпилоге функции обычно реализуется как
Так что на самом деле компилятору даже не обязательно об этом знать.
Фактически, если вы готовы немного больше манипулировать стеком, это может быть даже внешняя библиотечная функция, которую компилятор обрабатывает как любой другой вызов функции. Вот почему синтаксис вызова функции не интегрирован более глубоко в язык — исходная реализация была просто вызовом функции.
Итак, с этой реализацией у нас будет
То, что компилятор не понимал, могло что-то сломать, если оно было выполнено в середине передачи аргументов для вызова другой функции, так что, по-видимому,
Как вы можете себе представить, этот «крутой хак» перестал работать так хорошо, когда компиляторы стали умнее и захотели иметь возможность фактически контролировать указатель стека во время выполнения функции. Тогда изменение указателя стека за спиной компилятора как бы становится катастрофическим, поэтому пришлось предоставлять специальную поддержку компилятора, причиняя много боли авторам компиляторов. Идея в конечном итоге была полностью принята в языке, когда в C99 были представлены массивы переменной длины, но многие наблюдатели считают это решение ошибкой.
Что касается того, что делает современный компилятор, однозначного ответа не существует. Он точно знает, какая семантика
[*] Заметим, кстати, что при назначении слотов стека и вычислении использования стека блочная структура функции обычно сглаживается, а области видимости переменных не принимаются во внимание. Итак, даже в коде типа
int a;
if (...) {
int b;
...
}
while (...) {
int c;
...
}
Здесь