Open source: кодоюмор, кодотрюки, НЕ кодобред

в 8:24, , рубрики: C, open source, КодоБред, трюки в коде, юмор

Старый GLib vs новый Clang

Ковыряясь в разнообразном СПО, я периодически нахожу всякие интересные штуки: иногда это просто смешной комментарий, иногда — нечто остроумное в более широком смысле. Подобные подборки периодически появляются и в «глобальном Интернете», и на Хабре — есть, скажем, широко известный вопрос на 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+

Есть такой шуточный язык — 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:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

Программисты играют в гольф

Есть целый сайт на 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)

Автор: Анатолий Тросиненко

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js