System calls: del programa C al kernel

Com parla un programa amb el kernel?

Jordi Mateo Fornés

Objectius

Objectius d'Aprenentatge
  • Explicar per què un programa d’usuari no pot accedir directament als recursos i què és una system call.
  • Distingir API C (POSIX/libc) de ABI de syscall.
  • Identificar com la syscall ABI transporta el número, els arguments i el valor de retorn mitjançant registres a x86-64 i ARM64, i saber on consultar la convenció (man 2 syscall).
  • Explicar què fa (i què no fa) la instrucció syscall / svc.
  • Observar syscalls reals amb strace i gdb.

Pregunta central

Reflexió

Què passa realment quan un programa C necessita un servei del sistema operatiu?

Part 1 · Motivació

Què pot fer C sense demanar res al sistema operatiu?

int add(int a, int b)
{
    return a + b;
}
main()
  │
  └──► add()        ← una crida normal: call / ret
  • Tot passa dins de user space: mateix procés, mateixa pila, mateixos privilegis.
  • El kernel no intervé.

Això no ho podem fer directament des de user space

write(fd, buffer, size);
Reflexió

Qui controla realment el fitxer o el dispositiu? Per què el programa no pot, simplement, escriure al disc?

  • Sí podem cridar write() des del nostre programa.
  • El que no podem és accedir directament al dispositiu o al subsistema de fitxers saltant-nos el kernel.

\[ \text{programa C} \xrightarrow{\text{write()}} \text{kernel} \xrightarrow{\text{sys_write()}} \text{filesystem / driver} \xrightarrow{\text{hardware}} \text{dispositiu} \]

Definició — System call

Interfície controlada entre user space i kernel space: els serveis que el kernel ofereix als programes d’usuari.

Protecció de la CPU: rings

  • La CPU distingeix nivells de privilegi: ring 0 (kernel) i ring 3 (programes) a x86; EL1 i EL0 a ARM64.
  • Certes instruccions privilegiades només s’executen al nivell del kernel: accés a dispositius, taules de pàgines, parar la CPU…
  • Si un programa d’usuari les intenta executar → excepció i el kernel el pot aturar.
Concepte — Conseqüència

Sense el kernel, un procés d’usuari no pot gestionar directament dispositius, fitxers ni la memòria física del sistema.

Rings de privilegi a x86 i ARM64.

Part 2 · libc, POSIX i syscalls

Però write() és una funció C?

write(fd, buffer, size);
Reflexió

Sembla una crida a funció. Ho és? I si ho és, per què diem que una syscall no és una crida a funció?

Solució

Sí, write() és una funció C: un wrapper de la libc. Són dues coses diferents encadenades.

API vs implementació · POSIX ≠ libc

Definició — API

Contracte a nivell de codi font: noms, tipus, semàntica. ssize_t write(int fd, const void *buf, size_t count);

Definició — POSIX

Especificació (estàndard) d’una API Unix. Diu què ha d’existir, no com es fa.

Definició — libc

Implementació de l’API C (i de gran part de POSIX) per a un sistema concret: glibc, musl…

Concepte — Punts que confonen
  • POSIX ≠ libc ≠ kernel.
  • La mateixa API POSIX pot acabar en syscalls diferents en sistemes diferents.
  • printf, fopen → libc (stdio) · write, open → wrappers de syscall.

Primera demo: strace

Exercici

Quantes syscalls creus que farà el programa hello?

Demo: printf() i buffering

Exemple

Mostra que printf() no és una syscall, sinó una funció de la libc que bufferitza la sortida i només arriba al kernel amb write. Amb strace -e trace=write sobre un programa que fa tres printf, si la sortida va al terminal es veuen 3 write de 8 bytes (una per línia), perquè stdout és habitualment line-buffered. Si la redirigim a un fitxer, en surt un de sol de 24 bytes, perquè llavors és habitualment fully buffered, i el buffer es buida al final.

Part 3 — API vs ABI

Tres nivells

  • read, write, close… són serveis del kernel exposats a través de la interfície de syscalls.
  • El nom C i la syscall no sempre coincideixen: open() acaba fent openat, fork() acaba fent clone.

Una syscall té una identitat

No n’hi ha prou amb dir write(): el kernel ha de saber quina syscall se li demana.

Definició — Syscall number

Enter que identifica cada servei del kernel. El programa el posa en un registre abans d’entrar al kernel.

  • Els números formen part de l’ABI d’una arquitectura i no s’han de considerar intercanviables entre arquitectures: write és el 1 a x86-64, el 64 a ARM64 i el 4 a i386.
  • Fins a 6 arguments, de mida de paraula (32 o 64 bits), també en registres.

