Подтвердить что ты не робот

Понимание пустого 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.

4b9b3361

Ответ 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

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