Как мы представляем себе кроссплатформенность? Мы пишем программу на языке, который либо компилируется в исполняемый файл отдельно для каждой поддерживаемой платформы, либо использует разновидность виртуальной машины вместо бинарника (и тогда эта среда должна присутствовать в целевых системах). Есть также и низкоуровневые языки, на которых писать серьёзные программы менее удобно, чем на высокоуровневых монстрах со своими компиляторами или рантаймами, но зато такие программы менее требовательны к предустановленному софту или наличию ОС в принципе, как и менее разборчивы в архитектуре. И всё же, есть возможность писать один и тот же код и собирать один и тот же бинарь под все популярные архитектуры и ОС (и даже 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 |
---|---|
|
|
Разница налицо!
Заключение
«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% скидку на первый месяц аренды сервера любой конфигурации!
K-ILYA-V
Любой 64-битный код в котором есть PUSH/POP уже ущербен по сути.
Беда печаль в том что другого кода по сути и нет.
Tangeman
"Мы не знаем как должно быть но вы делаете неправильно"?
K-ILYA-V
Никто не знает как надо, но все знают тысячи способов как не надо, после того как вы испробуете все способы как не надо у вас останется только один способ как сделать и он и будет тем способом который как надо.
nonrblGyN4ik
… у вас останется только один способ как сделать и он и будет… тысяча первым способом как не надо :)
K-ILYA-V
смысл вашего комментария мне не понятен.
Alex_ME
Ну так по-идее System V amd64 ABI предполагает передачу целочисленных аргументов через rdi, rsi, rdx, rcx, r8, r9, что должно быть оптимальнее стека.
beeruser
Почему ущербен?
В х86 процессорах есть stack engine, который позволяет эффективно выполнять последовательности push/pop.
Alex_ME
Насколько это эффективно по сравнению с регистрами? Как я понял, stack engine генерирует адреса, а load/store никуда не исчезают?
picul
Просто для справки, эти push/pop предназначены не для передачи аргументов, а для сохранения состояния тех регистров общего назначения, которые функция обязана сохранять согласно calling convention.
beeruser
3 операнда в приведённой в статье функции strlcpy() и передаются через регистры RDX,RSI,RDI.
K-ILYA-V
передача параметров через стек это анахронизм 60-70 годов.
beeruser
Они через регистры передаются. Я выше ответил.
Что делать если параметров больше, чем регистров выделенных в ABI для передачи параметров?
FenestramDeveloper
В первую очередь — подумать, нельзя ли уменьшить число параметров. Но если речь о каком-нибудь специфическом ABI или передаваемом типе данных, то без стека не обойтись.
CanisAlbus
Допустим параметры идут через регистры и так, а куда локальные переменные девать? Регистров же не напасешся? Компайлер и так по возможности старается регистры использовать и под локальные переменные, но на все не хватит.
Плюс как трейсить вызовы без стека? Рекурсия под запретом? Хвостовую не все умеют.
Кучу использовать вместо стека тоже не получиться, там другие проблемы.
Чего сразу ущербен то?
K-ILYA-V
Большинство локальных переменных это избыточные сущности существование которых вызвано не способность построит оптимальный путь обработки данных.