Сборка GCC пустой программы на x86, win32 - PullRequest
55 голосов
/ 23 августа 2009

Я пишу пустые программы, чтобы чертовски раздражать кодеров stackoverflow, НЕ Я просто изучаю набор инструментов gnu.

Теперь следующее может быть слишком глубоким для меня, но чтобы продолжить пустую сагу программы, я начал проверять вывод компилятора C, то, что GNU потребляет.

gcc version 4.4.0 (TDM-1 mingw32)

test.c:

int main()
{
    return 0;
}

gcc -S test.c

    .file   "test.c"
    .def    ___main;    .scl    2;  .type   32; .endef
    .text
.globl _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    call    ___main
    movl    $0, %eax
    leave
    ret 

Можете ли вы объяснить, что здесь происходит? Вот мои усилия, чтобы понять это. Я использовал руководство as и мои минимальные знания x86 ASM:

  • .file "test.c" - это директива для логического имени файла.
  • .def: в соответствии с документами «Начать определение информации отладки для имени символа» . Что такое символ (имя функции / переменная?) И какая отладочная информация?
  • .scl: в документах написано "Класс хранения может указывать, является ли символ статическим или внешним" . Это то же самое статическое и внешнее Я знаю из C? И что это за «2»?
  • .type: сохраняет параметр "в качестве атрибута типа записи таблицы символов" , я понятия не имею.
  • .endef: нет проблем.
  • .text: Теперь это проблематично, кажется, что это называется разделом, и я читал, что это место для кода, но документы не сказали мне слишком много.
  • .globl "делает символ видимым для ld." , в руководстве это вполне понятно.
  • _main: Это может быть начальный адрес (?) Для моей основной функции
  • pushl_: длинный (32-битный) пуш, который помещает EBP в стек
  • movl: 32-битный ход. Псевдо-C: EBP = ESP;
  • andl: Логическое И. Псевдо-C: ESP = -16 & ESP, я не вижу смысла в этом.
  • call: выталкивает IP в стек (чтобы вызываемая процедура могла найти свой путь назад) и продолжается там, где __main. (что такое __main?)
  • movl: этот ноль должен быть константой, которую я возвращаю в конце моего кода. MOV помещает этот ноль в EAX.
  • leave: восстанавливает стек после инструкции ENTER (?). Почему?
  • ret: возврат к адресу инструкции, сохраненному в стеке

Спасибо за помощь!

Ответы [ 5 ]

56 голосов
/ 23 августа 2009

.file "test.c"

Команды, начинающиеся с. директивы ассемблеру. Это просто говорит, что это «file.c», эта информация может быть экспортирована в отладочную информацию exe.

.def ___main; .scl 2; .type 32; .endef

Директивы

.def определяют символ отладки. scl 2 означает класс хранения 2 (класс внешнего хранения). type 32 говорит, что этот символ является функцией. Эти числа будут определены в формате pe-coff exe

___ main - это вызываемая функция, которая заботится о начальной загрузке, необходимой gcc (она будет выполнять такие вещи, как запуск статических инициализаторов c ++ и другие необходимые служебные действия).

.text

Начинается текстовый раздел - код живет здесь.

.globl _main

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

.def        _main;  .scl    2;      .type   32;     .endef

То же самое, что и _main, создает отладочные символы, указывающие, что _main является функцией. Это может быть использовано отладчиками.

_main:

Запускает новый ярлык (в итоге получится адрес). директива .globl выше делает этот адрес видимым для других объектов.

pushl       %ebp

Сохраняет старый указатель кадра (регистр ebp) в стеке (чтобы его можно было вернуть на место после завершения этой функции)

movl        %esp, %ebp

Перемещает указатель стека в регистр ebp. ebp часто называют указателем кадра, он указывает на верхнюю часть значений стека в текущем «кадре» (обычно это функция) (обращение к переменным в стеке через ebp может помочь отладчикам)

andl $ -16,% esp

Заканчивает стек с помощью fffffff0, который эффективно выравнивает его по 16-байтовой границе. Доступ к выровненным значениям в стеке намного быстрее, чем если бы они были выровнены. Все эти предыдущие инструкции в значительной степени являются стандартным прологом функции.

call        ___main

Вызывает функцию ___main, которая будет выполнять инициализацию вещей, необходимых gcc. Call вызовет указатель текущей инструкции в стеке и перейдет к адресу ___ main

movl        $0, %eax

