Основная структура стековых фреймов

В настоящее время я играю, изучаю стековые фреймы, пытаюсь понять, как это работает. После прочтения нескольких статей, которые всегда объясняли, что общая структура будет:

местные переменные <--- 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" (чтобы принудительно использовать соглашение о вызовах по умолчанию).

Регистры используются, потому что это намного быстрее, чем отправка аргументов по стеку.

Другие вопросы по тегам