Компиляторы: понимание кода сборки, созданного с помощью небольших программ

Я сам изучаю, как работают компиляторы. Я изучаю, читая разборку кода, созданного GCC, из небольших 64-разрядных программ Linux.

Я написал эту программу на C:

#include <stdio.h>

int main()
{
    for(int i=0;i<10;i++){
        int k=0;
    }
}

После использования objdump я получаю:

00000000004004d6 <main>:
  4004d6:       55                      push   rbp
  4004d7:       48 89 e5                mov    rbp,rsp
  4004da:       c7 45 f8 00 00 00 00    mov    DWORD PTR [rbp-0x8],0x0
  4004e1:       eb 0b                   jmp    4004ee <main+0x18>
  4004e3:       c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
  4004ea:       83 45 f8 01             add    DWORD PTR [rbp-0x8],0x1
  4004ee:       83 7d f8 09             cmp    DWORD PTR [rbp-0x8],0x9
  4004f2:       7e ef                   jle    4004e3 <main+0xd>
  4004f4:       b8 00 00 00 00          mov    eax,0x0
  4004f9:       5d                      pop    rbp
  4004fa:       c3                      ret    
  4004fb:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

Теперь у меня есть некоторые сомнения.

  • Что это за NOP в конце, и почему он там? (Выравнивание?)

  • Я компилирую с gcc -Wall <program.c>. Почему я не получаю предупреждение control reaches end of non-void function?

  • Почему компилятор не выделяет пространство в стеке с помощью sub rsp,0x10? Почему он не использует регистр rbp для ссылки на данные локального стека?

    PS: Если я вызываю функцию (например, printf) в цикле for, почему компилятор неожиданно генерирует sub rsp,0x10? Почему он все еще ссылается на локальные данные с регистром rsp. Я ожидаю, что сгенерированный код ссылается на локальные данные стека с помощью rbp!

Ответы

Ответ 1

В отношении второго вопроса, поскольку стандарт C99 разрешил не иметь явного return 0 в функции main, компилятор добавит его неявно. Обратите внимание, что это только для функции main, никакой другой функции.

Что касается третьего вопроса, регистр rbp действует как указатель фрейма .

Наконец, PS. Вероятно, вызываемая функция использует 16 bytes (0x10) для аргументов, переданных функции. Вычитание - это то, что "удаляет" эти переменные из стека. Возможно, это два указателя, которые вы передаете в качестве аргументов?

Если вы серьезно изучаете, как работают компиляторы вообще, и, возможно, хотите создать свой собственный (это весело!:)), я предлагаю вам инвестировать в некоторые книги о теории и практике этого. Книга драконов является отличным дополнением к любой книжной полке для программистов.

Ответ 2

Все, что после ret не может считаться кодом. Декодирование как nop означает "Без операции"

Вторая точка - это компилятор, обнаруживающий, что вы оставите функцию main, не возвращая значения, и вставляет return 0 (только для main).

Регистр rbp, с bp, означающий "Base Pointer", указывает на стек стека функции currect. Вызов функции часто приводит к сохранению записи функции rbp и использованию текущего значения rsp для rbp. Параметры ввода/сохранения функций и локальные переменные выполняются относительно rbp.


Я думаю, что ваш третий вопрос требует дополнительного внимания: "Почему компилятор не выделяет пространство в стеке с помощью sub rsp,0x10 ? Почему он не использует регистр rbp для ссылки на данные локального стека?"

Собственно, компилятор выделяет пространство в стеке. Но это не меняет указатель стека. Он может это сделать, потому что functon не вызывает других функций. Он просто использует пространство ниже curent sp (стек растет), и он использует rbp для доступа к i ([rbp-0x8]) и k ([rbp-0x4]).


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

Вопрос о прерываниях, решенных в Компилятор с использованием локальных переменных без настройки RSP

Ответ 3

  • Да, nop для выравнивания. Компиляторы используют разные инструкции для различной длины заполнения, зная, что современный процессор будет предварительно получать и декодировать несколько инструкций вперед.

  • Как говорили другие, стандарт C99 возвращает 0 из main() по умолчанию, если нет явного оператора return (см. 5.1.2.2.3 в C99 TC3), поэтому предупреждение не возникает.

  • 64-разрядная система V Linux ABI резервирует 128-байтную "красную зону" ниже текущего указателя стека, функции (функции, которые не вызывают никаких других функций), и ваш основной() один из них) может использоваться для локальных переменных и других значений нуля без необходимости использования sub rsp/add rsp. И поэтому rbp == rsp.

И для PS: когда вы вызываете функцию в цикле for() (или где угодно в вашем main()), main() больше не является функцией листа, поэтому компилятор больше не может использовать красную зону. Именно поэтому он выделяет пространство в стеке с помощью sub rsp, 0x10. Однако он знает взаимосвязь между rsp и rbp, поэтому он может использовать либо при доступе к данным.