Open source: кодоюмор, кодотрюки, НЕ кодобред
Ковыряясь в разнообразном СПО, я периодически нахожу всякие интересные штуки: иногда это просто смешной комментарий, иногда — нечто остроумное в более широком смысле. Подобные подборки периодически появляются и в «глобальном Интернете», и на Хабре — есть, скажем, широко известный вопрос на StackOverflow про комментарии в коде, а здесь недавно публиковалась подборка забавных названий юрлиц и топонимов. Попробую и я структурировать и выложить то, что постепенно у меня копилось. Под катом вас ждут цитаты из QEMU, ядра Linux, и не только.
Linux kernel
Думаю, для многих не является секретом то, что письма из Linux Kernel Mailing List периодически расходятся на цитаты. Поэтому давайте лучше посмотрим в код. И сразу же система сборки ядра встречает нас сюрпризом: как известно, проекты, собираемые Autoconf имеют Makefile с двумя стандартными целями для очистки: clean
и distclean
. Естественно, ядро не собирается с помощью Autoconf, да и чего только стоит один лишь menuconfig
, поэтому целей здесь побольше: clean
, distclean
и mrproper
— да-да, «Мистер Пропер», ядро чище в два раза быстрей.
Кстати о системе конфигурирования: когда-то давно я был удивлён, когда наткнулся в ней помимо понятных команд вроде allnoconfig
, allyesconfig
(подозреваю, что вкомпилироваться может что-нибудь сильно отладочное, поэтому сейчас бы я такое загружать на реальном железе не рискнул бы…) и allmodconfig
на загадочную цель allrandconfig
. «Они что, издеваются» — подумал я, потом рассказал об этом наблюдении своему знакомому, на что тот ответил, что, вероятно, это вполне осмысленная команда, но не для реальной сборки, а для тестирования правильности расстановки зависимостей между опциями — как я сказал бы уже сейчас, этакий фаззинг параметров конфигурации.
Впрочем, есть в ядре жизнь и за пределами сборочной системы: документация иногда представляет не только техническую, но и, своего рода, художественную ценность. Предположим, вы хотите предупредить пользователей спящего режима о его хрупкости и риске потери данных при несоблюдении определённых правил. Я бы уныло написал, мол ВНИМАНИЕ: <подставить пару максимально нудных строчек>. Но разработчик, писавший это, поступил по-другому:
Some warnings, first.
* BIG FAT WARNING *********************************************************
*
* If you touch anything on disk between suspend and resume...
* ...kiss your data goodbye.
*
* If you do resume from initrd after your filesystems are mounted...
* ...bye bye root partition.
* [this is actually same case as above]
*
* ...
Маленькие хитрости
Неудивительно, что не всякий код можно собирать с оптимизациями: когда я попытался принудительно их включить для всех объектных файлов, я закономерно напоролся на некий источник энтропии или что-то подобное, который выдавал #error
, если включена оптимизация. Ну, криптография — она такая. А не хотите ли код, который не соберётся, если вы отключите все оптимизации, инлайнинг и т.д.? Как такое возможно? А это такой статический assert:
/* SPDX-License-Identifier: GPL-2.0 */
// ...
/*
* This function doesn't exist, so you'll get a linker error if
* something tries to do an invalidly-sized xchg().
*/
extern void __xchg_called_with_bad_pointer(void);
static inline
unsigned long __xchg(unsigned long x, volatile void *ptr, int size)
{
unsigned long ret, flags;
switch (size) {
case 1:
#ifdef __xchg_u8
return __xchg_u8(x, ptr);
#else
local_irq_save(flags);
ret = *(volatile u8 *)ptr;
*(volatile u8 *)ptr = x;
local_irq_restore(flags);
return ret;
#endif /* __xchg_u8 */
// ...
default:
__xchg_called_with_bad_pointer();
return x;
}
}
Предполагается, видимо, что при любом использовании с константным аргументом эта функция развернётся в одну лишь ветвь switch
, а при использовании с корректным аргументом, эта ветвь будет не default:
.
В не оптимизированном виде эта функция вызовет ошибку линковки практически by design…
Знаете ли вы
- … что в ядре есть JIT-компилятор байткода из пользовательского режима? Эта технология называется eBPF и используется для маршрутизации, трассировки и много чего ещё. Кстати, если не боитесь экспериментальных «ядерных» инструментов, посмотрите на пакет bpftools.
- … что в ядро можно уйти минут на пять процессорного времени? Есть такой системный вызов
sendfile
, копирующий байты из одного файлового дескриптора в другой. Если указать ему один и тот же дескриптор и выставить правильное смещение в файле, он будет наматывать одни и те же данные, пока не скопирует 2 Гб. - … что есть вариант работы гибернации, проводимой пользовательским процессом — не удивлюсь, если можно и на сетевое хранилище так сохраниться.
QEMU
Вообще, когда я почитал Роберта Лава про устройство Linux kernel, а потом полез в исходники QEMU, у меня возникло некоторое ощущение дежавю. Там были и списки, встраивающиеся в структуры по значению (а не как в начальном курсе программирования учат — через указатели), и некая подсистема RCU (что это такое, я так до конца и не понял, но в ядре оно тоже есть) и, наверное, много ещё похожего.
С чем первым делом знакомится аккуратный человек, желающий поработать над проектом? Наверное, с Coding style. И уже в этом, можно сказать, парадном, документе, мы видим:
1. Whitespace
Of course, the most important aspect in any coding style is whitespace.
Crusty old coders who have trouble spotting the glasses on their noses
can tell the difference between a tab and eight spaces from a distance
of approximately fifteen parsecs. Many a flamewar has been fought and
lost on this issue.
Здесь же про извечный вопрос о максимальной длине строк:
Lines should be 80 characters; try not to make them longer.
...
Rationale:
- Some people like to tile their 24" screens with a 6x4 matrix of 80x24
xterms and use vi in all of them. The best way to punish them is to
let them keep doing it.
...
(Хмм… Это же в два раза больше по каждой оси, чем иногда использую я. Это такой Linux HD?)
Там ещё много интересного — почитайте.
И снова хитрости
Говорят, С — низкоуровневый язык. Но если хорошо извратиться, проявлять чудеса compile-time кодогенерации можно и без всяких Scala и даже C++.
Вот, например, в кодовой базе QEMU притаился файл softmmu_template.h
. Когда я увидел это название, то подумал, что его предполагается скопировать в свою реализацию TCG backend-а и подправлять, пока из него не получится правильная реализация TLB. Как бы не так! Вот как правильно его использовать:
accel/tcg/cputlb.h:
define DATA_SIZE 1
#include "softmmu_template.h"
#define DATA_SIZE 2
#include "softmmu_template.h"
#define DATA_SIZE 4
#include "softmmu_template.h"
#define DATA_SIZE 8
#include "softmmu_template.h"
Как видите, ловкость рук и никакого C++. Но это довольно простой пример. Как насчёт чего посложнее?
Есть такой файл: tcg/tcg-opc.h. Содержимое его довольно загадочно и выглядит как-то так:
...
DEF(mov_i32, 1, 1, 0, TCG_OPF_NOT_PRESENT)
DEF(movi_i32, 1, 0, 1, TCG_OPF_NOT_PRESENT)
DEF(setcond_i32, 1, 2, 1, 0)
DEF(movcond_i32, 1, 4, 1, IMPL(TCG_TARGET_HAS_movcond_i32))
/* load/store */
DEF(ld8u_i32, 1, 1, 1, 0)
DEF(ld8s_i32, 1, 1, 1, 0)
DEF(ld16u_i32, 1, 1, 1, 0)
DEF(ld16s_i32, 1, 1, 1, 0)
...
На самом деле всё очень просто — используется он вот так:
tcg/tcg.h:
typedef enum TCGOpcode {
#define DEF(name, oargs, iargs, cargs, flags) INDEX_op_ ## name,
#include "tcg-opc.h"
#undef DEF
NB_OPS,
} TCGOpcode;
Или так:
tcg/tcg-common.c:
TCGOpDef tcg_op_defs[] = {
#define DEF(s, oargs, iargs, cargs, flags) \
{ #s, oargs, iargs, cargs, iargs + oargs + cargs, flags },
#include "tcg-opc.h"
#undef DEF
};
Даже странно, что с ходу других случаев использования не нашлось. И заметьте, в данном случае нет никаких хитрых скриптов для кодогенерации — только C, только хардкор.
Знаете ли вы
- … что QEMU может работать не только в режиме эмуляции полной системы, но и запускать отдельный процесс для другой архитектуры, общающийся с хостовым ядром?
Java, JVM и все-все-все
Что же я всё о Линуксе? Давайте о чём-нибудь кросс-платформенном поговорим. О JVM, например. Ну, про GraalVM, наверное, многие разработчики в этой экосистеме уже слышали. Если не слышали, то в двух словах: это эпично. Итак, после рассказа о Graal перейдём к старой-доброй JVM.
Иногда JVM нужно остановить все managed-потоки — стадия сборки мусора такая заковыристая или ещё чего —, но вот незадача, останавливать потоки можно только на так называемых safepoints. Как рассказывается здесь, нормальная проверка глобальной переменной занимает много времени, в том числе некое шаманство с memory barriers. Что же сделали разработчики? Они ограничились одним чтением переменной.
Есть такой шуточный язык — HQ9+. Он создавался как «очень удобный учебный язык программирования», а именно, на нём очень просто выполнять типичные задачи, которые задают ученикам:
- по команде 'H' интерпретатор печатает Hello, World!
- по команде 'Q' печатает сам текст программы (квайн)
- на '9' он печатает текст песенки про 99 bottles of the beer
- по 'i' он увеличивает переменную i на единицу
- больше он ничего делать не умеет, а зачем?…
Как же JVM одной инструкцией добивается цели? А очень просто — при необходимости остановки она убирает отображение для страницы памяти с этой переменной — потоки падают по SIGSEGV, а JVM их паркует и снимает с паузы, когда «техобслуживание» заканчивается. Помню, на StackOverflow на вопрос с собеседования How do you crash a JVM? ответили:
JNI. In fact, with JNI, crashing is the default mode of operation. You have to work extra hard to get it not to crash.
Шутки шутками, а иногда в JVM оно на самом деле так.
Ну и раз уж я упомянул про кодогенерацию в Scala, да и говорим мы сейчас как раз об этой экосистеме, то вот вам занятный факт: кодогенерация в Scala (та, которая макросы), устроена приблизительно так: вы пишите код на Scala, использующий API компилятора, и компилируете его. Потом при следующем запуске компилятора вы просто передаёте получившийся кодогенератор в classpath самого компилятора, а тот, увидев специальную директиву, его вызывает, передав синтаксические деревья, полученные при вызове. В ответ он получает AST, которое нужно подставить в месте вызова.
Особенности идеологий лицензирования
Мне нравится идеология свободного ПО, но и в ней бывают забавные особенности.
Когда-то, лет десять назад, я обновил свой Debian stable и, задумавшись про синтаксис какой-то команды, привычно набрал man <команда>
, на что получил исчерпывающее описание вроде »[имя программы] — это программа с документацией, распространяемой под лицензией GNU GFDL с неизменяемыми секциями, которая не является DFSG-free». Говорят, эту программу написали какие-то злые проприетарщики из какого-то FSF… (Сейчас гуглится обсуждение.)
А какая-то маленькая, но важная библиотека некоторыми дистрибутивами считается несвободным ПО, поскольку к стандартной пермиссивной лицензии автор сделал приписку про то, что эта программа должна использоваться для добра, а не для зла. Смех смехом, а я бы тоже, наверное, побоялся такое в production брать — мало ли, какие представления о добре и зле у автора.
Всякое разное
Особенности интернационального компиляторостроения в период закона Мура
Суровые разработчики LLVM ограничили поддерживаемое выравнивание:
The maximum alignment is 1 << 29.
Как говорится, заставляет сначала посмеяться, а потом задуматься: первая мысль — да кому нужно выравнивание на 512 MiB. Потом почитал про разработку ядра на Rust, а там предлагают сделать структуру «таблица страниц», выровненную на 4096 байтов. А как почитаешь Википедию, так там вообще:
A full mapping hierarchy of 4 KB pages for the whole 48-bit space would take a bit more than 512 GB of memory (about 0.195% of the 256 TB virtual space).
Версия формата — как хранить?
Как-то раз решил я разобраться, почему не работает экспорт в одной программе, а он, оказывается, работает… Или нет?
Позапускав вручную команды backend-а, понял, что, в принципе, всё в порядке, просто версия должна передаваться как »2.0», а уходит просто »2». Предвкушая тривиальное исправление путём правки строковой константы, обнаруживаю функцию double getVersion()
— ну, а что, major есть, minor есть, даже точка есть! Впрочем, в итоге всё решилось не намного сложнее, чем предполагалось, я просто повысил точность вывода переправил тип данных и пробросил строки.
О разнице между теоретиками и практиками
По моему, где-то на Хабре я уже видел перевод статьи про то, какова минимальная падающая при запуске, но всё же компилирующаяся программа на C? int main;
— символ main
есть, и технически, можно передать на него управление. Правда, программа при этом упадёт, но правилам это вполне соответствует.
Итак, ронять то, что должно работать, мы умеем, а как насчёт того, чтобы запустить не запускаемое?
$ ldd /bin/ls
linux-vdso.so.1 (0x00007fff93ffa000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f0b27664000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0b2747a000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f0b27406000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0b27400000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0b278e9000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f0b273df000)
$ /lib/x86_64-linux-gnu/libc.so.6
…, а libc ему человеческим голосом:
GNU C Library (Ubuntu GLIBC 2.28-0ubuntu1) stable release version 2.28.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 8.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
.
Программисты играют в гольф
Есть целый сайт на StackExchange, посвященный Code Golf — соревнованиям с стиле «Решите эту задачу с минимальным штрафом, зависящим от размера исходного кода». Сам формат предполагает весьма изысканные решения, но иногда они становятся уж очень изысканными. Поэтому в одном из вопросов была собрана коллекция стандартных запрещённых лазеек. Мне особенно нравится эта:
Using MetaGolfScript
MetaGolfScript is a family of programming languages. For example, the empty program in MetaGolfScript-209180605381204854470575573749277224 prints «Hello, World!».
Одной строкой
Наконец, откуда такое название статьи? Это перефразированный трюк из вывода компилятора emcc
из состава Emscripten:
$ emcc --help
...
emcc: supported targets: llvm bitcode, javascript, NOT elf
(autoconf likes to see elf above to enable shared object support)