Базовая структура кадров стека

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

локальные переменные ‹--- 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
??

Я предполагаю, что буферы находятся где-то между аргументами и старым BP. Но я действительно не уверен, где именно ... поскольку они оба всего 15 байтов всего и 48 байтов, где зарезервировано ... не будет ли там куча неиспользуемого пространства? Может ли кто-нибудь помочь мне описать, что здесь происходит? Это что-то, что зависит от процессора? Я использую Intel i7.

Привет, Брик


person user2239930    schedule 03.04.2013    source источник
comment
won't there be a bunch of unused space in there? важно отметить, что выделение стека всегда будет округляться до кратного размера слова целевой архитектуры. наряду с этим ABI может также навязывать требования к выравниванию (например, требование 16 байтов на x64), что также может увеличить размер.   -  person Necrolis    schedule 03.04.2013


Ответы (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 для требований к выравниванию локальных переменных и т. д.

person Brett Hale    schedule 03.04.2013

Это скорее зависит от компилятора. Вы можете попробовать отключить оптимизацию или пометить функцию с помощью ключевого слова «extern» (чтобы принудительно использовать соглашение о вызовах по умолчанию).

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

person Ernest Staszuk    schedule 03.04.2013
comment
Спасибо за быстрый ответ. В этом случае имеет смысл использовать регистры. Тем не менее, я попытался собрать его с отключенной оптимизацией gcc -O0 -S -masm=intel main.c, но все равно получаю тот же результат. Я так понимаю, это просто способ сделать это. Тем не менее, я хотел бы точно знать, как в этом случае состоит кадр стека. Может ли кто-нибудь помочь мне с этим? У меня возникли проблемы с идентификацией буферов в кадре стека... кроме того, не должны ли адреса буферов также храниться где-то в стеке? - person user2239930; 03.04.2013