Понимание пустого main() перевода в сборку
Может кто-нибудь объяснить, что GCC делает для этой части кода? Что это за инициализация? Исходный код:
#include <stdio.h>
int main()
{
}
И это было переведено на:
.file "test1.c"
.def ___main; .scl 2; .type 32; .endef
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
call __alloca
call ___main
leave
ret
Я был бы признателен, если бы гуру компилятора/сборки заставил меня начать с объяснения стека, регистрации и инициализации раздела. Я не могу сделать голову или хвост из кода.
EDIT:
Я использую gcc 3.4.5. и аргументом командной строки является gcc -S test1.c
Спасибо,
kunjaan.
Ответы
Ответ 1
Я должен предисловие ко всем моим комментариям, сказав, что я все еще усердно учился.
Я проигнорирую инициализацию секции. Объяснение инициализации секции и, в основном, все, что я расскажу, можно найти здесь:
http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax
Ebp register - это стек стека базовый указатель, следовательно, BP. Он хранит указатель на начало текущего стека.
Регистр esp является указателем стека. Он содержит ячейку памяти в верхней части стека. Каждый раз, когда мы нажимаем что-то на стек, esp обновляется, так что он всегда указывает на адрес вершины стека.
Таким образом, ebp указывает на базу и esp указывает на верх. Таким образом, стек выглядит так:
esp -----> 000a3 fa
000a4 21
000a5 66
000a6 23
esb -----> 000a7 54
Если вы нажмете e4 в стеке, это произойдет:
esp -----> 000a2 e4
000a3 fa
000a4 21
000a5 66
000a6 23
esb -----> 000a7 54
Обратите внимание, что стек растет в сторону более низких адресов, этот факт будет важен ниже.
Первые два шага известны как пролог процедуры или, чаще всего, пролог функции, они готовят стек для использования локальными переменными. См. Краткую пропозицию процедуры внизу.
На шаге 1 мы сохраняем указатель на старый стек стека в стеке, вызывая,
pushl% ebp. Поскольку main является первой вызванной функцией, я понятия не имею, что такое предыдущее значение% ebp.
Шаг 2: Мы вводим новый стек кадров, потому что мы вводим новую функцию (основную). Поэтому мы должны установить новый указатель базы кадров стека. Мы используем значение в esp как начало нашего фрейма стека.
Шаг 3. Выделяет 8 байтов пространства в стеке. Как мы уже упоминали выше, стек растет к более низким адресам, таким образом, вычитая на 8, перемещает вершину стека на 8 байтов.
Шаг 4; Выделите стек, я нашел разные мнения по этому поводу. Я не совсем уверен, что это сделано. Я подозреваю, что это делается для того, чтобы позволить большие инструкции (SIMD) выделяться в стеке,
http://gcc.gnu.org/ml/gcc/2008-01/msg00282.html
Этот код "и" s ESP с 0xFFFF0000, выравнивание стека со следующей самая низкая 16-байтовая граница. изучение исходного кода Mingw показывает, что это может быть для SIMD инструкции, появляющиеся в "_main" рутины, которые работают только на выровненных адреса. Поскольку наша процедура не содержат инструкции SIMD, эта строка не требуется.
http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax
Шаги с 5 по 11, похоже, не имеют для меня никакой цели. Я не мог найти никаких объяснений в Google. Может кто-то, кто действительно знает этот материал, обеспечивает более глубокое понимание. Я слышал слухи, что этот материал используется для обработки исключений C.
Шаг 5 сохраняет значение возврата main 0 в eax.
Шаг 6 и 7 по неизвестной причине добавим 15 в hex к eax. eax = 01111 + 01111 = 11110
Шаг 8 мы сдвигаем биты eax 4 бита вправо. eax = 00001, потому что последние бит сдвигаются с конца 00001 | 111.
Шаг 9 сдвигаем биты eax 4 бита влево, eax = 10000.
Шаги 10 и 11 перемещают значение в первых 4 выделенных байтах в стеке в eax, а затем перемещают его из eax назад.
Шаги 12 и 13 устанавливают библиотеку c.
Мы достигли функции epilogue. То есть, часть функции возвращает указатели стека, esp и ebp в состояние, в котором они находились, до того, как была вызвана эта функция.
Шаг 14, оставьте значения esp равным значению ebp, перемещая верхнюю часть стека по адресу, который был до того, как был вызван main. Затем он устанавливает ebp, чтобы указать адрес, который мы сохранили в верхней части стека, на этапе 1.
Отпуск можно заменить только следующими инструкциями:
mov %ebp, %esp
pop %ebp
Шаг 15, возвращает и выходит из функции.
1. pushl %ebp
2. movl %esp, %ebp
3. subl $8, %esp
4. andl $-16, %esp
5. movl $0, %eax
6. addl $15, %eax
7. addl $15, %eax
8. shrl $4, %eax
9. sall $4, %eax
10. movl %eax, -4(%ebp)
11. movl -4(%ebp), %eax
12. call __alloca
13. call ___main
14. leave
15. ret
Процедура Пролог:
Первое, что нужно сделать функции называется процедурой пролога. Это сначала сохраняет текущий указатель базы (ebp) с инструкцией pushl% ebp (помните, что ebp - это регистр, используемый для доступа к функциональным параметрам и локальные переменные). Теперь он копирует указатель стека (esp) на базу указатель (ebp) с инструкцией movl% esp,% ebp. Это позволяет доступ к параметрам функции как индексы от базового указателя. Местный переменные всегда вычитаются от ebp, например, -4 (% ebp) или (% ebp) -4 для первой локальной переменной, возвращаемое значение всегда равно 4 (% ebp) или (% ebp) +4, каждый параметр или аргумент равен N * 4 + 4 (% ebp), например 8 (% ebp) для первого аргумента while старый ebp находится в (% ebp).
http://www.milw0rm.com/papers/52
Существует действительно отличный поток, который отвечает на многие из этих вопросов.
Почему в моем выпуске gcc есть дополнительные инструкции?
Хорошую ссылку на инструкции машинного кода x86 можно найти здесь:
http://programminggroundup.blogspot.com/2007/01/appendix-b-common-x86-instructions.html
Это лекция, в которой содержатся некоторые из приведенных ниже идей:
http://csc.colstate.edu/bosworth/cpsc5155/Y2006_TheFall/MySlides/CPSC5155_L23.htm
Вот еще один ответ на ваш вопрос:
http://www.phiral.net/linuxasmone.htm
Ни один из этих источников не объясняет все.
Ответ 2
Здесь хороший пошаговый прорыв простой функции main()
, скомпилированной GCC, с большим количеством подробной информации: Синтаксис GAS (Википедия)
Для кода, который вы вставили, инструкции разбиваются следующим образом:
- Первые четыре инструкции (pushl through andl): настройка нового фрейма стека
- Следующие пять команд (movl through sall): генерируют странное значение для eax, которое станет возвращаемым значением (я понятия не имею, как он решил это сделать)
- Следующие две команды (оба movl): сохраняют вычисленное возвращаемое значение во временной переменной в стеке
- Следующие две команды (оба вызова): вызовите функции init библиотеки C
-
leave
инструкция: срывает фрейм стека
-
ret
инструкция: возвращает вызывающему абоненту (внешняя функция выполнения или, возможно, функция ядра, вызывающая вашу программу)
Ответ 3
Ну, не знаю много о GAS, и я немного ржавый на сборке Intel, но он выглядит как его инициализирующий основной стек кадров.
если вы посмотрите, __main - это какой-то макрос, должен выполняться инициализация.
Затем, когда основной объект пуст, он вызывает команду leave, чтобы вернуться к функции, которая называется main.
От http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax#.22hello.s.22_line-by-line:
Эта строка объявляет метку "_main", обозначая место, которое вызывается из кода запуска.
pushl %ebp
movl %esp, %ebp
subl $8, %esp
Эти строки сохраняют значение EBP в стеке, затем перемещают значение ESP в EBP, а затем вычитают 8 из ESP. "L" в конце каждого кода операции указывает, что мы хотим использовать версию кода операции, которая работает с "длинными" (32-битными) операндами;
andl $-16, %esp
Этот код "и" s ESP с 0xFFFF0000, выравнивая стек со следующей нижней 16-байтной границей. (обязательно при использовании инструкций simd, здесь не полезно)
movl $0, %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
Этот код перемещает ноль в EAX, а затем перемещает EAX в ячейку памяти EBP-4, которая находится во временном пространстве, которое мы зарезервировали в стеке в начале процедуры. Затем он перемещает память EBP-4 обратно в EAX; очевидно, это не оптимизированный код.
call __alloca
call ___main
Эти функции являются частью настройки библиотеки C. Поскольку мы вызываем функции в библиотеке C, мы, вероятно, нуждаемся в них. Точные операции, которые они выполняют, зависят от платформы и версии установленных инструментов GNU.
Здесь полезная ссылка.
http://unixwiz.net/techtips/win32-callconv-asm.html
Ответ 4
Это действительно поможет узнать, какую версию gcc вы используете, и какой libc. Похоже, у вас очень старая версия gcc или странная платформа или и то, и другое. Что происходит, это странность с вызовами. Я могу сказать вам несколько вещей:
Сохранить указатель кадра в стеке в соответствии с соглашением:
pushl %ebp
movl %esp, %ebp
Сделайте место для вещей на старом конце кадра и вокруг указателя стека до кратного 4 (почему это нужно, я не знаю):
subl $8, %esp
andl $-16, %esp
С помощью безумной песни и танца приготовьтесь вернуть 1 из main
:
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
Восстановите любую память, выделенную с помощью alloca
(GNU-ism):
call __alloca
Объявите libc, что main
выходит (больше GNU-ism):
call ___main
Восстановить указатели на фрейм и стек:
leave
Возврат:
ret
Вот что происходит, когда я компилирую тот же самый исходный код с gcc 4.3 в Debian Linux:
.file "main.c"
.text
.p2align 4,,15
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (Debian 4.3.2-1.1) 4.3.2"
.section .note.GNU-stack,"",@progbits
И я сломаю это так:
Сообщите отладчику и другим инструментам исходный файл:
.file "main.c"
Код находится в текстовом разделе:
.text
Ударь меня:
.p2align 4,,15
main
- экспортированная функция:
.globl main
.type main, @function
main
точка входа:
main:
Возьмите адрес возврата, выровняйте стек по 4-байтовому адресу и снова сохраните возвращаемый адрес (почему я не могу сказать):
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
Сохранить указатель кадра с использованием стандартного соглашения:
pushl %ebp
movl %esp, %ebp
Непостижимое безумие:
pushl %ecx
popl %ecx
Восстановить указатель кадра и указатель стека:
popl %ebp
leal -4(%ecx), %esp
Возврат:
ret
Дополнительная информация для отладчика:
.size main, .-main
.ident "GCC: (Debian 4.3.2-1.1) 4.3.2"
.section .note.GNU-stack,"",@progbits
Кстати, main
является специальным и магическим; когда я компилирую
int f(void) {
return 17;
}
Я получаю что-то немного более нормальное:
.file "f.c"
.text
.p2align 4,,15
.globl f
.type f, @function
f:
pushl %ebp
movl $17, %eax
movl %esp, %ebp
popl %ebp
ret
.size f, .-f
.ident "GCC: (Debian 4.3.2-1.1) 4.3.2"
.section .note.GNU-stack,"",@progbits
Там еще тонна украшения, и мы по-прежнему сохраняем указатель на фрейм, перемещая его и восстанавливая его, что совершенно бессмысленно, но остальная часть кода имеет смысл.
Ответ 5
Похоже, что GCC действует так, как будто редактировать main()
, чтобы включить код инициализации CRT. Я только что подтвердил, что я получаю то же самое листинг сборки из MinGW GCC 3.4.5 здесь, с вашим исходным текстом.
Используемая мной команда:
gcc -S emptymain.c
Интересно, что если я изменю имя функции на qqq()
вместо main()
, я получаю следующую сборку:
.file "emptymain.c"
.text
.globl _qqq
.def _qqq; .scl 2; .type 32; .endef
_qqq:
pushl %ebp
movl %esp, %ebp
popl %ebp
ret
что имеет смысл для пустой функции без включения оптимизаций.