Експерименти з nolibc TCP-сервером
Such is human stupidity that whatever is difficult
to obtain is always thought to be better.
-- Peter Martyr d'Anghiera, 1524.
Колись давно, років 8 тому, ув часи сивочолого гетьмана і кволих реформ мусоріату, на HN з'явився лінка на http-сервера розміром 1KB. Не src того сервера був 1KB, а його статично зкомпільований байнарник. Все що той сервер вмів робити це відповідати на будь-який ріквест 1 файлом. Я проявив 0 інтересу, тому що написано воно було на asm/C ув пропорції 60/40.
Ідея такого розміру програм сподобалася французькому автору haproxy і він вмовив нарід ув LKML погодитися закомітити окрему бібліотеку для написання "nolibc" аплікацій. Прикладом такої аплікації є rcutorture--тестування кернела--де генерується невеличкий initrd, написаний на C без використання будь-якої libc.
Іронія ув необхідності спеціяльної бібліотеки щоб не використовувати стандартну бібліотеку нагадує спробу обійти коло, рухаючись по його контуру.
На тему < 1KB байнарників є класичний текста Really Teensy ELF Executables, але у ньому йде мова про асемблера, на який я не бажав звертати уваги ув минулому і на який не бажаю звертати уваги зараз.
Чи можна створювати крихітні (байнарі-вайз) аплікації без асемблеру на
C без стандартної бібліотеки? Майже пусту main() лінкер--лінкує, але:
$ cat 42.v1.c
int main() { return 42; }
$ clang -Os 42.v1.c -o 42.v1.o -c
$ ld.lld -e main -o 42.v1 42.v1.o
$ ./42.v1
Segmentation fault (core dumped)Виявляється, осемблер потрібен щоб кликати сисколи лайнакса:
$ cat 42.v2.c
static inline long syscall(long NR, long arg1) {
long r;
__asm__ volatile("syscall" : "=a" (r) : "a" (NR),
"D" (arg1) : "rcx", "r11", "memory");
return r;
}
int main() { return 42; }
void _start() { syscall(60, main()); }
$ clang -Os 42.v2.c -o 42.v2.o -c
$ ld.lld -e _start -o 42.v2 42.v2.o
$ strip -s 42.v2
$ strace ./42.v2
execve("./42.v2", ["./42.v2"], 0x7ffc7190b9a0 /* 105 vars */) = 0
exit(42) = ?
+++ exited with 42 +++Ага! Такий статичний байнарник має розмір 800 байт і залежить від 0
бібліотек. Номер сисколу (60) для exit можна подивитися ув файлі
arch/x86/entry/syscalls/syscall_64.tbl кернельного src. На відміну
від віндюка, де ABI змінюється з щорічними "feature updates", такий
800-байтовий ікзек'ютабл буде працювати доки є жива орхітектура
x86_64.
Чи складно тоді, маючи обгортки для лайнаксних сисколів, написати елементарного TCP сервера для x86_64? Я вирішив спробувати:
$ _out/uptime-tcp-nolibc &
[1] 53161
$ nc --recv-only 127.0.0.1 1377
07:16:29 up 1 day(s) 22:14, load average: 0.19, 0.31, 0.30Воно вертає шось схоже на команду uptime на ремоутній мошині, але не використовує ніяких екстернальних команд чи бібліотек. Якого розміру uptime-tcp-nolibc та скільки він їсть пам'яті написано ув кінці посту.
Якщо без libc немає жодного способу робити I/O чи відкривати сокета, тоді С стає фанкшонал мовою, про яку мріяли вчоні з 80х років, але, на їх невдачу, кернела набув IP стек та аналоги open(2) з write(2).
42.v2.c не вміє читати argv чи environ, а варіятивні хфункції у ньому роблять кордамп. Останне лікується
__attribute__((force_align_arg_pointer))
void _start() { ... }З argv все є набагато гірше: якщо мета є саме отримувати поінтера на
argv, потрібен знову осемблер (~14 рядків), який я тут постити не
буду, щоб не лякати людей. Технічно, прочитати argv можна просто
відкривши /proc/self/cmdline, ув якому \0 є роздільником аргументів:
$ tr \\0 \\n < /proc/self/cmdline
tr
\0
\nале вас засміють ув інтервебах.
Перша каменюка на шляху до сокетів лежить ув man-сторінках відповідних
libc враперів. Наприклад, bind(2) чекає на аргумента struct sockaddr *,
опис якого ув man-сторінці ip(7) був скопійований з документації
якоїсь 4.3BSD-Tahoe 1987 року і відповідає реальності нутрощів
лайнуксного сьогодення з висоти дзвіниці Києво-Печерської
лаври. (Потрібний додатковий паддінґ' unsigned char __pad[8].)
Якщо libc-врапери вертають -1 на помилку та виставляють errno, то сисколи вертають errno × -1. Ув справжніх libc errno є локальна для поточного треду. Для мікімаусного nolibc серверу писати свою версію позікс тредів це як збирати гелікоптер AK1-3 на подвір'ї поміж 2ма хрущовками, тому тут errno--глобальна змінна. Як зберігати інтерхфейса, до якого звикли всі, хто коли-небудь писав мережеві майкросервіси на C, то найпростіший враппер виглядає як
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
int r = my_syscall(43, sockfd, (long)addr, (long)addrlen, 0, 0, 0);
if (r < 0) {
errno = -r;
r = -1;
}
return r;
}Сигнали
Ув класичних Unix-серверах, якщо вони писалися не для inetd, конкурентна модель є форкова модель, яка вимагає прислуховуватися до SIGCHLD і чекати доки child завершить роботу, інакше після кожного ріквесту буде залишатися зомбі.
Форкова модель була популярна, тому що дозволяла серверу не лякатися сіґфолта, якщо сервер мав бага на який натрапив клайент: зупинявся лише child-невдаха, всі інші продовжували працювати.
Для встановлення колбеку на окремий сигнал лайнакс має свій rt_sigaction сискола.
Ув аргументах той має
поінтера на struct sigaction з дуже підступним філдом
void (*sa_restorer)(void);Ув man-сторінках про нього написано ось таке:
'This flag is used by C libraries [тобто, нами] to indicate that the
sa_restorerfield contains the address of a "signal trampoline".'… the C library's sigaction(2) wrapper function informs the kernel of the location of the trampoline code by placing its address in the
sa_restorerfield of the sigaction structure.'
Шматок з мікімаусного врапера:
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact) {
// ...
struct kernel_sigaction kact, koact;
kact.k_sa_handler = act->sa_handler;
memcpy(&kact.sa_mask, &act->sa_mask, sizeof(sigset_t));
// ...
kact.sa_restorer = &restore_rt; // THE MOST IMPORTANT STEP
int r = my_syscall(13, sig,
(long)(act?&kact:NULL), (long)(oact?&koact:NULL),
_NSIG/8, 0, 0);
// ...
}Звідкіля береться адреса хфункції для kact.sa_restorer? Це є та сама
"signal trampoline", про необхідність якої попереджає лайнаксна
документація. Звичайно, можна її не виставляти, але тоді жоден з
сигналів не буде пійманий і жоден з колбеків не буде виконаний.
Спочатку я наївно написав
void restore_rt() {
my_syscall(15, 0, 0, 0, 0, 0, 0); // 15 це rt_sigreturn
__builtin_trap();
}сигнали перехоплювалися, колбеки виконувалися,
СЄЧЄНОВ Цей гіпноз корисний дуже, У хазяйстві вєрно служить, Злакі мощно прорастають І надої возрастають!
але після завершення останніх сервер гепався через сіґфолта. На жаль, тут був потрібен осемблер знову:
__asm__(
".globl restore_rt\n"
"restore_rt:\n"
" movq $15, %rax\n"
" syscall\n"
" hlt\n"
);Чому не працює С-версія я не знаю. Клод ЕйАй вважає вона "may modify registers or the stack layout."
snprintf та gettimeofday
Життя без snprintf--життя варварів.
-- Невідомий філософ.
Ув src кернелу є приклад мінімалістичної vfprintf на 101 рядок, але
вона не підтримує floats, які має друкувати uptime. Замість додавання
їх підтримки, ліпше пошукати справжню snprintf, яка не використовує
malloc. Така є ув pkg Федори (із усіх можливих місць саме тут) і
називається stb_sprintf-devel. Це самотній .h файла, який
зкомпільовується ув 18168 байт. Жахливе марнування ресурсів, але
нічого не вдіяти. Тій хфункції потрібні va_start з друзями, але gcc та
clang мають __builtin_va_* ув комплекті:
#define va_start(v,l) __builtin_va_start(v,l)Після інкорпорації stb_sprintf життя заграло новими барвами.
Дізнатися час ув секундах з початку буту можна зі сисколом sysinfo, поточний час--зі gettimeofday. Останній провайдить навіть кількість хвилин на захід від Ґрінвічу! Така розкіш.
Фіналє
Розмір нестріпованого статичного байнарника: 21736 байт. (Я до сих пір не можу повірити як це можливо.)
$ make info
wc -lc *.[ch] | while read -r lines bytes file; do \
elf=-;\
echo $file | grep -q '\.c$' && elf=`stat -c%s _out/${file%.*}.o`;\
echo $file $lines $bytes $elf;\
done | column -t -N\ ,LINES,BYTES,ELF
LINES BYTES ELF
main.c 134 3657 5896
signal.c 76 1699 1704
signal.h 113 2649 -
snprintf.c 6 120 18168
snprintf.h 12 274 -
sockets.c 37 896 1728
sockets.h 45 1119 -
u.c 98 2152 4496
u.h 46 1353 -
uptime.c 67 2148 2128
uptime.h 6 79 -
total 640 16146 -Якби не stb_sprintf, розмір був би < 10KB.
Але це є не найбільша цікавинка. Ось кількість пам'яті, яку їсть такий конкурентний tcp-сервер після відповіді на 100 одночасних ріквестів:
$ sudo ps_mem -p `pgrep -f uptime-tcp-nolibc`
Private + Shared = RAM used Program
36.0 KiB + 4.5 KiB = 40.5 KiB uptime-tcp-nolibc
---------------------------------
40.5 KiBДуже сміюся. Ув сучасному світі, якщо споживання серверу, який робить 0 складного, < 80 _мегабайт_, то це є видатний вчинок і безстрашний прояв волі, за який треба сертифікат хоноровий, медаль та грошовий бонус для походу ув підпільне казино з повіями.

