Syscalls - немного теории

<aside> ❗

Syscall(Системный вызов) - фундаментальный интерфейс между программой и ядром ОС (в данном случае - Linux)

</aside>

Зачем все это и как работает?

По своей сути - программы довольно ограниченые, в плане доступа к той или иной информации и ресурсам. Они считаются очень подозрительными элемнтами, потенциально содержащими вредоносный код или просто говно-код . Именно поэтому любые действия по чтению файлов, записи в терминал или открытии TCP соединения происходят через ядро ОС.

Рассмотрим пример с getpid() (хотим получить process id, текущей программы)

image.png

Как показано на картинке, пользователю кажется, что мы имеем прямой доступ к этим данным. Однако getpid() является функцией стандартной библиотеки libc.

В таком случае getpid() можно назвать оберткой (wrapper) системного вызова. (open, read, write - все тоже wrapper’ы). Они созданы для проверки данных и корректной обработки. Это лишь слой защиты, чтобы не нести мусор в ядро. После чего прыгаем в kernel space — system_call()(этап 3).

Ядро использует номер syscall в качестве индекса в sys_call_table — массив из указателей функций для каждой реализации syscall. Здесь вызывается sys_getpid():

image.png

В Linux, syscall-реализации являются в основном архитектурно-независимыми функциями C, иногда тривиальными, изолированными от механизма syscall благодаря отличной конструкции ядра. Это обычный код, кроме того факта, что они полностью параноидальны по поводу проверки аргументов.

Как только их работа закончена, они возвращаются, и архитектурно-специфичный код заботится о переходе обратно в пользовательский режим, где обёртка делает некоторую постобработку. В нашем примере getpid(2) кэширует PID, возвращенный ядром. Другие wrapper могут установить глобальную переменную errno, если ядро возвращает ошибку. GNU cares about you

~/code/x86-os$ strace ./pid

execve("./pid", ["./pid"], [/* 20 vars */]) = 0
brk(0)                                  = 0x9aa0000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7767000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=18056, ...}) = 0
mmap2(NULL, 18056, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7762000
close(3)                                = 0

[...snip...]

getpid()                                = 14678
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 1), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7766000
write(1, "14678\\n", 614678
)                  = 6
exit_group(6)             

На этом примере можно увидеть системные вызовы pid 1000 раз. Да, здесь всего один getpid(), все потому что он кэшируется.

<aside> ❗

Системный вызвов можно вызвать напрямую, glibc предлагает функцию syscall(2), которая делает системный вызов без обёртки. Вы также можете сделать это самостоятельно в сборке. Нет ничего магического или привилегированного в библиотеке С.

</aside>

VDSO

The "vDSO" (virtual dynamic shared object) is a small shared library that the kernel automatically maps into the address space of all user-space applications.

VDSO (Virtual Dynamic Shared Object) - механизм обращения к ядру ОС без вызова syscall. Это сделано в тех случаях, если функции, к которым идет обращение - частые. В приложениях, где миллионы операций выполняются каждую секунду (например, базы данных или системы реального времени), даже минимальная задержка на syscall может существенно сказаться напроизводительности. Пример: gettimeofday