Хабрахабр

Необычные системные вызовы на Linux

ls /usr/share/man/man2/

Он видит fopen, printf, scanf и ещё много других функций. Что видит программист, начиная работать с языком C? Но, в отличие от первой группы, эти две функции при выполнении на ядре Linux являются системными вызовами (на самом деле нет, почти никогда системный вызов нельзя просто вызвать как функцию, и поэтому libc содержит обёртки, перепаковывающие аргументы и иногда, как в случае с тем же open, заменяющие старые системные вызовы более общими новыми). Видит он и всякие open и mmap — казалось бы, зачем их выделять? Вообще, в отличие от тысяч библиотечных функций, доступных на типичной GNU/Linux системе, интерфейс ядра имеет довольно ограниченное количество точек входа — порядка нескольких сотен, зато то, что для user space — crash (например, обращение к отсутствующей странице), для ядра — default mode of operation.

В ней не будет futex-ов и прочих скучных (наверное) деталей реализации. В этой статье я расскажу некоторые интересные на мой взгляд факты. Будет преимущественно то, что вызывало у меня реакцию «А что, так можно было?!?».

Таких функций немного (что-то около четырёх, но конкретное количество, видимо, может зависеть от версии ядра и архитектуры) — это всякие time и gettimeofday, которые, с одной стороны, часто используются, а с другой — их удалось реализовать без переключения в контекст ядра. Во первых, некоторые комментарии к тексту до ката: некоторые системные вызовы имеют опциональный интерфейс в виде функции из shared object под названием vDSO, подкладываемого ядром в процесс.

Во-вторых, не всегда SIGSEGV заканчивается крешем процесса, но об этом мы ещё поговорим, когда речь зайдёт о userfaultfd.

Это нормально, если таким способом вы делаете опциональную оптитимизацию для конкретного типа систем или дополнительную возможность, которой иначе бы просто не было. DISCLAIMER: помните, что используя большинство из представленных здесь возможностей, вы завязываете свою программу на Linux. Но в противном случае рекомендую подумать о том, как сделать кросс-платформенный fallback.

Общие вопросы

Конечно же, нам поможет strace! Для начала, как можно всё это отлаживать? Если strace достаточно свежий, то с помощью параметра -k его можно попросить выдавать стек вызовов. Поскольку набор системных вызовов ограничен, и большинство strace знает «в лицо», то он покажет не просто «передан указатель 0x12345678», а опишет, что в этой структуре передаётся в ту или иную сторону.

Выглядит это как-то так

