Понимание пустого перевода main () в сборку

Может кто-нибудь объяснить, что GCC делает для этой части кода? Что это за инициализация? Исходный код:

#include  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.

Я должен предисловие ко всем моим комментариям, сказав: я все еще усердно учился.

Я проигнорирую инициализацию секции. Объяснение инициализации секции и в основном все, что я расскажу, можно найти здесь: http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax

Регистр ebp является базовым указателем фрейма стека , следовательно, 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 

Обратите внимание, что стек растет в сторону более низких адресов, этот факт будет важен ниже.

Первые два этапа известны как prolog процедуры или, чаще всего, prolog функции , они готовят стек для использования локальными переменными. См. Пролог процедуры процедуры внизу.

На шаге 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, сохраняет возвращаемое значение основного 0 в eax.

Шаг 6 и 7 мы добавляем по 15 раз в гексагон к 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.

Мы достигли функционального эпилога . То есть, часть функции, которая возвращает указатели стека, 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 

Процедура Пролог:

Первое, что нужно сделать функции, называется процедурой prologа. Сначала он сохраняет текущий базовый указатель (ebp) с помощью команды pushl% ebp (помните, что ebp – это регистр, используемый для доступа к параметрам функции и локальным переменным). Теперь он копирует указатель стека (esp) в базовый указатель (ebp) с инструкцией movl% esp,% ebp. Это позволяет вам получить доступ к параметрам функции в виде индексов из базового указателя. Локальные переменные всегда вычитаются из ebp, например -4 (% ebp) или (% ebp) -4 для первой локальной переменной, возвращаемое значение всегда равно 4 (% ebp) или (% ebp) +4, каждый параметр или аргумент находится в N * 4 + 4 (% ebp), например 8 (% ebp) для первого аргумента, а старый ebp равен (% ebp).

http://www.milw0rm.com/papers/52

Существует действительно замечательный stream переполнения стека, который отвечает на весь этот вопрос. Почему в моем выпуске 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

Ни один из этих источников не объясняет все.

Вот хорошая пошаговая разбивка простой функции main() скомпилированной GCC, с большим количеством подробной информации: GAS Syntax (Wikipedia)

Для кода, который вы вставили, инструкции разбиваются следующим образом:

  • Первые четыре инструкции (pushl through andl): настройка новой frameworks стека
  • Следующие пять команд (movl through sall): генерируют странное значение для eax, которое станет возвращаемым значением (я понятия не имею, как он решил это сделать)
  • Следующие две команды (оба movl): сохраняют вычисленное возвращаемое значение во временной переменной в стеке
  • Следующие две команды (оба вызова): вызовите функции init библиотеки C
  • leave инструкцию: срывает рамку стека
  • ret : возврат к вызывающей стороне (внешняя функция времени выполнения или, возможно, функция ядра, вызывающая вашу программу)

Ну, не знаю много о GAS, и я немного ржавый на сборке Intel, но он выглядит как его исходный стек стека инициализации.

если вы посмотрите, __main – это какой-то макрос, должен выполняться инициализация. Затем, когда тело 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

Это действительно поможет узнать, какую версию 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 "fc" .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 

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

Похоже, что GCC действует так, как будто нормально редактировать main() чтобы включить код инициализации CRT. Я только что подтвердил, что я получаю то же самое листинг сборки от MinGW GCC 3.4.5 здесь, с вашим исходным текстом.

Командная строка, которую я использовал, это:

 gcc -S emptymain.c

Интересно, что если я изменил имя функции на qqq() вместо main() , я получаю следующую сборку:

         .file "emptymain.c"
         .текст
 .globl _qqq
         .def _qqq;  .scl 2;  .type 32;  .endef
 _qqq:
         pushl% ebp
         movl% esp,% ebp
         popl% ebp
         RET

что имеет смысл для пустой функции без включения оптимизаций.