Основная структура стековых фреймов
В настоящее время я играю, изучаю стековые фреймы, пытаюсь понять, как это работает. После прочтения нескольких статей, которые всегда объясняли, что общая структура будет:
местные переменные <--- SP
низкий адрес
старый BP
<--- BP
ret addr args высокий адрес
У меня есть пример программы, которая вызывает функцию с тремя аргументами и имеет два буфера в качестве локальных переменных:
#include <stdio.h>
void function(int a, int b, int c);
int main()
{
function(1, 2, 3);
return 0;
}
void function(int a, int b, int c)
{
char buffer1[5];
char buffer2[10];
}
Я взглянул на ассемблерный код программы и был удивлен, что не нашел того, что ожидаю при вызове функции. Я ожидал что-то вроде:
# The arguments are pushed onto the stack:
push 3
push 2
push 1
call function # Pushes ret address onto stack and changes IP to function
...
# In function:
# Push old base pointer onto stack and set current base pointer to point to it
push rbp
mov rbp, rsp
# Reserve space for stack frame etc....
Так что структура фрейма при выполнении функции будет выглядеть примерно так:
buffers <--- SP low address
old BP <--- BP
ret Addr
1
2
3 high address
Но вместо этого происходит следующее:
Вызов функции:
mov edx, 3
mov esi, 2
mov edi, 1
call function
Зачем использовать регистры здесь, когда мы можем просто отправить в стек??? И в фактической функции, которую мы вызываем:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 48
mov DWORD PTR [rbp-36], edi
mov DWORD PTR [rbp-40], esi
mov DWORD PTR [rbp-44], edx
mov rax, QWORD PTR fs:40
mov QWORD PTR [rbp-8], rax
xor eax, eax
mov rax, QWORD PTR [rbp-8]
xor rax, QWORD PTR fs:40
je .L3
call __stack_chk_fail
Насколько я вижу, 48 байтов зарезервированы для кадра стека, верно? И впоследствии, используя регистры из вызова функции, аргументы функции копируются в конец стека. Так это будет выглядеть примерно так:
3 <--- SP
2
1
??
??
old BP <--- BP
return Address
??
Я предполагаю, что буферы находятся где-то между args и old BP
, Но я действительно не уверен, где именно... так как они всего 15 байтов и 48 байтов зарезервированы... не будет ли там куча неиспользованного пространства? Может кто-нибудь помочь мне описать, что здесь происходит? Это то, что зависит от процессора? Я пользуюсь intel i7.
Ура, кирпич
2 ответа
Есть пара вопросов. Во-первых, 3 аргумента передаются регистром, потому что это часть спецификации ELF ABI. Я не уверен, где хранится последний (x86-64) документ SysV ABI в эти дни (x86-64.org кажется несуществующим). Агнер Фог ведет множество отличной документации, в том числе по соглашениям о вызовах.
Распределение стека усложняется вызовом __stack_check_fail
, который добавляется в качестве контрмеры для обнаружения переполнения стека / переполнения буфера. Часть ABI также указывает, что стек должен быть выровнен по 16 байтов до вызова функции. Если вы перекомпилируете с -fno-stack-protector
, вы получите лучшее представление о том, что происходит.
Кроме того, поскольку функция ничего не делает, это не особенно хороший пример. Он хранит аргументы (без необходимости), требуя 12 байтов. buffer1
а также buffer2
вероятно, выровнены по 8 байтов, для чего фактически требуется 8 и 16 байтов соответственно, и, возможно, еще 4 байта для их выравнивания. Я могу ошибаться в этом - у меня нет спецификации под рукой. Так что это либо 36, либо 40 байтов. Выравнивание вызовов затем требует 16-байтового выравнивания для 48 байтов.
Я думаю, что было бы более поучительно отключить защиту стека и проверить кадр стека для этой конечной функции, а также обратиться к спецификации ABI x86-64 для требований к выравниванию локальных переменных и т. Д.
Это скорее зависит от компилятора. Вы можете попробовать отключить оптимизацию или пометить функцию с помощью ключевого слова "extern" (чтобы принудительно использовать соглашение о вызовах по умолчанию).
Регистры используются, потому что это намного быстрее, чем отправка аргументов по стеку.