$ strace -k sleep 1
execve("/bin/sleep", ["sleep", "1"], 0x7ffe9f9cce30 /* 60 vars */) = 0 > /lib/x86_64-linux-gnu/libc-2.30.so(execve+0xb) [0xe601b] > /usr/bin/strace(+0x0) [0xa279c] > /usr/bin/strace(+0x0) [0xa41d2] > /usr/bin/strace(+0x0) [0x7090b] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3] > /usr/bin/strace(+0x0) [0x7112a]
brk(NULL) = 0x558936ded000 > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x20b) [0x1ccdb] > /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1cd2) [0x1b872] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
arch_prctl(0x3001 /* ARCH_??? */, 0x7fff593c0070) = -1 EINVAL (Недопустимый аргумент) > /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e25) [0x1b9c5] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Нет такого файла или каталога) > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x10cb) [0x1db9b] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c12] > /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x1238) [0x1dd08] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_debug_state+0x73a) [0x11d4a] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_exception_free+0x908) [0x189c8] > /lib/x86_64-linux-gnu/ld-2.30.so() [0xa362] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55] > /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
fstat(3, ) = 0 > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x1009) [0x1dad9] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_debug_state+0x761) [0x11d71] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_exception_free+0x908) [0x189c8] > /lib/x86_64-linux-gnu/ld-2.30.so() [0xa362] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55] > /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
mmap(NULL, 254851, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc49621c000 > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x1426) [0x1def6] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_debug_state+0x79d) [0x11dad] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_exception_free+0x908) [0x189c8] > /lib/x86_64-linux-gnu/ld-2.30.so() [0xa362] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55] > /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
close(3) = 0 > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x10fb) [0x1dbcb] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_debug_state+0x780) [0x11d90] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_exception_free+0x908) [0x189c8] > /lib/x86_64-linux-gnu/ld-2.30.so() [0xa362] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55] > /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x1238) [0x1dd08] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x7d40] > /lib/x86_64-linux-gnu/ld-2.30.so() [0xa3a8] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55] > /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108]
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360r\2\0\0\0\0\0"..., 832) = 832 > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_error+0x12f8) [0x1ddc8] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x7d79] > /lib/x86_64-linux-gnu/ld-2.30.so() [0xa3a8] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x41b5) [0xeb35] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_catch_exception+0x65) [0x1ca85] > /lib/x86_64-linux-gnu/ld-2.30.so(_dl_rtld_di_serinfo+0x4603) [0xef83] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x3c55] > /lib/x86_64-linux-gnu/ld-2.30.so(__get_cpu_features+0x1e7b) [0x1ba1b] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x203c] > /lib/x86_64-linux-gnu/ld-2.30.so() [0x1108] ...
Много динамической линковки
... brk(NULL) = 0x558936ded000 > /lib/x86_64-linux-gnu/libc-2.30.so(brk+0xb) [0x11755b] > /lib/x86_64-linux-gnu/libc-2.30.so(__sbrk+0x67) [0x117617] > /lib/x86_64-linux-gnu/libc-2.30.so(__default_morecore+0xd) [0x9fd3d] > /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x2725) [0x9a745] > /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x3943) [0x9b963] > /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x3b2b) [0x9bb4b] > /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x4d9e) [0x9cdbe] > /lib/x86_64-linux-gnu/libc-2.30.so(textdomain+0x740) [0x3be70] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1d35) [0x35515] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5] > /bin/sleep() [0x25f0] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3] > /bin/sleep() [0x287e]
brk(0x558936e0e000) = 0x558936e0e000 > /lib/x86_64-linux-gnu/libc-2.30.so(brk+0xb) [0x11755b] > /lib/x86_64-linux-gnu/libc-2.30.so(__sbrk+0x91) [0x117641] > /lib/x86_64-linux-gnu/libc-2.30.so(__default_morecore+0xd) [0x9fd3d] > /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x2725) [0x9a745] > /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x3943) [0x9b963] > /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x3b2b) [0x9bb4b] > /lib/x86_64-linux-gnu/libc-2.30.so(thrd_yield+0x4d9e) [0x9cdbe] > /lib/x86_64-linux-gnu/libc-2.30.so(textdomain+0x740) [0x3be70] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1d35) [0x35515] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5] > /bin/sleep() [0x25f0] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3] > /bin/sleep() [0x287e]
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3 > /lib/x86_64-linux-gnu/libc-2.30.so(__open64_nocancel+0x4c) [0x11679c] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1ce9) [0x354c9] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5] > /bin/sleep() [0x25f0] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3] > /bin/sleep() [0x287e]
fstat(3, {st_mode=S_IFREG|0644, st_size=8994080, ...}) = 0 > /lib/x86_64-linux-gnu/libc-2.30.so(__fxstat64+0x19) [0x1107b9] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1e33) [0x35613] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5] > /bin/sleep() [0x25f0] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3] > /bin/sleep() [0x287e]
mmap(NULL, 8994080, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc495795000 > /lib/x86_64-linux-gnu/libc-2.30.so(mmap64+0x26) [0x11baf6] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1e5d) [0x3563d] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5] > /bin/sleep() [0x25f0] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3] > /bin/sleep() [0x287e]
close(3) = 0 > /lib/x86_64-linux-gnu/libc-2.30.so(__close_nocancel+0xb) [0x1165bb] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x1eab) [0x3568b] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0xbdf) [0x343bf] > /lib/x86_64-linux-gnu/libc-2.30.so(setlocale+0x215) [0x339f5] > /bin/sleep() [0x25f0] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3] > /bin/sleep() [0x287e]
nanosleep({tv_sec=1, tv_nsec=0}, NULL) = 0 > /lib/x86_64-linux-gnu/libc-2.30.so(nanosleep+0x17) [0xe5d17] > /bin/sleep() [0x5827] > /bin/sleep() [0x5600] > /bin/sleep() [0x27b0] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xf3) [0x271e3] > /bin/sleep() [0x287e]
close(1) = 0 > /lib/x86_64-linux-gnu/libc-2.30.so(__close_nocancel+0xb) [0x1165bb] > /lib/x86_64-linux-gnu/libc-2.30.so(_IO_file_close_it+0x70) [0x92fc0] > /lib/x86_64-linux-gnu/libc-2.30.so(fclose+0x166) [0x85006] > /bin/sleep() [0x5881] > /bin/sleep() [0x2d27] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_secure_getenv+0x127) [0x49ba7] > /lib/x86_64-linux-gnu/libc-2.30.so(exit+0x20) [0x49d60] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xfa) [0x271ea] > /bin/sleep() [0x287e]
close(2) = 0 > /lib/x86_64-linux-gnu/libc-2.30.so(__close_nocancel+0xb) [0x1165bb] > /lib/x86_64-linux-gnu/libc-2.30.so(_IO_file_close_it+0x70) [0x92fc0] > /lib/x86_64-linux-gnu/libc-2.30.so(fclose+0x166) [0x85006] > /bin/sleep() [0x5881] > /bin/sleep() [0x2d4d] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_secure_getenv+0x127) [0x49ba7] > /lib/x86_64-linux-gnu/libc-2.30.so(exit+0x20) [0x49d60] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xfa) [0x271ea] > /bin/sleep() [0x287e]
exit_group(0) = ?
+++ exited with 0 +++ > /lib/x86_64-linux-gnu/libc-2.30.so(_exit+0x36) [0xe5fe6] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_secure_getenv+0x242) [0x49cc2] > /lib/x86_64-linux-gnu/libc-2.30.so(exit+0x20) [0x49d60] > /lib/x86_64-linux-gnu/libc-2.30.so(__libc_start_main+0xfa) [0x271ea] > /bin/sleep(+0x0) [0x287e]

