[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.
    Иронично...