# Systémová volání, čili jediné dveře do jádra

Napíšeš `printf("ahoj\n")` a vypadá to jako jedna funkce. Jsou to tři vrstvy: `printf` je formátovač, ten zavolá `write`, `write` je obálka v libc a teprve ta provede jedinou instrukci procesoru, po které přestane běžet tvůj program a začne běžet jádro.

Mezi první a druhou vrstvou sedí vyrovnávací paměť. Na terminál posílá libc výstup po řádcích, do souboru a do roury si drží celý blok (typicky 4 KiB) a odešle ho, až se naplní nebo až program regulérně skončí.

Proto ti výpis zmizí, když program spadne dřív. `./program > log.txt`, uprostřed segfault, a v `log.txt` není nic, i když jsi „vypisoval" tisíc řádků. Ta data nikdy neopustila paměť procesu.

Rozdíl mezi „zavolal jsem funkci" a „požádal jsem jádro" je celá tahle stránka: jak přechod do jádra na x86-64 vypadá, co stojí a jak se sleduje. Co jádro s požadavkem dělá dál, patří na [vstup a výstup](Vstup-a-vystup) a na [souborový systém](Souborovy-system). Předpokládá [jádro a uživatelský režim](Jadro-a-uzivatelsky-rezim) - bez rozdílu ring 0 a ring 3 z toho nedává smysl nic.

## Proč nejde prostě zavolat funkci jádra

Jádro sídlí ve své vlastní části adresního prostoru a její stránky jsou označené jako privilegované. Procesor v ring 3 na ně nesmí skočit ani je přečíst. Kdyby to šlo, ochrana paměti by neexistovala.

I kdybys adresu znal a směl na ni skočit, běžel bys pořád v uživatelském režimu - bez privilegovaných instrukcí.

Tohle je nejdůležitější věc na celé stránce: **systémové volání není volání funkce jádra. Je to řízený vstup do jádra na jediném předem daném místě.** Jádro nikdy neskočí tam, kam mu řekneš. Procesor sám přepne režim a sám nastaví ukazatel instrukcí na adresu, kterou si jádro zaregistrovalo při startu. Ty dodáváš jen číslo požadavku a argumenty.

Stejným mechanismem vstupují do jádra [přerušení a výjimky](Preruseni-a-vyjimky) - jen tam si o vstup neříká program.

## Co se stane při jednom `write`

```mermaid
sequenceDiagram
    participant P as Program
    participant L as libc
    participant J as Jádro
    participant O as Ovladač
    P->>L: volání printf
    L->>L: uložení do vyrovnávací paměti
    L->>J: instrukce syscall, v rax číslo 1
    Note over L,J: přechod z ring 3 do ring 0, zásobník jádra
    J->>J: kontrola argumentů a deskriptoru
    J->>O: zápis na zařízení
    O-->>J: počet zapsaných bajtů
    J-->>L: sysret, výsledek v rax
    L-->>P: návratová hodnota, případně -1 a errno
```

Zbytek sekce rozebírá dva kroky ze schématu: předání argumentů a cestu zpátky.

### Mechanika na x86-64

Číslo volání se dá do `rax`, argumenty do `rdi`, `rsi`, `rdx`, `r10`, `r8`, `r9` v tomhle pořadí. Pak se provede `syscall` a jádro se vrátí instrukcí `sysret`, která obnoví uživatelský režim.

Všimni si `r10` na čtvrtém místě. Volací konvence jazyka C tam má `rcx`, jenže do `rcx` ukládá `syscall` návratovou adresu. Proto **obálka v libc není kosmetika** - přerovnává registry.

Argumentů je nejvýš šest, protože registrů je šest. Sedmý by musel jádro číst z uživatelského zásobníku, což jde, ale je to dražší a musí se to ověřovat. `mmap` se do šesti přesně vejde; na 32bitovém x86 se do tehdy použitelných registrů nevešel, a proto tam vznikl `mmap2`.

**Když se něco do šesti nevejde, předává se ukazatel na strukturu.** Novější volání k němu přidávají i její velikost, aby šlo přidat položku bez nového čísla volání - tak vzniklo `clone3` i `openat2`.

### Návratová hodnota a errno

Jádro vrací jedno číslo v `rax`. Nezáporné znamená úspěch - u `write` počet zapsaných bajtů. Záporné v rozsahu -1 až -4095 je přímo záporný kód chyby: `-EINVAL` je `-22`, `-ENOENT` je `-2`.

