Как ассемблерный код определяет, как далеко внизу помещаются переменные в стеке? - PullRequest
3 голосов
/ 29 апреля 2019

Я пытаюсь понять некоторые основные понятия кода ассемблера и зацикливаюсь на том, как код ассемблера определяет, куда поместить вещи в стек и сколько места для него предоставить.

Чтобы начать играть с нимЯ ввел этот простой код в проводнике компилятора godbolt.org.

int main(int argc, char** argv) {
  int num = 1;  
  num++;  
  return num;
}

и получил этот код сборки

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-20], edi
        mov     QWORD PTR [rbp-32], rsi
        mov     DWORD PTR [rbp-4], 1
        add     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret

Итак, пара вопросов здесь:

  1. Разве параметры не должны быть помещены в стек перед вызовом?Почему argc и argv размещены по смещению 20 и 32 от базового указателя текущего фрейма стека?Это кажется очень далеким, чтобы поместить их, если нам нужно только место для одной локальной переменной num.Есть ли причина для всего этого дополнительного пространства?

  2. Локальная переменная хранится на 4 ниже базового указателя.Так что, если бы мы визуализировали это в стеке и сказали, что базовый указатель в настоящее время указывает на 0x00004000 (просто придумаю это для примера, не уверен, что это реалистично), то мы поместим значение в 0x00003FFC, верно?И целое число имеет размер 4 байта, поэтому оно занимает пространство памяти от 0x00003FFC вниз до 0x00003FF8, или оно занимает пространство памяти от 0x00004000 до 0x00003FFC?

  3. Похожеуказатель стека никогда не перемещался вниз, чтобы освободить место для этой локальной переменной.Разве мы не должны были сделать что-то вроде sub rsp, 4, чтобы освободить место для локального int?

И затем, если я изменил это, чтобы добавить к нему больше местных жителей:

int main(int argc, char** argv) {
  int num = 1; 
  char *str1 = {0};
  char *str2 = "some string"; 
  num++;  
  return num;
}

Тогда мы получим

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-36], edi
        mov     QWORD PTR [rbp-48], rsi
        mov     DWORD PTR [rbp-4], 1
        mov     QWORD PTR [rbp-16], 0
        mov     QWORD PTR [rbp-24], OFFSET FLAT:.LC0
        add     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret

Так что теперь основные аргументы отодвинуты еще дальше от базового указателя.Почему пространство между первыми двумя местными жителями 12 байтов, а пространство между вторыми двумя местными жителями 8 байтов?Это из-за размеров типов?

Ответы [ 2 ]

3 голосов
/ 30 апреля 2019

Я собираюсь ответить только на эту часть вопроса:

Разве параметры не должны быть помещены в стек перед вызовом?Почему argc и argv размещены по смещению 20 и 32 от базового указателя текущего кадра стека?

Параметры для main действительно устанавливаются кодом, который вызывает main.

Это, похоже, код, скомпилированный в соответствии с 64-битным psABI ELF для x86 , в котором первые несколько параметров любой функции передаются в регистры , а не встек.Когда элемент управления достигнет метки main:, argc будет в edi, argv будет в rsi, а третий аргумент, обычно называемый envp, будет в rdx.(Вы не объявляли этот аргумент, поэтому вы не можете его использовать, но код, который вызывает main, является универсальным и всегда устанавливает его.)

Инструкции, которые, я полагаю, вы ссылаетесь на

    mov     DWORD PTR [rbp-20], edi
    mov     QWORD PTR [rbp-32], rsi

- это то, что ботаники компилятора называют spill инструкциями: они копируют начальные значения параметров argc и argv из своих исходных регистров в стек, на всякий случай, если эти регистрынужны для чего-то еще.Как отметили несколько других людей, это неоптимизированный код;эти инструкции не нужны и не будут отправлены, если вы включили оптимизацию.Конечно, если бы вы включили оптимизацию, вы бы получили код, который вообще не касается стека:

main:
    mov     eax, 2
    ret

В этом ABI компилятору разрешено помещать «слоты разлива»,msgstr "в который сохраняются значения регистров, , где он хочет в кадре стека.Их расположение не должно иметь смысла и может варьироваться от компилятора к компилятору, от уровня исправления до уровня исправления одного и того же компилятора или с явно не связанными изменениями в исходном коде.

(Некоторые ABI действительно задают кадр стекав некоторых деталях, например, IIRC, 32-битный Windows ABI для Windows, чтобы облегчить «раскрутку», но это сейчас не важно.)

(чтобы подчеркнуть, что аргументы main находятся в регистрах,это сборка, которую я получаю в -O1 из

int main(int argc) { return argc + 1; }

:

main:
    lea     eax, [rdi+1]
    ret

Все еще ничего не делает со стеком (кроме ret.))

2 голосов
/ 30 апреля 2019

Это «компилятор 101», и вы хотите исследовать «соглашение о вызовах» и «кадр стека».Детали зависят от компилятора / ОС / оптимизации.Вкратце, входящие параметры могут быть в регистрах или в стеке.Когда функция введена, она может создать стековый фрейм для сохранения некоторых регистров.И затем он может определить «указатель кадра», чтобы ссылаться на локальные стеки и параметры стека вне указателя кадра.Иногда указатель стека также используется в качестве указателя кадра.

Что касается регистров, то обычно кто-то (компания) определяет соглашение о вызовах и указывает, какие регистры являются «изменчивыми», что означает, что они могут использоватьсяПодпрограмма без проблем и «сохранена», что означает, что если подпрограмма использует их, они должны быть сохранены и восстановлены при входе и выходе из функции.Соглашение о вызовах также определяет, какие регистры (если таковые имеются) используются для передачи параметров и возврата функции.

...