переместить 0 в регистр eax (0 в ответ 0;) регистр eax используется для хранения значений, возвращаемых функцией для соглашения о вызовах stdcall.

отпуск

Инструкция по отпуску довольно краткая для

movl     ebp,esp
popl     ebp

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

1073 * RET *

Возвращается тому, кто вызвал эту функцию. Он извлечет указатель инструкций из стека (который будет помещена туда соответствующей инструкцией вызова) и перейдет туда.

12 голосов
/ 23 августа 2009

Здесь очень похожее упражнение: http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax

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

__main - это подпрограмма в стандартной библиотеке GNU, которая выполняет различные инициализации при запуске. Это не является строго обязательным для программ на C, но требуется только в том случае, если код C связан с C ++.

_main - ваша основная подпрограмма. Поскольку _main и __main являются местами расположения кода, они имеют один и тот же класс хранения и тип. Я еще не выкопал определения для .scl и .type. Вы можете получить некоторое освещение, определив несколько глобальных переменных.

Первые три инструкции устанавливают стековый фрейм, который является техническим термином для рабочего хранилища подпрограммы - по большей части локальные и временные переменные. Нажатие ebp сохраняет основание кадра стека вызывающего абонента. Помещение esp в ebp устанавливает основу нашего стекового фрейма. andl выравнивает кадр стека по 16-байтовой границе на случай, если любые локальные переменные в стеке требуют 16-байтового выравнивания (для инструкций x86 SIMD требуется это выравнивание, но выравнивание ускоряет обычные типы, такие как int s * * с тысячей двадцать-один. * * тысяча двадцать две

В этот момент вы обычно ожидаете, что esp будет перемещен в память для выделения стекового пространства для локальных переменных. У вашего main нет ни одного, поэтому gcc не беспокоит.

Вызов __main является особенным для основной точки входа и обычно не появляется в подпрограммах.

Остальное идет, как вы и предполагали. Регистр eax - это место для добавления целочисленных кодов возврата в двоичную спецификацию. leave отменяет кадр стека и ret возвращается к вызывающей стороне. В этом случае вызывающей стороной является низкоуровневая среда выполнения C, которая выполняет дополнительные действия (например, вызывает функции atexit(), устанавливает код завершения процесса и просит операционную систему завершить процесс.

5 голосов
/ 23 августа 2009

Что касается и $ -16,% esp

  • 32 бита: -16 в десятичном формате равно 0xfffffff0 в шестнадцатеричном представлении
  • 64 бита: -16 в десятичном формате равно 0xfffffffffffffff0 в шестнадцатеричном представлении

Таким образом, он маскирует последние 4 бита ESP (кстати: 2 ** 4 равно 16) и сохраняет все остальные биты (независимо от того, является ли целевая система 32 или 64-битной).

4 голосов
/ 23 августа 2009

В дополнение к andl $-16,%esp это работает, потому что установка младших битов в ноль всегда приведет к уменьшению значения %esp * , а стек увеличивается на x86.

2 голосов
/ 23 августа 2009

У меня нет ответов на все вопросы, но я могу объяснить, что я знаю.

ebp используется функцией для хранения начального состояния esp во время ее потока, ссылки на то, где аргументы передаются функции и где ее собственные локальные переменные. Первое, что делает функция, это сохраняет статус заданного ebp, выполняющего pushl %ebp, это жизненно важно для функции, которая выполняет вызов, и затем заменяет ее собственной текущей позицией стека esp, выполняющей movl %esp, %ebp , Обнуление последних 4 битов ebp в данный момент является специфическим для GCC, я не знаю, почему этот компилятор делает это. Это будет работать без этого. Теперь, наконец, мы идем в бизнес, call ___main, кто __main? Я тоже не знаю ... может быть, больше специфичных для GCC процедур, и, наконец, единственное, что делает ваш main (), устанавливает значение возврата равным 0 с помощью movl $0, %eax и leave, что аналогично выполнению movl %ebp, %esp; popl %ebp для восстановления ebp состояние, затем ret до конца. ret выскакивает eip и продолжает поток потока из той точки, где бы она ни находилась (в качестве своей функции main () этот ret, вероятно, приводит к некоторой процедуре ядра, которая обрабатывает конец программы).

Большая часть всего этого заключается в управлении стеком. Я написал подробное руководство о том, как стек используется некоторое время назад, было бы полезно объяснить, почему все эти вещи сделаны. Но это по-португальски ...

...