Стек вызовов также можно назвать стеком кадров.
По принципу 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-битных регистров.