API vs ABI

Concepte — API

Contracte a nivell de codi font: ssize_t write(int, const void *, size_t)

Concepte — ABI

Contracte a nivell de binari: quin registre porta què, quina instrucció entra al kernel, què queda modificat.

x86-64 (Linux)

Instrucció: syscall

Concepte — Diferència C ABI / syscall ABI
  • ABI de funcions C (System V): arguments a RDI, RSI, RDX, RCX, R8, R9.
  • ABI de syscall: el 4t és R10. Per què?
  • Perquè syscall sobreescriu RCX (hi deixa l’adreça de retorn) i R11 (hi deixa RFLAGS).
  • Per això, en inline asm s’han de declarar rcx i r11 com a clobbered.

ARM64 (AArch64)

Instrucció: svc #0 (supervisor call).

Concepte — La interfície depèn de l'arquitectura

Registres, instrucció i números canvien. Exemple: el número 56 és clone a x86-64 i openat a ARM64. No cal memoritzar els registres: cal saber que existeixen i on mirar-los (man 2 syscall).

Comparació: l’ABI és un contracte arquitectònic

write() vista des de l’ABI

write(fd, buffer, size);

Demo 2: una syscall sense libc

Exemple — Exemple de syscall sense libc

Aquest exemple és identic a write(1, "Hello\n", 6), però sense passar per la libc. Posa a mà el número de syscall (1) i els arguments als registres RAX, RDI, RSI i RDX, i executa directament la instrucció syscall. Amb strace es veu exactament el mateix write(1, “Hello”, 6) = 6, i això demostra que el kernel no distingeix qui ha preparat els registres, i que l’ABI és un contracte de registres, no de C. La variant syscall(SYS_write, …) fa el mateix amb la funció portable de la libc, sense assemblador.

Part 4 · La instrucció syscall

La frontera

Reflexió

Què canvia aquí? No només executem una altra funció: canvia el mode d’execució.

Com s’hi entra de forma segura?

Reflexió

No hi ha cap instrucció passa a mode kernel i salta on vulguis. Llavors, com es fa sense trencar la protecció?

  • La instrucció syscall / svc canvia el privilegi i salta a una adreça que el programa NO tria.
  • Aquesta adreça la fixa el kernel en arrencar.
  • El programa només pot demanar entra al kernel i el kernel decideix què s’executa.
Concepte — Idea de seguretat

Un únic punt d’entrada, controlat pel kernel: així no es pot saltar al mig de codi privilegiat.

Què canvia? Què no canvia?

Concepte — Què canvia
  • Nivell de privilegi (ring 3 → ring 0 / EL0 → EL1).
  • PC/RIP: salta a l’entrada fixa del kernel.
  • La pila: el kernel n’usa la seva (a x86-64, ho fa el codi d’entrada; syscall no toca RSP).
  • Alguns registres (RCX, R11 a x86-64).
Concepte — Què NO canvia
  • El procés: mateix PID, mateix espai d’adreces (mateixa taula de pàgines).
  • El codi que hi ha a la CPU no canvia de procés: no és un canvi de context (context switch).
  • La majoria de registres: es desen i es restauren.
Error Comú — Confusió freqüent

Mode switch (user ↔︎ kernel, dins del mateix procés) ≠ context switch (passar a executar un altre procés). Una syscall és el primer, no el segon.

Demo 3: veure la instrucció syscall amb gdb

Exemple

La instrucció dissassemble permet localitzar syscall i break ens permet posar un punt d’aturada just abans. Observeu com s’atura just a la instrucció syscall dins de write(), i abans d’executar-la es veu que rax = 1 (el número de write) i que rdi, rsi i rdx porten els arguments. Amb un stepi s’executa aquesta única instrucció: surt Hello per pantalla, rax passa a valer 6 (els bytes escrits) i rcx i r11 han canviat, perquè syscall els trepitja.

Part 5 · Tancament

Resum

Resum
  • Una syscall és el mecanisme controlat perquè un programa d’usuari demani serveis al kernel.
  • write() és una funció C (wrapper de libc); la transició posterior és el que no és una crida convencional.
  • printf → stdio (libc, amb buffering) → write → kernel; strace ens deixa veure el tram real.
  • La interfície és una ABI: el número, els arguments i el retorn viatgen en registres; syscall (x86-64) o svc #0 (ARM64). Depèn de l’arquitectura: no cal memoritzar-ne els registres, cal saber que existeixen i on consultar-los.
  • L’entrada és controlada: privilegi i adreça d’entrada els fixa el kernel.
  • Mode switch ≠ context switch.