[0001] Cамописные build-скрипты и оптимизации _____________________________________________________________ В этом посте я буду писать о своих попытках сделать ultra lightweigh билд системы для проектов на си. Сразу подведу итог: makefile лучше. Также немного затрону тему -nostartfiles и -nostdlib программ на си, чтобы размер конечного бинарника был около 7 байт. * Cамописные build-скрипты и оптимизации ............. [0001] * Реализации на си ................................... [0002] * CAB v1 ........................................... [0003] * CAB v2 ........................................... [0004] * mk.conf и ./make ................................... [0005] * Оптимизации и размеры конечных файлов .............. [0006] * -nostartfiles и -nostdlib .......................... [0007] * CRT в 2 строчки .................................. [0008] * syscalls и прочее ................................ [0009] * loslib ........................................... [0010] * Ссылки ............................................. [0011] 28 Ноября 2022 года [0002] Реализации на си _____________________________________________________________ По сути система сборки состоит из нескольких основных компонентов: 1. Поиск файлов для сборки (и их проверка по времени) 2. Настройка проекта (что-то вроде ./configure) 3. Компиляция проекта с нужными CFLAGS, LDFLAGS и тд (желательно в многопотоке) Для проектов на си, использование билд систем, написанных на си, в принципе звучит логично. Для нормального билд скрипта на си нужно написать свою библиотеку, ибо стандартной явно не хватит, либо можно написать билд скрипт самому, а затем добавить возможность его конфигурировать. Собственно, я сделал второе. --[0003] CAB v1 --------------------------------------------- Поток: ~Amchik/vol2-utils ~Amchik/vol2-utils/tree/master/cab Одна из особенностей этой системы в том, что я её завершил до состояния, когда она может выполнять свою основную функцию - собирать проект. Конфигурацию пиздил у suckless программ, поэтому случилось это: +---------------------------------------------------------+ | cab/cabfile.c | +---------------------------------------------------------+ | | | /********** | | * CONFIG * | | **********/ | | #define SOURCE_DIR "src" | | #define OUTPUT_DIR "out" | | #define OUTPUT_FMT "%s_%s.o" | | #define BINARY_DIR "bin" | | #define BINARY_FMT "a.out" | | | | #define DEFAULT_CFLAGS "-pipe" | | #define DEFAULT_LDFLAGS "" | | | | const TARGET TARGETS[] = { | | /* NAME CFLAGS LDFLAGS */ | | { "release", "-O3 -march=native -flto", "" }, | | { "debug", "-O0 -g", "" }, | | { 0, 0, 0 } | | }; | | | +---------------------------------------------------------+ %s_%s В остальном, ничего интересного из себя cab не представляет. --[0004] CAB v2 --------------------------------------------- Поток: imagine a world without github... В отличии от первой версии, во второй конфигурация сосёт ещё больше: парсинг конфига проходит в рантайме. И было бы это главной проблемой, но сам конфиг записан всё ещё в файле с остальным кодом. Смотря на прекрасные макросы в расте, я решил сделать что-то подобное. +---------------------------------------------------------+ | cab2.c | +---------------------------------------------------------+ | | | #define TOSTRING(h) #h | | const char *config = TOSTRING( | | cc = "clang"; | | cflags = "-Wall -Wextra -pipe"; | | | | strict = true; | | pedantic = true; | | | | target "debug" { | | verbose = true; | | debug = true; | | } | | target "release" { | | cflags = "-flto -O3 -march=native"; | | ldflags = "-fuse-ld=lld -Wl,-O2" | | } | | ); | | | +---------------------------------------------------------+ Чисто в теории можно и файл было так парсить. Проект так и не был доделан, зато у него были очень подробные сообщения об ошибках в конфиге. [0005] mk.conf и ./make _____________________________________________________________ Несмотря на то, что я уже использовал эту систему у себя в проектах, отдельного репозитория у неё нет. Поток: ~Amchik/dwm-status ~ValgrindLLVM/zerobloat-lang [1] (боже прости) Лучше всего она показывается в последнем репозитории, так как там есть user-friendly конфигурация процесса сборки: +-----------------------------------------------------------+ | ~ValgrindLLVM/zerobloat-lang/blob/master/make | +-----------------------------------------------------------+ | | | files \ | | | make ex intoex \ | | | true # hide output | | | +-----------------------------------------------------------+ По сути ничего не мешает компилировать проект и в один файл: +-----------------------------------------------------------+ | ./make | +-----------------------------------------------------------+ | | | files \ | | | make cc intoobj \ | | | make ld intoapp \ | | | make st true \ # strip | | | true # hide output | | | +-----------------------------------------------------------+ Конечно, один из плюсов, это конфигурация в POSIX Shell. Так можно было прописать логику прямо к конфиг, и в зависимости от библиотек ставить нужные флаги. +-----------------------------------------------------------+ | mk.conf | +-----------------------------------------------------------+ | | | target=debug # функция, что запускается перед сборкой | | log=mklog # команда для логов (mklog встроенный) | | | | cc=$(which clang || die "clang required") | | cflags="-pipe -nostartfiles -nostdlib ${CFLAGS}" | | | | debug() { # target | | cflags="-g $cflags" | | } | | | +-----------------------------------------------------------+ Стандартный набор команд есть в man pages, но я никуда их не выкладывал. Может быть когда-нибудь... [0006] Оптимизации и размеры конечных файлов _____________________________________________________________ Если у нас уже есть ZERO BLOAT[1] система сборки, то наверное нужно и писать такие же программы. Начиная с [0005] mk.conf и make я стал добавлять strip в сборку. Таким образом можно убрать ненужные вещи из бинарника и сократить его размер. Для библиотек рекомендуется убирать debug символы из сборки (-g), но так как мне нужен был минимальный размер, я убирал даже названия функций (-s). Так же, для отладки, можно убрать мусор компилятора, не повредив остальной бинарник (-X). В итоге, используя -O3 [2] и strip -s можно было получить файлы размером в 9Кб. Но для hello-world это всё ещё много, даже у zerobloat-lang и то меньше: +-----------------------------------------------------------+ | $ echo 'hello, world!' > hello.zb | | $ zb hello.zb | | hello, world! | | $ wc -c hello.zb | | 14 hello.zb | +-----------------------------------------------------------+ [0007] -nostartfiles и -nostdlib _____________________________________________________________ -nostdlib и -nostartfiles убирают crt и libc из конечной сборки. Нужны ли они? Разумеется да. Но так же можно попробовать писать без них (особенно если учесть, что стандарт библиотек си и так ужасный). --[0008] CRT в 2 строчки ----------------------------------- Для начала нам потребуется написать CRT. В принципе, если не использовать аргументы командной строки, то можно тупо в _start() всё писать. В остальных случаях, нам нужно написать naked _start() функцию, которая будет вызывать start(long *p), где p - указание на аргументы (*p это argc, p[n] это argv[n - 1]). +---------------------------------------------------------+ | hello-bob.c [x86_64] | +---------------------------------------------------------+ | | | __attribute__((noreturn, naked)) void _start() { | | asm("mov %rsp, %rdi"); | | asm("call start"); | | } | | | +---------------------------------------------------------+ _start() не должен ничего возвращать, а так же не должен иметь начала. В принципе, в том же clang эти аттрибуты уже проставлены (разве что clang может сгенерировать ret). Можно так же было писать в global assembly, тогда точно не будет ret инструкции, но это не сильно красиво. --[0009] syscalls и прочее ---------------------------------- Можно наверное их писать вручную, но лучше всего их доставать из libc. Самое простое: musl. /usr/src/musl/arch/$arch/syscall_arch.h Коды можно брать оттуда же. В musl все функции __syscallX() объявлены как __inline static. Теперь, выведем "Hello, world!" в stdout: +---------------------------------------------------------+ | hello-bob.c [linux x86_64] | +---------------------------------------------------------+ | | | #include "syscall_arch.h" | | | | const char str[] = "Hello, world!"; | | void _start() { | | __syscall3(1, 1, str, sizeof(str)); /* write(2) */ | | | | for (;;) __syscall1(60, 0); /* exit(2) */ | | } | | | +---------------------------------------------------------+ Компилируя с -O3 и не забывая про strip -s, мы избавляемся от ненужного crt и libc. Если вдруг нам понадобится argv, то можно использовать crt из [0008]. --[0010] loslib --------------------------------------------- Я её опять никуда не выкладывал, но она есть в одном из моих проектов. Поток: ~ValgrindLLVM/zerobloat-lang Представляет собой небольшой интерфейс для написания nolibc программ. Невероятно маленькая, в следствии чего достаточно бессмысленная. Примеры можно посмотреть в репозитории выше. [0011] Ссылки _____________________________________________________________ [1] ~ValgrindLLVM/zerobloat-lang - ирония над "0% bloat" в ЯП [2] В clang и gcc так же есть -Os (size) и -Oz (очень size), но для оптимизации размера они работают хуже, чем -O3. Иронично...