Вам поможет addr2line (если эта информация в принципе присутствует, конечно). Правда, тут не отображаются имена файлов с исходным кодом и номера строк.

Тогда можно воспользоваться универсальной обёрткой под названием syscall: Есть и второй вопрос: некоторые системные вызовы не имеют обёрток в libc.

syscall(SYS_kcmp, getpid(), getpid(), KCMP_FILE, 1, fd)

Файл — это очень уж странный предмет...

Это ещё и универсальное API, понятное всем библиотекам в системе. Системные вызовы — это не только способ попросить ядро обратиться к оборудованию от имени процесса. К тому же, часть «настроек» процесса наследуется при execve, поэтому таким образом можно попробовать обойтись без сложных костылей, просто правильно сформировав состояние перед запуском процесса (что-то вроде «зачем вручную перекладывать stderr в файл, если можно просто открыть файл и сделать его FD #2 для дочернего процесса»). Значит, если в библиотеке не поддержали нужную вам функциональность, возможно, она автоматически получится, если правильно попросить ядро.

В какой-то момент количество костылей превысило все разумные пределы, и я решил, что вряд ли libpcap будет сложнее, чем то, что я написал, к тому же, это стандарт, и для открытия этих файлов существуют общепринятые инструменты. Как-то раз мне потребовалось вычитать из файла последовательность сетевых пакетов. Всё! Оказалось, что пользоваться libpcap для чтения дампов примерно настолько же сложно, как и fopen для чтения файлов: вы просто открываете дамп с помощью pcap_(f)open_offline и вычерпываете пакеты через pcap_next_ex. Ну, ещё стоит закрыть дамп по завершению работы...

Может, и умеет конечно, если покопаться, но для нашей «лабораторки» представим, что не умеет. Но вот незадача: похоже, libpcap не умеет читать из памяти.

Я понимаю, что можно использовать буферизованный ввод и какой-нибудь ungetc (поскольку libpcap всё равно требует FILE *), но в общем случае мы, может, на ходу распаковываем, например, или библиотека может непосредственно работать с read / write. Итак, модельный пример: мы ждём на stdin некую последовательность байтов, после которой идёт выровненный на 4 байта дамп.

Решение 1: memfd_create

Файл находится в памяти и существует, пока на него открыт хоть один дескриптор. Системный вызов memfd_create позволяет создать «вообще анонимный» файловый дескриптор. В простейшем случае, вы просто получаете такой дескриптор, записываете в него данные через write, перематываете lseek, и с помощью fdopen даёте о нём знать libc:

int fd = memfd_create("pcap-dump-contents", 0); write(fd, buf, length); lseek(fd, 0, SEEK_SET); FILE *file = fdopen(fd, "r");

Имя файла, передаваемое первым аргументом, будет отображаться в символьной ссылке в /proc/<PID>/fd:

$ ls -l /proc/31747/fd
итого 0
lr-x------ 1 trosinenko trosinenko 64 ноя 10 13:12 0 -> /path/to/128test.pcap
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 1 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 2 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 23 -> '/home/trosinenko/.cache/appstream-cache-AH3OA0.mdb (deleted)'
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 3 -> '/memfd:pcap-dump-contents (deleted)'
lrwx------ 1 trosinenko trosinenko 64 ноя 10 13:12 57 -> 'socket:[41036]'

Решение 2: open с флагом O_TMPFILE

В итоге файл, как (приблизительно) говаривал один литературный персонаж, вроде он есть, но его как бы нет… Пишутся ли данные на диск — не знаю, но наверное, это зависит и от файловой системы (кстати, она должна поддерживать этот режим). В Linux, начиная с какой-то версии, при создании файла можно указать опцию O_TMPFILE и имя каталога вместо имени файла. Файл всё так же исчезает при закрытии последней ссылки, но его можно прикрепить к дереву каталогов с помощью linkat:

int fd = open(".", O_RDWR | O_TMPFILE, S_IRUSR | S_IWUSR); assert(fd != -1); assert(write(fd, buffer + offset, len - offset) == len - offset); assert(lseek(fd, 0, SEEK_SET) == 0); const char *link_to = getenv("LINK_TO"); if (link_to != NULL) { char path[128]; snprintf(path, sizeof(path), "/proc/self/fd/%d", fd); linkat(AT_FDCWD, path, AT_FDCWD, link_to, AT_SYMLINK_FOLLOW); }

д., а потом атомарно прилинковать в дерево каталогов. Кроме возможности не мучаться с именованием файла, это даёт возможность заполнить файл, настроить права и т.

Пример (для обоих подходов)

#define _GNU_SOURCE #ifdef NDEBUG
// Чтобы спокойно использовать assert с сайд-эффектами в примерах
# undef NDEBUG
#endif #include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> #include <pcap.h> // Идентификатор файла в формате PCAP (один из возможных -- см. спецификацию)
static const uint32_t pcap_mgc = 0xA1B2C3D4; char buffer[1 << 20]; int main()
{ int len = read(0, buffer, sizeof(buffer)); // По какой-то причине у нас сначала идёт "мусор", // который никогда не содержит pcap_mgc, а потом // выровненный на 4 байта дамп. Просто для примера... int offset = -1; for (int i = 0; i < len; i += 4) { if (*(uint32_t *)(buffer + i) == pcap_mgc) { offset = i; break; } } if (offset >= 0) { printf("Found PCAP dump at offset %d\n", offset); } else { fprintf(stderr, "No PCAP dump found.\n"); exit(1); } // Теперь сделаем так, чтобы libpcap считал, что читает // данные из файла.
#if 0 int fd = memfd_create("pcap-dump-contents", 0); assert(fd != -1); assert(write(fd, buffer + offset, len - offset) == len - offset); assert(lseek(fd, 0, SEEK_SET) == 0);
#else int fd = open(".", O_RDWR | O_TMPFILE, S_IRUSR | S_IWUSR); assert(fd != -1); assert(write(fd, buffer + offset, len - offset) == len - offset); assert(lseek(fd, 0, SEEK_SET) == 0); const char *link_to = getenv("LINK_TO"); if (link_to != NULL) { char path[128]; snprintf(path, sizeof(path), "/proc/self/fd/%d", fd); linkat(AT_FDCWD, path, AT_FDCWD, link_to, AT_SYMLINK_FOLLOW); }
#endif raise(SIGSTOP); // Чтобы посмотреть в /proc/PID/fd/ // Теперь попробуем открыть дамп и что-нибудь вывести... FILE *file = fdopen(fd, "r"); char errbuf[PCAP_ERRBUF_SIZE]; pcap_t * dump = pcap_fopen_offline(file, errbuf); assert(dump != NULL); struct pcap_pkthdr *hdr; const uint8_t *data; while (pcap_next_ex(dump, &hdr, &data) == 1) { printf("Read packet: full length = %d bytes, available %d bytes.\n", hdr->len, hdr->caplen); } return 0;
}

$ fallocate -l 128 zero128
$ cat zero128 test.pcap > 128test.pcap
$ ./memfd < 128test.pcap
Found PCAP dump at offset 128
Read packet: full length = 105 bytes, available 105 bytes.
Read packet: full length = 105 bytes, available 105 bytes.
Read packet: full length = 66 bytes, available 66 bytes.
Read packet: full length = 385 bytes, available 385 bytes.
Read packet: full length = 66 bytes, available 66 bytes.
...

userfaultfd: обработка ошибок памяти в userspace

Например, на Linux это может быть сокет, pipe, eventfd или даже ссылка на ebpf-программу. Думаю, не будет чего-то сильно нового в том, чтобы сказать, что в UNIX-like системах файловые дескрипторы на что только не указывают. В начале статьи я говорил о том, что для ядра page faults — обычное дело: своп, copy-on-write, вот это всё… Когда же пользовательский процесс «промахивается», ему отправляется SIGSEGV. Но, возможно, этот пример вас всё-таки удивит. Не так давно в Linux появился абсолютно задокументированный способ, называется userfaultfd: с помощью одноимённого системного вызова вы открываете файловый дескриптор, чтение и запись в который специальных структур являются командами. Насколько я знаю, возврат управления из обработчика SIGSEGV, сгенерированного ядром, является undefined behavior, и тем не менее, существует библиотека GNU libsigsegv, обобщающая особенности обработки ошибки доступа к памяти на различных платформах, даже Windows (ВНИМАНИЕ: лицензия GPL, если не готовы под ней же распространять свою программу, то не используйте libsigsegv).

После этого при первом обращение к каждой помеченной странице памяти, обратившийся поток заснёт, а чтение из файлового дескриптора вернёт информацию о произошедшем. Имея такой файловый дескриптор, вы можете пометить некий диапазон виртуальных адресов вашего процесса. При этом предполагается наличие отдельного потока, в чьи обязанности входит чтение команд из дескриптора и выдача ответов. После чего обработчик заполнит ответную структуру с указателем на данные, которые нужно использовать для инициализации «проблемной» страницы, ядро её проинициализирует и разбудит обратившийся поток. Вообще, говоря, через userfaultfd можно получать и другую информацию, например, некоторые уведомления об изменении карты виртуальной памяти процесса.

Пример использования

#define _GNU_SOURCE #ifdef NDEBUG
// Чтобы спокойно использовать assert с сайд-эффектами в примерах
# undef NDEBUG
#endif #include <linux/userfaultfd.h>
#include <syscall.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <pthread.h>
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // По-хорошему, нужно спросить sysconf...
#define PAGE_SIZE 4096
#define PAGE_MASK (PAGE_SIZE - 1) static void *thread_fn(void * arg)
{ int uffd = (intptr_t)arg; struct uffd_msg msg; // Интересно, как заработает с hugepages... uint8_t *replacement_page = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 ,0); while(1) { assert(read(uffd, &msg, sizeof msg) > 0); // Бывают разные типы сообщений, обрабатываем только одно if (msg.event == UFFD_EVENT_PAGEFAULT) { uintptr_t addr = msg.arg.pagefault.address; fprintf(stderr, "Fault: addr = 0x%zx\n", addr); // Округляем адрес вниз до размера страницы uint8_t *page_addr = (uint8_t *)((uintptr_t)addr & ~PAGE_MASK); // Заполняем "подменную" страницу, не "сбойную"! memset(replacement_page, 0xAB, PAGE_SIZE); // просим ядро инициализировать страницу struct uffdio_copy copy; copy.src = (uintptr_t)replacement_page; copy.dst = (uintptr_t)page_addr; copy.mode = 0; // флаги, здесь -- по умолчанию copy.copy = 0; // возвращаемое значение -- сколько байтов скопировали или ошибка copy.len = PAGE_SIZE; assert(ioctl(uffd, UFFDIO_COPY, &copy) != -1); } }
} static int init_userfaultfd(void)
{ // Открываем дескриптор int uffd = syscall(__NR_userfaultfd, 0); // Говорим, поддержку чего мы от него ожидаем struct uffdio_api api; api.api = UFFD_API; api.features = 0; assert(ioctl(uffd, UFFDIO_API, &api) != -1); fprintf(stderr, "UFFD open\n"); // Запускаем поток-обработчик pthread_t thread; memset(&thread, 0, sizeof(thread)); // Хмм... допустимо ли хранить int в void *? pthread_create(&thread, 0, thread_fn, (void *)(intptr_t)uffd); return uffd;
} static void register_region(int uffd, void * aligned_addr, size_t size)
{ struct uffdio_register reg; memset(&reg, 0, sizeof reg); reg.range.start = (uintptr_t)aligned_addr; reg.range.len = size; reg.mode = UFFDIO_REGISTER_MODE_MISSING; assert (ioctl(uffd, UFFDIO_REGISTER, &reg) != -1);
} int main()
{ void *addr = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); int uffd = init_userfaultfd(); register_region(uffd, addr, PAGE_SIZE); fprintf(stderr, "Before reading\n"); fprintf(stderr, "Data at %p: %x\n", addr, *(volatile int *)addr); return 0;
}