no subject
no subject
no subject
скальпеляldd - подивимося, що там за статика.no subject
$ ldd _out/uptime-tcp-nolibc not a dynamic executable $ file _out/uptime-tcp-nolibc _out/uptime-tcp-nolibc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ size _out/uptime-tcp-nolibc text data bss dec hex filename 18376 6 16 18398 47de _out/uptime-tcp-nolibcno subject
no subject
no subject
no subject
Потому что в стандартной за 50 лет нарос пресловутый блекджек с женщинами ста профессий. Позикс адаптеры, апи утилиты, варианты вызовов, эвристики, порты, багфиксы...
no subject
no subject
Голанг генерит бинарники не юзающие либц, а сразу сисколлы. На Винде тоже!
На Винде вообще с++ аналог либц превратилось в десяток msvcrt версий от разных версий вижал студий и версий с++. Как-то длл хелл разруливают.
no subject
no subject
no subject
https://en.wikipedia.org/wiki/Musl
no subject
немає там ніякого спрощення
всі ці musl та uclibc--товчення води ув ступі та безглуздий мотлох з 80х: по 4 функції для копіювання байтів, які роблять те саме з різною семантикою, і 0 нормальних функцій для хеш таблиць (за hsearch причетних треба відправити рубати ліс в Юконі).
no subject
no subject
$ cat test.c #include <stdio.h> #include <string.h> int main() { char buf[10]; strcpy(buf, "too long......................................................"); printf("%s\n", buf); } $ gcc test.c -o test-glibc -fstack-protector-all $ musl-gcc test.c -o test-musl -fstack-protector-all $ ./test-glibc too long...................................................... *** stack smashing detected ***: terminated Aborted (core dumped) $ ./test-musl too long...................................................... Segmentation fault (core dumped)це свіжа федора і musl 1.2.5
no subject
А ось Вам челлендж: розділити "сіамських близнюків", IPv4 і IPv6 у ядрі Linux. Одна швейцарська фірма за таку роботу платить $100 :D