Один бинарник, любое окружение. Магия чистого C +105




Как мы представляем себе кроссплатформенность? Мы пишем программу на языке, который либо компилируется в исполняемый файл отдельно для каждой поддерживаемой платформы, либо использует разновидность виртуальной машины вместо бинарника (и тогда эта среда должна присутствовать в целевых системах). Есть также и низкоуровневые языки, на которых писать серьёзные программы менее удобно, чем на высокоуровневых монстрах со своими компиляторами или рантаймами, но зато такие программы менее требовательны к предустановленному софту или наличию ОС в принципе, как и менее разборчивы в архитектуре. И всё же, есть возможность писать один и тот же код и собирать один и тот же бинарь под все популярные архитектуры и ОС (и даже bare metal), и эта возможность появилась благодаря гениальной Justine Tunney. Она написала Cosmopolitan, библиотеку на C, позволяющую исполнять один и тот же код на любой машине, подобно Java… но без какого-либо предустановленного интерпретатора или виртуальной машины! Один и тот же скомпилированный файл может исполняться как минимум в любом дистрибутиве Linux, на Mac OS, Windows NT, FreeBSD, OpenBSD, и NetBSD и на bare-metal на x86 и ARM*. Это настоящая магия.

* — для ARM всё же потребуется своего рода эмулятор, но он также встраивается в единственный исполняемый файл. При этом страдает минимальный размер файла, но не его производительность, и разница всё равно будет заметна только для совсем крохотных программ вроде hello world.

?c?µ?lly p?r??bl? ?x?cµ??bl?


Всё началось с переосмысления формата Windows Portable Executable. Оказывается, совместив в одном файле заголовки Windows и UNIX, можно выполнять WPE как скрипт для Thompson shell: пока его не сменил sh в седьмой версии UNIX, скрипты не использовали шебанг. А значит, такой формат позволяет бинарнику запускаться на Windows, Linux и Mac OS:

  MZqFpD='
  BIOS BOOT SECTOR'
  exec 7<> $(command -v $0)
  printf '\177ELF...LINKER-ENCODED-FREEBSD-HEADER' >&7
  exec "$0" "$@"
  exec qemu-x86_64 "$0" "$@"
  exit 1
  REAL MODE...
  ELF SEGMENTS...
  OPENBSD NOTE...
  NETBSD NOTE...
  MACHO HEADERS...
  CODE AND DATA...
  ZIP DIRECTORY...


На видео ниже — визуализация выполнения этого кода в другом инструменте этой же разработчицы, Blinkenlights. Если при выполнении файла в Win символы MZqFpD распознаются как заголовок WPE, то при запуске под UNIX это pop %r10 ; jno 0x4a ; jo 0x4a, \177ELF считывается как jg 0x47. Затем программа пропускает выражение mov, что означает что она выполняется в системе, а не в загрузчике, и переходит к точке входа в скрипт.



Этот формат теперь называется ?c?µ?lly p?r??bl? ?x?cµ??bl? (если вы против пост-мета-сарказмо-иронии, лучше не читайте исходники у этой разработчицы, а формат называйте APE. Исходники правда классные). Проект Cosmopolitan — пример реального использования APE, развивавшийся от PoC до первых релизов и включения в другие проекты.

Скрытый текст



Нужна лишь одна строка, чтобы прокачать gcc для компиляции в APE:

  gcc -g -O -static -fno-pie -no-pie -mno-red-zone -nostdlib -nostdinc -o hello.com hello.c   -Wl,--oformat=binary -Wl,--gc-sections -Wl,-z,max-page-size=0x1000 -fuse-ld=bfd   -Wl,-T,ape.lds -include cosmopolitan.h crt.o ape.o cosmopolitan.a


Помимо простоты, Cosmopolitan удивляет легковесностью: hello world весит примерно в сто раз меньше аналога на «оптимизированном и кроссплатформенном» Go и занимает всего 16 килобайт! Вышеупомянутый эмулятор для ARM уменьшит превосходство с 100 до 10 раз, но такая большая разница только для такого малого размера. Но и это не всё: библиотека ещё и показывает великолепную производительность: чуть медленнее glibc, но с меньшим размером кода, и значительно быстрее Musl и Newlib при сопоставимом размере.