$ ./userfaultfd
UFFD open
Before reading
Fault: addr = 0x7f46f40d5000
Data at 0x7f46f40d5000: abababab

«Ключевой вопрос математики: не всё ли равно» ©

Казалось бы, if (fd == 0) ... — и все дела. Что, если вам нужно узнать, ссылается ли этот файловый дескриптор на stdin? Ну, окей...

#define _GNU_SOURCE #include <unistd.h>
#include <stdio.h> int main()
{ int fd = dup(0); printf("stdin is fd %d, too\n", fd); if (fd == 0) printf("stdin"); else printf("not stdin"); return 0;
}

$ gcc kcmp.c -o kcmp
$ ./kcmp
stdin is fd 3, too
not stdin

Нам поможет CRIU — Checkpoint/Restore In Userspace. Упс… Дескриптор-то вроде как один, но алиасы разные. Просто для нужд своих userspace-инструментов, разработчики из этого проекта, как я понял, добавили в ядро системный вызов kcmp: ему передаются два PID, тип ресурса и, собственно, два ресурса, а он говорит, указывают ли они на одну и ту же сущность ядра: Без паники, я не предлагаю сдампить процесс, посмотреть на результат и загрузить обратно.

#define _GNU_SOURCE #include <linux/kcmp.h>
#include <syscall.h>
#include <unistd.h>
#include <stdio.h> int main()
{ int fd = dup(0); printf("stdin is fd %d, too\n", fd); int pid = getpid(); if (syscall(SYS_kcmp, pid, pid, KCMP_FILE /* не путать с _FILES! */, 0 /* stdin fd */, fd) == 0) printf("stdin\n"); else printf("not stdin\n"); if (syscall(SYS_kcmp, pid, pid, KCMP_FILE, 1 /* stdout fd */, fd) == 0) printf("stdout\n"); else printf("not stdout\n"); return 0;
}