Obálka v libc to přeloží: zápornou hodnotu z chybového rozsahu otočí, uloží do `errno` a vrátí `-1`. **`errno` tedy nastavuje knihovna, ne jádro.** Je to obyčejná proměnná v paměti procesu, kterou jádro nikdy nevidělo.

Z toho plyne pravidlo, na kterém padají hodiny ladění: **`errno` má smysl číst jen po volání, které ohlásilo chybu.** Po úspěšném v něm může být cokoli - obálka ho nenuluje a zůstane tam hodnota z něčeho, co selhalo dřív uvnitř libc.

## Jak se do jádra vstupuje a jak se do něj nechodí vůbec

Instrukce, kterou dnes použiješ, je jedna. Zbylé dvě potkáš v návodech - a nejrychlejší cesta do jádra nevede vůbec.

### Historie, která pořád straší v návodech

Původní cestou na 32bitovém x86 bylo softwarové přerušení `int 0x80`. Procesor při něm prochází tabulku deskriptorů přerušení jako u hardwarového přerušení, což je výrazně dražší než dnešní specializované instrukce. Intel proto přidal `sysenter`, AMD `syscall`, a to je dnes na x86-64 jediná správná odpověď.

`int 0x80` v návodech pořád je, protože textů z doby 32bitového Linuxu je na internetu víc a nikdo je nemaže. Na x86-64 ti dokonce může projít, má-li jádro zapnutou 32bitovou emulaci - a to je horší, než kdyby spadlo. Trefí **32bitovou tabulku s jinými čísly**: `write` je tam čtyřka, ne jednička, a argumenty se osekají na 32 bitů. Číslo volání není nic univerzálního, je to položka v tabulce architektury - na ARM64 má `write` číslo 64.

### vDSO: volání, které do jádra vůbec nedojde

Zavolej `clock_gettime` v cyklu a změř to. Vyjdou ti desítky nanosekund, míň než vstup do jádra. Není v tom podvod - do jádra se nešlo.

Jádro namapuje do každého procesu malou sdílenou knihovnu jménem vDSO (virtual dynamic shared object) a k ní stránku dat, do které samo průběžně zapisuje čas. Funkce ve vDSO ta data jen přečtou. Vejde se tam `clock_gettime`, `gettimeofday` a od jádra 6.11 (rok 2024) i `getrandom`.

```bash
cat /proc/self/maps | grep vdso   # ukáže, kam je vDSO v procesu namapované
```

Uvidíš jeden řádek s právy `r-xp` - kód mapovaný přímo do tvého procesu.

## Kolik jedno volání stojí

Přechod tam a zpět stojí zhruba 50 až 100 ns. Pro srovnání: přístup do RAM je asi 80 ns a [přepnutí kontextu](Prepinani-kontextu) jednotky mikrosekund. Volání je drahé proti běžné funkci a levné proti všemu, co sáhne na disk.

**To číslo ale není konstanta daná hardwarem.** Jak se dá změnit, ukazuje následující sekce.

Cesta ven je nevolat tolikrát. `io_uring` (od jádra 5.1, rok 2019) jsou dvě kruhové fronty sdílené mezi procesem a jádrem: požadavky se do jedné zapisují, výsledky se z druhé čtou a jedním vstupem do jádra se jich odbaví klidně sto. Dřív se to řešilo přes `epoll`, jenže ten šetří jen dotazování na připravenost - `read` a `write` se volaly dál po jednom.

**Doporučení: neřeš to, dokud ti to neukáže měření.** Když `strace -c` ukáže v jádře jednotky procent času, je přepis na `io_uring` jen přidaná složitost. Když leze z programu milion `read` po 512 bajtech, oprav velikost bloku.

## Rok 2018 a účet, který platíš dodnes

V lednu 2018 byly zveřejněné Meltdown a Spectre - chyby ve spekulativním provádění, kvůli kterým šlo z uživatelského režimu číst paměť jádra. Netýkaly se jednoho výrobce, ale procesorů, které byly tehdy všude.

Oprava Meltdownu v Linuxu se jmenuje KPTI: jádro už není namapované v tabulkách stránek běžícího procesu, takže se při každém vstupu a návratu musí přepnout tabulka stránek. Přesně to, čemu se návrh systémových volání dlouhá léta vyhýbal.

Škoda nebyla v uniklých datech, ale ve výkonu - volání se na některých strojích prodloužilo ke 500 ns a zátěže s velkým počtem volání ztratily i desítky procent. Poučení: **cena vstupu do jádra není daná fyzikou, je to rozhodnutí, které ti může někdo změnit bezpečnostní záplatou.**

## „Nerozbíjíme uživatelský prostor"

