Il linguaggio C è stato creato da Dennis Ritchie nel 1972 per sviluppare programmi UNIX. Infatti alcuni caratteristiche originali di UNIX sono ancora visibili, come il concetto di “everything is a file”.
I OS sono normalmente scritti in programmi C (o C++).
I tipi di dato primitivi in C sono gli inerti (includendo short e long), caratteri, e numeri floating-point (con la virgola). Tipi di dati strutturati possono essere costruiti utilizzando array, strutture e unioni.
Una caratteristica particolare di C è l’utilizzo dei puntatori (controllo sulla memoria, perfetti per scrivere OS). I puntatori sono un tipo di variabili che “puntano” (contengono l’indirizzo di) ad una variabile o struttura dati.
Per esempio :
char c1, c2, *p;
c1 = 'c';
p = &c1;
c2 = *p;I puntatori sono costrutti molto potenti, che però possono portare ad errori quando usati senza attenzione.
Everything is a file
Secondo questa “filosofia”, non solo file di testo, ma anche l’hardware (disco, stampanti), i canali di comunicazione (pipe) e le connessioni di rete (sockets) sono trattati dal OS come file.
Per questo motivo quando un processo viene eseguito, il OS gli assegna automaticamente tre file già aperti :

(file number = file descriptor)
Build process - processo di compilazione
Per fare il build del OS, ogni file .c è compilato in un file oggetto da un compilatore C. Questo file oggetto contiene le istruzioni macchina che andranno eseguite dalla CPU (dopo il linking).
Prima del compilatore, vi è il preprocessore C, che legge i file sorgente e risolve gli #include, espande le macro, gestisce la compilazione condizionale e poi passa il risultato al compilatore.
Una volta che il file oggetto sono pronti, vengono passati al linker che li combinerà tra di loro per ottenere un unico file eseguibile.

