Стек вызовов также можно назвать стеком кадров.
По принципу LIFO складываются не локальные переменные, а целые кадры стека («вызовы») вызываемых функций . Локальные переменные вставляются и выталкиваются вместе с этими кадрами в так называемом прологе функции и эпилоге. соответственно.
Внутри фрейма порядок переменных полностью не определен; Компиляторы «переупорядочивают» позиции локальных переменных внутри кадра соответствующим образом, чтобы оптимизировать их выравнивание, чтобы процессор мог получить их как можно быстрее. Важным фактом является то, что смещение переменных относительно некоторого фиксированного адреса является постоянным на протяжении всего времени существования кадра. поэтому достаточно взять адрес привязки, скажем, адрес самого кадра, и работать со смещениями этого адреса до переменные. Такой адрес привязки фактически содержится в так называемом указателе базы или кадра.который хранится в регистре EBP. С другой стороны, смещения четко известны во время компиляции и поэтому жестко закодированы в машинный код.
Этот рисунок из Википедии показывает, как устроен типичный стек вызовов 1 :

Добавьте смещение переменной, к которой мы хотим получить доступ, к адресу, содержащемуся в указателе кадра, и мы получим адрес нашей переменной. Короче говоря, код просто обращается к ним напрямую через постоянные смещения времени компиляции от базового указателя; Это простая арифметика с указателями.
пример
#include <iostream>
int main()
{
char c = std::cin.get();
std::cout << c;
}
gcc.godbolt.org дает нам
main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl std::cin, %edi
call std::basic_istream<char, std::char_traits<char> >::get()
movb %al, -1(%rbp)
movsbl -1(%rbp), %eax
movl %eax, %esi
movl std::cout, %edi
call [... the insertion operator for char, long thing... ]
movl $0, %eax
leave
ret
.. для main. Я разделил код на три подраздела. Пролог функции состоит из первых трех операций:
- Базовый указатель помещается в стек.
- Указатель стека сохраняется в базовом указателе
- Указатель стека вычитается, чтобы освободить место для локальных переменных.
Затем cinперемещается в регистр EDI 2 иget вызывается; Возвращаемое значение - в EAX.
Все идет нормально. Теперь происходит интересное:
Младший байт EAX, обозначенный 8-битным регистром AL, берется и сохраняется в байте сразу после базового указателя : то есть -1(%rbp)смещение базового указателя равно -1. Этот байт - наша переменнаяc . Смещение отрицательное, потому что стек растет вниз на x86. Следующая операция сохраняет cв EAX: EAX перемещаются ESI, coutперемещаются в ЭОД , а затем оператор вставки вызываются coutи cбыть аргументы.
В заключение,
- Возвращаемое значение
mainсохраняется в EAX: 0. Это из-за неявного returnоператора. Вы также можете увидеть xorl rax raxвместоmovl .
- уйти и вернуться на место вызова.
leaveсокращает этот эпилог и неявно
- Заменяет указатель стека на базовый указатель и
- Выдвигает указатель базы.
После того, как эта операция retбыла выполнена, фрейм фактически выталкивается, хотя вызывающей стороне все равно нужно очистить аргументы, поскольку мы используем соглашение о вызовах cdecl. Другие соглашения, например stdcall, требуют от вызываемого объекта наведения порядка, например, путем передачи количества байтов в ret.
Пропуск указателя кадра
Также возможно использовать смещения не от указателя базы / кадра, а от указателя стека (ESB). Это делает регистр EBP, который в противном случае содержал бы значение указателя кадра, доступным для произвольного использования, но это может сделать невозможной отладку на некоторых машинах и будет неявно отключен для некоторых функций . Это особенно полезно при компиляции для процессоров с небольшим количеством регистров, включая x86.
Эта оптимизация известна как FPO (пропуск указателя кадра) и устанавливается -fomit-frame-pointerв GCC и -Oyв Clang; обратите внимание, что он неявно запускается при каждом уровне оптимизации> 0 тогда и только тогда, когда отладка все еще возможна, поскольку она не требует никаких дополнительных затрат. Для получения дополнительной информации см. Здесь и здесь .
1 Как указано в комментариях, указатель кадра предположительно предназначен для указания адреса после адреса возврата.
2 Обратите внимание, что регистры, начинающиеся с R, являются 64-битными аналогами регистров, которые начинаются с E. EAX обозначает четыре младших байта RAX. Для ясности я использовал названия 32-битных регистров.