Rozhraní systémových volání je nejtvrdší závazek v Linuxu. Vnitřek jádra se přepisuje volně, ale to, co vidí uživatelský program, se rozbít nesmí.

Rozdíl mezi API a ABI je přesně tady. **API je `fopen`** - dohoda na úrovni zdrojového kódu, kterou drží libc. **ABI je to, že `write` má na x86-64 číslo 1** - dohoda na úrovni strojového kódu.

Kdyby se čísla přerovnala, každá přeložená binárka na světě by při `write` trefila něco jiného. Proto se stará volání nemění a přidávají se nová s vlastním číslem: `openat` má 257, jeho novější varianta `openat2` dostala číslo úplně jinde.

## Jak se na to díváš v praxi

```bash
strace -c ls                        # sečte volání podle typu a čas strávený v nich
strace -f -e trace=openat ./program # jen otevírání souborů, včetně potomků (-f)
ltrace ./program                    # volání do sdílených knihoven, ne do jádra
```

`strace` se připojí přes `ptrace` a vidí přesně to, co prošlo hranicí do jádra. `ltrace` odchytává volání do sdílených knihoven - uvidíš v něm `printf` a `malloc`, které do jádra nejdou, a u staticky slinkovaného programu neukáže nic.

Obojí zpomaluje program řádově, takže **`strace` je nástroj na zjištění co, ne na měření jak dlouho**. Na to je `perf` z [nástrojů a diagnostiky](Nastroje-a-diagnostika).

### seccomp jako seznam povolených volání

`seccomp` je filtr, který procesu dovolí jen vyjmenovaná volání. Nastaví se jednou, dědí se do potomků a nedá se vypnout.

```bash
grep Seccomp /proc/self/status   # 0 = vypnuto, 2 = běží filtr
```

Na tomhle stojí izolace v Dockeru, jehož výchozí profil blokuje desítky volání, i sandbox v prohlížečích - podrobněji u [kontejnerů](Virtualizace-a-kontejnery) a [práv](Prava-a-uzivatele).

## Když se to rozbije

| Příznak | Kde je problém |
|---|---|
| Program „nic nedělá", `strace` stojí v `read` | nic není rozbité, volání čeká na data |
| `EACCES` | nesedí práva na souboru nebo na adresáři v cestě |
| `EPERM` | práva sedí, procesu chybí oprávnění |
| `ENOENT` u souboru, který existuje | chybí interpret nebo sdílená knihovna |
| `EINTR` uprostřed čtení | přišel signál, volání se přerušilo - opakuj ho |
| Stovky `openat` při startu | hledání knihoven po zbytečně dlouhé cestě |

**`EACCES` versus `EPERM`.** U `EACCES` rozhodly bity `rwx` - špatný vlastník nebo chybějící `x` na adresáři v cestě. `EPERM` znamená, že práva sedí a zakázal to někdo jiný: chybějící capability, seccomp filtr, operace jen pro roota.

**`ENOENT` na soubor, který vidíš.** Klasika je `./program: No such file or directory` u binárky, která tam je. Volání `execve` neselhalo na ní, ale na dynamickém linkeru nebo na interpretu ze shebangu. `file` a `ldd` to ukážou hned.

## Co si odnést

**Systémové volání je řízený vstup na jediné pevné místo.** Adresu si nevybíráš, dodáváš číslo a argumenty.

**Mezi `printf` a jádrem je vyrovnávací paměť.** Když program spadne, její obsah je pryč.

**Číslo v `rax`, argumenty v `rdi rsi rdx r10 r8 r9`, nejvýš šest.** Víc se předává ukazatelem na strukturu.

**Jádro vrací záporný kód chyby, `errno` nastavuje libc.** Po úspěšném volání ho nečti.

**vDSO je důvod, proč čtení času skoro nic nestojí.** Do jádra se vůbec nejde.

**Cena volání se může změnit záplatou.** KPTI to v roce 2018 předvedla naráz na celém světě.

**Čísla volání se nepřečíslují.** To je celý rozdíl mezi API a ABI.

## Kam dál

- **[Jádro a uživatelský režim](Jadro-a-uzivatelsky-rezim)** - co se při vstupu přepíná a proč to hlídá hardware
- **[Přerušení a výjimky](Preruseni-a-vyjimky)** - druhá cesta do jádra, bez tvého přičinění
- **[Vstup a výstup](Vstup-a-vystup)** - co se s `write` děje potom
- **[Nástroje a diagnostika](Nastroje-a-diagnostika)** - `strace`, `perf` a `/proc` v souvislostech
- **[Slovníček](Slovnicek)** - ABI, vDSO a další zkratky
