GNU ассемблер, или просто gas
или as
входит в пакет Binutils, а это значит, что он скорее всего уже есть на Вашей Linux. В gas
по умолчанию используется AT&T синтаксис, более полно можно изучить его здесь. Впрочем gas
позволяет использовать синтаксис Intel и даже порядок аргументов как в синтаксисе Intel, но сейчас речь не об этом. Здесь и далее я буду придерживаться AT&T синтаксиса, как родного для gas
. GAS, как и всякий уважающий себя ассемблер имеет мощный макро-язык. Единственный макрос, который присутствует в нашей библиотеке:
.macro fn name
.global \name
.type \name, @function
\name:
.endm
Рассмотрим его подробнее. Собственно начало макроса .macro
, конец макроса .endm
, тело располагается посередине. Сразу после директивы .macro
следует имя макроса fn
и аргументы name
, если они нужны (как в нашем случае). Что же за кулисами? Использование аргументов внутри макроса возможно через конструкцию \arg
, в нашем случае \name
. Первая директива .global \name
. .global
- это встроенный макрос, который определяет глобальный символ (метку), в нашем случае как раз и нужна глобальная метка. На второй строке используется встроенный макрос .type
, который определяет тип метки - в нашем случае это функция. Ни наконец сама метка.
Например для fn SomeName
это макрос развернётся в следующую конструкцию:
.global SomeName
.type SomeName, @function
SomeName:
Обращаю Ваше внимание, что .global
и .type
не определяют метки, а только дают им особые свойства. Их вообще можно прописать в начале файла, а саму метку разместить в конце.
Этот макрос облегчает написание функций для нашей библиотеки. Замечу, что макрос .type
вовсе не обязателен. Имя метки - это и есть имя функции, и метка эта должна быть глобальной. Например метку some_fn
впоследствии можно будет вызвать из C
как функцию some_fn().
В общем виде функция выгладит следующим образом:
имя_функции:
# тело функции
ret # возврат из функции
например функция которая ничего не делает
.global lazy
lazy:
nop # ничего не делает
ret
в Си будет выглядеть так:
void lazy(void) {
return;
}
По поводу параметра -nostdlib
, если Вы заглянете в Makefile
то увидите, что программа собирается с этой опцией. Эта опция не подключает стандартную библиотеку со всеми вытекающими, т.е. итоговый файл становится намного легче, да ещё и сокращается время запуска. Но вот беда, точка входа - это функция main
, как бы не так, если собирать со стандартной библиотекой, то да (если быть точнее - то запускается сначала init
, а потом main
). Но без неё - точка входа - это метка _start
. И если у Вас по каким либо причинам не будет запускаться итоговый исполняемый файл - то переименуйте функцию main
в _start
. Вообще она называется main
только для виду, Вы можете назвать её x
или entry
например. Во время компиляции Вы увидите:
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400350
Что означает: "не могу найти символ _start; использую по умолчанию 0000000000400350" - это адрес. Т.е. так выходит, что не найдя метки _start
компилятор отмечает точку входа и она совпадает (удивительным образом) с началом первой функции. Но, стоит добавить перед этой функцией ещё одну - и ошибка сегментирования гарантирована. Правильно использовать точку входа - функцию _start
, если сборка идёт с флагом -nostdlib
. Такие аргументы функции main
как argc, argv, env
не будут присутствовать.
Makefile
берёт всю рутину на себя. Синтаксис его прост. Используйте команду make
для компиляции и запуска. Если на каком либо этапе произойдёт ошибка, процесс прервётся. Есть одно замечание по поводу команды echo $?
- это печать кода выхода последней завершённой программы. Нормально завершившаяся программа возвращает 0
, но наша должна возвращать 23
. Вы можете запустить её и выяснить с каким кодом она завершилась
./hello
echo $?
Порядок передачи параметров в Си функцию: %rdi, %rsi, %rdx, %rcx, %r8, %r9. Справедливо для целочисленных параметров и указателей, для чисел с плавающей точкой используются %xmm-регистры. Более подробно вы можете почитать погуглив: linux x64 abi.
Для передачи параметров системной функции есть различия:%rdi, %rsi, %rdx, %r10, %r8, %r9, номер системного вызова кладётся в %eax.
Как видите в нашей функции os.Exit
первый параметр кладётся в %rdi, он же является и первым параметром для системной функции, поэтому никаких перемещений не производится.
Инструкция movq hw@GOTPCREL(%rip), %rsi
- что это такое? Дело в том, что во время линковки, не будет известно в по какому адресу будет располагаться та или иная секция библиотеки, как и сама библиотека. Поэтому все символы относительны GOT - Global Offset Table, глобальной таблицы смещений. Если писать код для исполняемого файла, то можно было просто прописать movq $hw, %rsi
. Так в регистр %rsi
будет положен адрес, на который ссылается hw
. Если писать код для разделяемой библиотеки, то hw
при ассемблировании будет транслировано в простое число. Но в момент загрузки библиотеки происходят перемещения секций и это число уже будет неактуальным. Поэтому используется форма записи относительно GOT. Переменная hw@GOTPCREL
- это указатель на данные, расположенные по метке hw
. Запись hw@GOTPCREL(%rip)
позволяет не беспокоится ни о чём. Просто: там где в исполняемом файле вы бы записали movq $x, %reg
для разделяемой библиотеки стоит писать mpwq x@GOTPCREL, %reg
.
На этом всё.
Божественный туториал. В избранное)