Dalle librerie standard alle system call
Vediamo tre modi per stampare “Hello World!” nella console (output standard - stdout), per livello di astrazione :
- livello alto (
printf) : utilizzando una libreria standardstdio.h
void printf(char *str, ...);#include <stdio.h>
int main(int argc, char **argv){
printf("Hello World!\n");
return 0;
}- livello medio (
write) : utilizziamo una funzione di sistema che parla direttamente con il kernel, dobbiamo però specificare dove, cosa e quanto scrivere.
int write(int fd, char *buf, size_t len);#include <unistd.h>
#define STDOUT 1
int main(int argc, char **argv){
char msg[] = "Hello World!\n";
write(STDOUT, msg, sizeof(msg));
return 0;
}- livello basso (
syscall) : invochiamo direttamente il kernel usando l’id della funzione (es. per lawritesu x86_64 è 1)
int syscall(int number, ...);#include <unistd.h>
#include <stdio.h>
#inlude <sys/syscall.h>
#define STDOUT 1
int main(int argc, char **argv){
char msg[] = "Hello World!\n";
int nr = SYS_write; //id funzione write
syscall(nr, STDOUT, msg, sizeof(msg));
return 0;
}
Per avere una panoramica migliore di quello che succede possiamo vedere la Syscall table.
Libc fornisce wrapper utili intorno alle syscall, ad esempio con
write,readedexit.
Lunga attraversata da read() a ksys_read()
Vediamo cosa succede quando viene invocata la funzione read().
1 - read come wrapper
La funzione read non chiama direttamente il kernel, ma una funzione della libreria standard glibc chiamata __libc_read, che funge da wrapper per la system call read (vera e propria).
Questa logica è contenuta nel file della libreria C glibc/sysdeps/unix/sysv/linux/read.c, di cui il contenuto rilevante :
ssize_t __libc_read (int fd, void *buf, size_t nbytes){
return SYSCALL_CANCEL(read, fd, buf, nbytes);
}(discesa verso il kernel con questa macro)
2 - Macro SYSCALL_CANCEL
La ragione per cui glibc non fa direttamente la chiamata, ma usa questa macro specifica è che :
- cancellazione pulita se un thread viene cancellato mentre sta aspettando che la
read()finisca - la macro disabilita temporaneamente la capacità di interrompere il thread in quel momento critico
- evitare che glibc o il programma rimangano in uno stato corrotto se l’operazione viene interrotta bruscamente
All’interno di questa “protezione”, viene chiamata un’altra macro ancora più interna : INLINE_SYSCALL_CALL(read, fd, buf, nbytes).
3 - Macro INLINE_SYSCALL_CALL
La macro INLINE_SYSCALL_CALL utilizza la macro INLINE_SYSCALL per costruire l’istruzione assembly che effettua la system call.
Infatti la definizione della macro INLINE_SYSCALL dipende dall’architettura, per x86_84 la troviamo in glibc/sysdeps/unix/sysv/linux/x86_64/sysdep.h.
Infatti questa macro prepara i registri con i parametri della system call (syscall) (secondo x86_64) :
- in
%rdi: file descriptor -fd - in
%rsi: buffer -buf - in
%rdx: count - in
%eax: id della system call (_NR_read, ovvero 0)
#undef internal_syscall3
#define internal_syscall3(number, arg1, arg2, arg3) \
({ \
unsigned long int resultvar; \
TYPEFY (arg3, __arg3) = ARGIFY (arg3); \
TYPEFY (arg2, __arg2) = ARGIFY (arg2); \
TYPEFY (arg1, __arg1) = ARGIFY (arg1); \
register TYPEFY (arg3, _a3) asm ("rdx") = __arg3; \
register TYPEFY (arg2, _a2) asm ("rsi") = __arg2; \
register TYPEFY (arg1, _a1) asm ("rdi") = __arg1; \
asm volatile ( \
"syscall\n\t" \
: "=a" (resultvar) \
: "0" (number), "r" (_a1), "r" (_a2), "r" (_a3) \
: "memory", REGISTERS_CLOBBERED_BY_SYSCALL); \
(long int) resultvar; \
})Qui viene caricato il numero (
number) della system call in%raxe passa gli argomenti dei registri appropriati secondo la convenzione Linux a 64 bit.
Il parametro number è una costante definita dal kernel e resa disponibile da glibc tramite header come syscall_64.tbl :
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
4 common stat sys_newstatEsegue poi l’istruzione
syscallche fa il salto alla modalità kernel :
- la CPU salva lo stato corrente del processo
- cambia lo stack e i segmenti per entrare nello spazio kernel
- il controllo passa ad un indirizzo specifico definito nel registro
MSR_LSTAR(labelentry_SYSCALL_64in Linux nel filearch/x86/entry/entry_64.S) Questa istruzione è la prima ad essere eseguita nel kernel come parte della system callread().
5 - Simbolo entry_SYSCALL_64
Quindi questa è l’entry point nel kernel per tutte le system call a 64 bit, un punto di ingresso che il processore trova quando esegue syscall.
Il codice in entry_SYSCALL_64 prepara il terreno per eseguire la system call vera e propria.
SYM_CODE_START(entry_SYSCALL_64)
UNWIND_HINT_ENTRY
ENDBR
swapgs
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
movq PER_CPU_VAR(cpu_current_top_of_stack), %rspLe operazioni sono :
swapgs: scambia il registro GS (puntatore a stack utente/kernel), in modo che la CPU possa usare la struttura dati per il kernelmovq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2): salva lo stack pointer utente nella Task State Segment (TSS) (per tornarci dopo)SWTICH_TO_KERNEL_CR3: cambia le tabelle delle pagine (CR3 registro che punta a queste tabelle), quindi passa da quelle utente a quelle del kernel (protezione memoria)movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp: imposta lo stack pointer (%rsp) nella cima dello stack del kernel e ora il kernel ha il suo stack su cui lavorare
Solo dopo questi passaggi, il kernel può continuare con
do_syscall_64().
6 - Funzione do_syscall_64()
Questa è una funzione C del kernel Linux e rappresenta il vero dispatcher delle system call a 64 bit ed è chiamata direttamente da entry_SYSCALL_64 (in assembly) dopo il passaggio in modalità kernel.
Riceve 2 parametri :
struct pt_regs *regs: puntatore alla struttura che contiene tutti i registri della CPUint nr: id (numero) della system call (es._NR_read = 0)
Secondo il file arch/x86/entry/syscall_64.c il suo flusso principale è il seguente :
syscall_enter_from_user_mode(): gestisce il contesto di ingresso nel kerneldo_syscall_x64(): sceglie (dispatcher) la funzione appropriata consultando lasys_call_tablesyscall_exit_to_user_mode(): prepara il ritorno allo user space
La funzione do_sycall_x64() :
- controlla il numero della system call sia valido (
nr) - se valido chiama la funzione
x64_sys_call(regs, nr)
7 - Funzione x64_sys_call()
Questa funzione contiene uno switch generato automaticamente dal file asm/syscalls_64.h, dove ogni case corrisponde ad un numero di system call e richiama la funzione kernel corrispondente.
Per esempio :
case 0:__x64_sys_read(regs)case 1:__x64_sys_write(regs)
Quindi nel caso nostro di read(), il kernel chiama __x64_sys_read(), definita nel file fs/read_write.c.
La funzione
__x64_sys_read()a sua volta chiamaksys_read(fd, buf, cont)che implementa la logica reale : - accede al file descriptor e lookup del file descriptor - controlla i permessi - legge i dati nel buffer kernel tramitevfs_read()- copia i dati nello user space
La chiamata vfs_read() chiama file->f_op->read_iter (legge i byte), vedremo poi come funziona a questo livello.
Da dove viene __x64_sys_read() e chi la crea?
Questa funzione non esiste scritta nel codice sorgente, ma viene generata automaticamente dalla macro SYSCALL_DEFINE3().
Come abbiamo visto prima tutte le system call sono registrate in una tabella nel file arch/x86/entry/syscalls/syscall_64.tbl.
- uno script
scripts/syscalltbl.shgenera il file headerasm/syscalls_64.hche contiene per ogni system call__SYSCALL(nr, sys_ni_syscall). - questo file header viene incluso in
arch/x86/entry/syscall_64.c(come visto prima) che poi all’interno contiene i case di questo tipocase 0: return __x64_sys_read(regs)
La macro SYSCALL_DEFINE3() implementata nel file fs/read_write.c :
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}e definita in include/linux/syscalls.h, che quello che fa è in fase di compilazione espande (trasforma) il codice in :
asmlinkage long __x64_sys_read(unsigned int fd, char __user *buf, size_t count)
{
return ksys_read(fd, buf, count);
}Tra gli esempi di SYSCALL_DEFINE3 del kernel linux abbiamo :

Riassumiamo ora i passaggi :
- User space
- funzione C
read(fd, buf, count)invoca la system call read - numero della system call viene caricato in
%rax
- funzione C
- Entry point in assembly
- file :
arch/x86/entry/entry_64.S - istruzione
syscallfa il salto in kernel mode - controllo passa a
entry_SYSCALL_64() - controllo passa a
do_syscall_64()
- file :
- Preparazione contesto kernel
- funzione
syscall_enter_from_user_mode() - salva registri
- verifica privilegi
- prepara lo stack kernel
- funzione
- Dispatcher della system call
- file :
arch/x86/entry/syscall_64.c do_syscall_64()chiamado_syscall_x64()do_syscall_x64()invocax64_sys_call(regs, nr)- switch-case generato automaticamente che invoca
__x64_sys_read(regs)
- file :
- Definizione della system call vera
- file :
fs/read_write.c - macro
SYSCALL_DEFINE3che implementa (espande/trasforma)__x64_sys_read() __x64_sys_read()chiamaksys_read(fd, buf, count)
- file :
- Implementazione kernel pura
- file :
fs/read_write.c ksys_read()esegue la logica della read
- file :
- File system layer
- funzione :
vfs_read()che chiamafile->f_op->read_iter - …
- funzione :
