Компиляторы: понимание кода сборки, созданного с помощью небольших программ
Я сам изучаю, как работают компиляторы. Я изучаю, читая разборку кода, созданного 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, поэтому он может использовать либо при доступе к данным.