Ответ 1
Вот как это делается:
.file "test.c"
Исходное имя исходного файла (используется отладчиками).
.section .rodata
.LC0:
.string "Hello world!"
Строка с нулевым завершением включена в раздел ".rodata" ( "ro" означает "только для чтения": приложение сможет считывать данные, но любая попытка записи в него вызовет исключение).
.text
Теперь мы пишем вещи в раздел ".text", где находится код.
.globl main
.type main, @function
main:
Мы определяем функцию, называемую "main" и глобально видимую (другие объектные файлы могут ее вызывать).
leal 4(%esp), %ecx
Мы сохраняем в регистре %ecx
значение 4+%esp
(%esp
- указатель стека).
andl $-16, %esp
%esp
немного изменен, так что он становится кратным 16. Для некоторых типов данных (формат с плавающей запятой, соответствующий C double
и long double
), производительность лучше, когда обращения к памяти находятся по адресам которые являются кратными 16. Здесь это действительно не нужно, но при использовании без флага оптимизации (-O2
...) компилятор имеет тенденцию создавать довольно много общего бесполезного кода (то есть кода, который может быть полезен в некоторых случаев, но не здесь).
pushl -4(%ecx)
Это немного странно: в этот момент слово по адресу -4(%ecx)
является словом, которое находилось поверх стека до andl
. Код извлекает это слово (которое, кстати, должно быть обратным адресом) и снова выталкивает его. Этот тип эмулирует то, что было бы получено при вызове функции, которая имела выровненный по 16 байт стек. Я предполагаю, что этот push
является остатком последовательности копирования аргументов. Поскольку функция скорректировала указатель стека, она должна скопировать аргументы функции, которые были доступны через старое значение указателя стека. Здесь нет аргументов, кроме адреса возврата функции. Обратите внимание, что это слово не будет использоваться (опять же, это код без оптимизации).
pushl %ebp
movl %esp, %ebp
Это стандартная функция пролог: мы сохраняем %ebp
(так как мы собираемся ее модифицировать), затем установите %ebp
, чтобы указать на стек стека. После этого %ebp
будет использоваться для доступа к аргументам функции, после чего %esp
освободится. (Да, аргументов нет, поэтому для этой функции это бесполезно.)
pushl %ecx
Мы сохраняем %ecx
(нам понадобится его при выходе из функции, чтобы восстановить %esp
по значению, имевшемуся до andl
).
subl $20, %esp
Мы оставляем 32 байта в стеке (помните, что стек растет "вниз" ). Это пространство будет использоваться для хранения аргументов printf()
(которые переполняют, поскольку существует один аргумент, который будет использовать 4 байта [это указатель]).
movl $.LC0, (%esp)
call printf
Мы "нажимаем" аргумент на printf()
(т.е. убедитесь, что %esp
указывает на слово, которое содержит аргумент, здесь $.LC0
, который является адресом постоянной строки в разделе rodata). Тогда будем называть printf()
.
addl $20, %esp
Когда printf()
возвращается, мы удаляем пространство, выделенное для аргументов. Этот addl
отменяет то, что сделал выше subl
.
popl %ecx
Мы восстанавливаем %ecx
(нажата выше); printf()
может изменить его (условные обозначения вызовов описывают, какой регистр может изменять функцию, не восстанавливая их при выходе, %ecx
- один из таких регистров).
popl %ebp
Эпилог функции: это восстанавливает %ebp
(соответствует pushl %ebp
выше).
leal -4(%ecx), %esp
Мы восстанавливаем %esp
до его начального значения. Эффект этого кода операции заключается в сохранении в %esp
значения %ecx-4
. %ecx
был установлен в первом опционном коде функции. Это отменяет любое изменение до %esp
, включая andl
.
ret
Выход функции.
.size main, .-main
Здесь задается размер функции main()
: в любой момент сборки ".
" является псевдонимом для "адреса, в котором мы сейчас добавляем вещи". Если здесь добавлена другая инструкция, она будет идти по адресу, указанному ".
". Таким образом, ".-main
" здесь является точным размером кода функции main()
. Директива .size
указывает ассемблеру записать эту информацию в объектный файл.
.ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"
GCC просто любит оставлять следы своего действия. Эта строка заканчивается как комментарий в объектном файле. Линкера удалит его.
.section .note.GNU-stack,"",@progbits
Специальный раздел, в котором GCC пишет, что код может содержать неиспользуемый стек. Это нормальный случай. Исполняемые стеки необходимы для некоторых специальных применений (не стандартных C). На современных процессорах ядро может создавать неиспользуемый стек (стек, который запускает исключение, если кто-то пытается выполнить как код некоторые данные, находящиеся в стеке); это рассматривается некоторыми людьми как "функция безопасности", потому что помещение кода в стек является распространенным способом использования переполнения буфера. В этом разделе исполняемый файл будет помечен как "совместимый с неиспользуемым стеком", который ядро с удовольствием предоставит как таковое.