Например, по быстродействию функции memcpy() Cosmopolitan вообще всех обгоняет из-за специфичной механики копирования памяти:



Работает это так: чтобы ускорить часто используемые функции libc, функция вызывается внутри макроса, в котором компилятор получает информацию об используемых регистрах CPU, что позволяет экономить на сохранении состояния CPU, работая только с изменёнными регистрами. На примере memcpy:

  #define memcpy(DEST, SRC, N) ({           void *Dest = (DEST);                    void *Src = (SRC);                      size_t Size = (N);                      asm("call memcpy"                           : "=m"(*(char(*)[Size])(Dest))          : "D"(Dest), "S"(Src), "d"(n),            "m"(*(char(*)[Size])(Src))            : "rcx", "xmm3", "xmm4", "cc");       Dest;                                 })


При этом ускоряется не только сам объект оптимизации, но и, в качестве побочного эффекта, вызывающие его функции. Таким образом, только при применении оптимизации к memcpy, количество генерируемого кода для многих других функций уменьшилось на треть.

Вот так выглядит код функции strlcpy, BSD-аналог strcpy:

  /**
  * Copies string, the BSD way.
  *
  * @param d is buffer which needn't be initialized
  * @param s is a NUL-terminated string
  * @param n is byte capacity of d
  * @return strlen(s)
  * @note d and s can't overlap
  * @note we prefer memccpy()
  */
  size_t strlcpy(char *d, const char *s, size_t n) {
    size_t slen, actual;
    slen = strlen(s);
    if (n) {
      actual = MIN(n - 1, slen);
      memcpy(d, s, actual);
      d[actual] = '\0';
    }
    return slen;
  }


А теперь сравним результаты её компиляции:

классический libc cosmopolitan libc
        strlcpy:
            push	%rbp
            mov	%rsp,%rbp
            push	%r14
            mov	%rsi,%r14
            push	%r13
            mov	%rdi,%r13
            mov	%rsi,%rdi
            push	%r12
            push	%rbx
            mov	%rdx,%rbx
            call	strlen
            mov	%rax,%r12
            test	%rbx,%rbx
            jne	1f
            pop	%rbx
            mov	%r12,%rax
            pop	%r12
            pop	%r13
            pop	%r14
            pop	%rbp
            ret
        1:	cmp	%rbx,%rax
            mov	%r14,%rsi
            mov	%r13,%rdi
            cmovbe	%rax,%rbx
            mov	%rbx,%rdx
            call	memcpy
            movb	$0,0(%r13,%rbx)
            mov	%r12,%rax
            pop	%rbx
            pop	%r12
            pop	%r13
            pop	%r14
            pop	%rbp
            ret
            .endfn	strlcpy,globl
      

        strlcpy:
            mov	%rdx,%r8
            mov	%rdi,%r9
            mov	%rsi,%rdi
            call	strlen
            test	%r8,%r8
            je	1f
            cmp	%r8,%rax
            lea	-1(%r8),%rdx
            mov	%r9,%rdi
            cmova	%rax,%rdx
            call	MemCpy
            movb	$0,(%r9,%rdx)
        1:	ret
            .endfn	strlcpy,globl
      



Разница налицо!

Заключение


«As far as I'm concerned, this is literal magic», «this is the best programming-related thing I've seen on the internet in a long time», «This is one of the most interesting projects I have seen this year» — комментарии на hackernews и в твиттере буквально ломятся от восторженных возгласов. Несмотря на некоторые ограничения, концепция APE действительно выглядит как большой и важный прорыв в подходе к кроссплатформенности. Уже есть несколько реальных примеров использования Cosmopolitan, из них самым мощным точно можно назвать сервер Redbean. Однофайловый, независимый от платформы сервер. Потенциал этой штуки сложно даже мысленно охватить, а тред на HN собрал больше двух тысяч комментов. Помимо этого в твиттере автора периодически появляются всякие интерпретаторы (Lua, JS) и примеры помельче.



Облачные серверы по низким ценам для любых задач. Используем новейшее железо, лучший дата-центр в Москве уровня надёжности TIER IV, бесплатно предоставляем защиту от DDoS-атак на любом тарифном плане, который можно создать самостоятельно в течение мгновения.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!




К сожалению, не доступен сервер mySQL