$ ./kcmp
stdin is fd 3, too
stdin
stdout

Подождите ка, а что если... Опять двадцать пять!

$ ls -l /proc/self/fd
итого 0
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 0 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 1 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 2 -> /dev/pts/17
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 23 -> '/home/trosinenko/.cache/appstream-cache-AH3OA0.mdb (deleted)'
lr-x------ 1 trosinenko trosinenko 64 ноя 10 14:45 3 -> /proc/17265/fd
lrwx------ 1 trosinenko trosinenko 64 ноя 10 14:45 57 -> 'socket:[41036]'

Логично считать, что bash точно так же поступает и с моей программой, как и с ls! Ага, тот неловкий момент, когда пытаешься понять, где ошибка, а она оказалась в твоём понимании происходящего.

$ ./kcmp < kcmp.c
stdin is fd 3, too
stdin
not stdout

Не буду утверждать, что это работает всегда и идеально — для этого нужно самому его использовать много и по-разному, но как best effort инструмент для каких-нибудь эвристик наверняка может пригодиться.

Обо всём и понемножку

Знаете ли вы, ...

  • … что ядро содержит в себе JIT-компилятор для байткода из userspace? Так вот, для целей фильтрации пакетов и прочей трассировки ядро поддерживает eBPF-байткод. В процессе загрузки он проверяется на безопасность, терминируемость (а значит, как я понимаю, даже теоретически не может быть Тьюринг-полным) и т. д., после чего либо JIT-ится, либо интерпретируется. Кстати, не путайте его с его предшественником, BPF.
  • … что обработчики сигналов можно запускать на отдельном стеке? Если нет, то вот описание системного вызова sigaltstack.
  • … что можно просто предупредить ядро о том, что некоторый файл пригодится позже: readahead

А ещё в процессе просмотра списка системных вызовов я нашёл oldolduname...

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Проверьте также

Закрыть
Кнопка «Наверх»
Закрыть