Skip to content

Instantly share code, notes, and snippets.

@logrusorgru
Last active January 25, 2023 18:13
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save logrusorgru/b844133cee0f283c736b to your computer and use it in GitHub Desktop.
Save logrusorgru/b844133cee0f283c736b to your computer and use it in GitHub Desktop.
GNU Assembler: пишем разделяемые библиотеки

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.

На этом всё.

// команда для компиляции и линковки
// gcc -nostdlib -Wall -L. -I. hello.c -lhelloexit -Wl,-rpath,. -o hello
// подключение заголовков нашей супербиблиотеки
#include "helloexit.h"
// поехали!
int main(/*int argc, char const *argv[]*/ /*здесь нет никаких аргументов*/ ) { // правильно назвать эту функцию _start
// объявляем переменную i
register int i = 23;
es_hello_world(); // вызов библиотечной функции es.HelloWorld
os_exit(i); // вызов библиотечной функции os.Exit (первая переменная помещается в %rdi)
return 0; // а вот этого уже не произойдёт
}
/*
Если бы наши функции в библиотеке назывались os_exit и es_hello_world, то можно было бы
не заморачиваясь использовать такую конструкцию:
void os_exit(int);
void es_hello_world();
Но, т.к. в именах имеется точка, придётся немного по выкручиваться.
*/
extern void os_exit(int) asm("os.Exit");
extern void es_hello_world() asm("es.HelloWorld");
// теперь всё в порядке, наши функции теперь называются os_exit и es_hello_world
# команда для компиляции
# gcc -nostdlib -Wall -shared -fPIC hello_exit.S -Wl,-soname,libhelloexit.so.1 -o libhelloexit.so.1.0
#
# макросы
.macro fn name
.global \name
.type \name, @function
\name:
.endm
# секция данных, из неё можно читать, в неё можно записывать
.data
.type hw, @object # тип локальной переменной object - что значит "данные" - это строка опциональна
hw: # определяем локальную метку для сообщения
.ascii "Hello, world\n" # значение метки - это ascii строка, нет она не кончается нулём
.set hs, .-hw # сохраним размер строки в hs, это не метка - это просто значение, число
# секция кода
.text
# определяем первую функцию, имена выбраны нарочно так, чтобы они конфликтовали с конвенцией имён в Си
fn es.HelloWorld
# собственно это вызов системной функции write
# write(1, message, size)
movq $1, %rax # системный вызов номер 1 - это функция write
movq $1, %rdi # файловый дескриптор, куда записывать, 1 - это стандартный вывод (читай терминал)
movq hw@GOTPCREL(%rip), %rsi # адрес строки для вывода GOT - это Global Offset Table
movq $hs, %rdx # длина строки в байтах
syscall # обращение к системному вызову
retq # возвращение управления вызывающему (return)
# вторая функция - это выход из программы, её единственный параметр помещается в регистр %rdi
fn os.Exit
# exit(%rdi)
movq $60, %rax # системный вызов номер 60 - это функция exit(), завершает программу
syscall # вызов
# ret # возврат не требуется, любой код далее не имеет значения
all: clean build run
build:
gcc -nostdlib -Wall -shared -fPIC hello_exit.S -Wl,-soname,libhelloexit.so.1 -o libhelloexit.so.1.0
ln -sv libhelloexit.so.1.0 libhelloexit.so.1
ln -sv libhelloexit.so.1.0 libhelloexit.so
gcc -nostdlib -Wall -L. -I. hello.c -lhelloexit -Wl,-rpath,. -o hello
@echo Done
run:
@./hello || echo $$?
clean:
rm -vf hello
rm -vf libhelloexit.so
rm -vf libhelloexit.so.1
rm -vf libhelloexit.so.1.0
@echo Done
@Vadimatorik
Copy link

Божественный туториал. В избранное)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment