Понимание перевода пустой main () на сборку - PullRequest
15 голосов
/ 05 мая 2009

Может кто-нибудь объяснить, что 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.

Ответы [ 5 ]

14 голосов
/ 05 мая 2009

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

Я буду игнорировать инициализацию раздела. Объяснение инициализации раздела и, в основном, всего остального, что я рассмотрю, можно найти здесь: 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

Обратите внимание, что стек увеличивается по направлению к младшим адресам, этот факт будет важен ниже.

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

На шаге 1 мы сохраняем указатель на старый кадр стека в стеке, вызывая pushl% ebp. Поскольку main - это первая вызванная функция, я не знаю, какое предыдущее значение указывает% ebp.

Шаг 2, Мы вводим новый кадр стека, потому что мы вводим новую функцию (основную). Следовательно, мы должны установить новый базовый указатель фрейма стека. Мы используем значение в esp, чтобы быть началом нашего стекового фрейма.

Шаг 3. Выделяет 8 байт пространства в стеке. Как мы уже упоминали выше, стек увеличивается к младшим адресам, таким образом, вычитая на 8, перемещает вершину стека на 8 байтов.

Шаг 4; Выравнивая стек, я нашел разные мнения по этому поводу. Я не совсем уверен, что именно это делается. Я подозреваю, что это сделано для того, чтобы в стеке могли размещаться большие инструкции (SIMD),

http://gcc.gnu.org/ml/gcc/2008-01/msg00282.html

Этот код "а" ESP с 0xFFFF0000, выравнивание стека со следующим младшая 16-байтовая граница. проверка исходного кода Mingw показывает, что это может быть для SIMD инструкции, появляющиеся в "_main" рутины, которые действуют только по согласованию адреса. Поскольку наша рутина не содержат инструкции SIMD, эта строка не нужен.

http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax

Шаги с 5 по 11 кажутся мне бесполезными. Я не мог найти никакого объяснения в Google. Может ли тот, кто действительно знает этот материал, дать более глубокое понимание. До меня дошли слухи, что этот материал используется для обработки исключений в Си.

Шаг 5, сохраняет возвращаемое значение main 0 в eax.

Шаг 6 и 7 мы добавляем 15 в гекс к eax по неизвестной причине. eax = 01111 + 01111 = 11110

Шаг 8 мы сдвигаем биты eax 4 бита вправо. eax = 00001, потому что последние биты сдвинуты от конца 00001 | 111.

Шаг 9 мы сдвигаем биты 4-х бит eax влево, 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

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

Первое, что должна сделать функция называется процедурой пролог. Это сначала сохраняет текущий базовый указатель (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

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

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

9 голосов
/ 05 мая 2009

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

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

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

Было бы действительно полезно узнать, какую версию 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

Return:

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

Return:

        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

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

4 голосов
/ 05 мая 2009

Ну, я не очень разбираюсь в ГАЗЕ, и я немного заржавел на сборке Intel, но похоже, что он инициализирует основной стек стека.

если вы посмотрите, __main - это какой-то макрос, должен выполнять инициализации. Затем, поскольку тело main пусто, оно вызывает инструкцию выхода, чтобы вернуться к функции, которая вызвала 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

Это код "и" 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

1 голос
/ 05 мая 2009

Похоже, что 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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...