LKMPG: Fasi preliminari
Moduli vs Programmi
Come i moduli iniziano e finiscono
Un programma di solito inizia con la funzione main(), esegue una serie di istruzioni e termina al completamento di queste istruzioni. I moduli del kernel lavorano in modo leggermente diverso. Un modulo inizia sempre o con la funzione init_module
o con la funzione che specifichi nella chiamata di module_init
. Questa è la funzione d'ingresso per i moduli: dice al kernel che funzionalità il modulo fornisce e imposta il kernel in modo da eseguire le funzioni del modulo quando richieste. Una volta fatto tutto questo, la funzione d'ingresso termina ed il modulo non fa nulla finchè il kernel vuole fare qualcosa con il codice che il modulo fornisce.
Tutti i moduli terminano chiamando o la funzione cleanup_module
o la funzione specificata nella chiamata di module_exit
. Questa è la funzione d'uscita dei moduli: questa rimuove qualsiasi cosa la funzione d'ingresso ha fatto. Elimina le funzionalità registrate dal modulo all'interno del kernel dalla funzione d'ingresso.
Ogni modulo deve avere una funzione d'ingresso ed una d'uscita. Poichè c'è piu di un modo di specificare le funzioni d'ingresso ed uscita, cercherò di fare del mio meglio usando i termini `funzione d'ingresso' e `funzione d'uscita', ma se dovessi confondermi, riferendomi a loro come init_module
e cleanup_module
, penso che capirai cosa cerco di dire.
Funzioni disponibili per i moduli
I programmatori usano funzioni che non definiscono tutte le volte. Un primo esempio è printf()
. Utilizzi queste funzioni messe a disposizione dalla libreria standard del C, libc. Le definizioni di queste funzioni non vengono inserite nel tuo programma fino al momento del linking, che assicura che il codice (per la printf()
ad esempio) è disponibile, e aggiusta le chiamate alla funzione in modo da puntare al relativo codice.
I moduli del kernel sono differenti in questo. Nell'esempio "Ciao Mondo", potresti aver notato che abbiamo usato una funzione, printk()
, ma non abbiamo incluso una libreria di I/O. Questo perchè i moduli sono file oggetti i cui simboli sono risolti quando si carica il modulo con insmod
. Le definizioni dei simboli arrivano dal kernel stesso; le uniche funzioni esterne che puoi usare sono quelle messe a disposizione dal kernel. Se sei curioso sui simboli che sono esportati dal tuo kernel, dai uno sguardo a /proc/kallsyms
.
Un punto da tenere a mente è la differenza tra funzioni di libreria e chiamate di sistema. Le funzioni di libreria sono di alto livello, vengono eseguite ad user space e mettono a disposizione una comoda interfaccia, per il programmatore, alle funzioni che fanno il vero lavoro---le chiamate di sistema (syscall). Le syscall vengono eseguite in kernel mode per conto dell'utente e sono messe a disposizione dal kernel stesso. La funzione di libreria printf()
può sembrare una funzione di stampa generica, ma tutto quello che fa è formattare i dati in stringhe e scriverle usando una syscall di basso livello, write()
, che poi invia i dati allo standard output.
Vuoi vedere da quali syscall è fatta printf()
? E' facile! Compila il seguente programma:
#include <stdio.h> int main(void) { printf("hello"); return 0; }
con gcc -Wall -o hello hello.c. Esegui il programma con strace ./hello. Sei impressionato? Ogni linea che vedi corrisponde ad una syscall. strace[1] è un comodo programma che ti dà i dettagli su quali syscall il programma chiama, come una chiamata è fatta, quali sono gli argomenti e cosa ritorna. E' uno strumento inestimabile per capire cose come a quali file un programma sta provando ad accedere. Verso la fine, vedrai una linea come write(1, "hello", 5hello)
. Questo è. La faccia dietro la maschera printf()
. Potresti non essere familiare con write()
poichè molte persone usano le funzioni di libreria per le operazioni di I/O su file (come fopen
,fputs
, fclose
). Se questo è il caso, prova a dare uno sguardo a man 2 write. La seconda sezione del man è dedicata alle chiamate di sistema (come kill()
e read()
). La terza sezione del manuale è dedicata alle chiamate di libreria, alle quali probabilmente siete molto più abituati (come cosh()
e random()
).
Puoi anche scrivere moduli per rimpiazzare le syscall, come faremo a breve. I crackers spesso fanno uso di questo tipo di cose per eseguire backdoors o trojan, ma puoi scrivere il tuo modulo per fare cose benigne, come stampare Tee hee, fa il solletico! ogni volta che qualcuno prova a cancellare un file sul tuo sistema.
User Space vs Kernel Space
Il kernel gestisce l'accesso a qualsiasi risorsa, sia essa relativa ad una scheda video, ad un hard disk o anche alla memoria. I programmi spesso competono per le stesse risorse. Nel momento in cui ho salvato questo documento, updatedb ha iniziato ad aggiornare il database locale. La mia sessione di vim ed updatedb stanno entrambi usando l'hard disk in modo concorrenziale. Il kernel ha bisogno di mantenere le cose ordinate e non dare agli utenti l'accesso alle risorse in qualunque modo vogliano. A questo punto, la CPU può eseguire codice in modalità differenti. Ogni modalità dà diversi livelli di libertà per fare quello che si vuole sul sistema. L'architettura Intel 80386 ha 4 modalità differenti, che sono chiamati anelli (rings). Unix usa solo due anelli; l'anello più alto (ring 0, anche conosciuto come `modalità supervisore' dove tutto è permesso) e l'anello più basso, che è chiamato `modalità utente'.
Ricordati della discussione riguardo alle funzioni di libreria e le chiamate di sistema. Di solito, si usano le librerie di funzioni nella modalità utente. La funzione di libreria chiama una o più chiamate di sistema, e queste chiamate di sistema viengono eseguite alle spalle delle funzioni di libreria, ma in modalità supervisore poichè fanno parte del kernel stesso. Una volta che la chiamata di sistema ha completato il suo compito, finisce e l'esecuzione ritorna in modalità utente.
Name Space
Quando scrivi un piccolo programma in C, usi variabili che sono convenienti ed hanno senso se le leggi. Se, d'altra parte, stai scrivendo delle funzioni che saranno parte di un problema più grande, qualsiasi variabile globale hai scritto è parte di una comunità di variabili globali scritte da altre persone; alcuni nomi di queste variabili possono coincidere. Quando un programma ha molte variabili globali che non hanno abbastanza significato da essere distinte, hai ottenuto un inquinamento dello spazio dei nomi (namespace). In grandi progetti, uno sforzo deve essere fatto per ricordarsi i nomi riservati, e trovare un modo per sviluppare schemi per creare nomi univoci di variabili e simboli.
Quando si scrive codice per il kernel, anche il più piccolo dei moduli sarà collegato all'intero kernel, rendendo quanto detto sopra, un problema. Il miglior modo per evitare questo problema è di dichiarare tutte le variabili come statiche ed usare un prefisso ben definito per i simboli. Per convenzione, tutti i prefissi del kernel sono minuscoli. Se non vuoi dichiarare tutto come statico, un'altra opzione è di dichiarare una tabella dei simboli e registrarla con il kernel. Arriveremo a questo dopo.
Il file /proc/kallsyms
contiene tutti i simboli che il kernel conosce e che sono accessibili al tuo modulo poichè condividono lo spazio del codice (codespace) del kernel.
Code space
La gestione della memoria è un argomento davvero complicato---la maggior parte del libro della O'Reilly `Understanding The Linux Kernel' tratta solo la gestione della memoria!. Non ci dichiariamo esperti nella gestionde della memoria, ma abbiamo bisogno di sapere un paio di cose preoccupanti sulla scrittura di veri moduli.
Se non hai mai pensato a cosa un segfault significhi, potresti essere sorpreso di sentire che i puntatori non puntano realmente a locazioni di memoria. Non a quelle reali. Quando un processo viene creato, il kernel mette da parte una porzione della reale memoria fisica e la passa al processo per utilizzarla per conservare il codice d'esecuzione, le variabili, lo stack, l'heap ed altre cose che uno scenziato del computer vorrebbe sapere[2]. Questa memoria inizia con 0x00000000 e si estende verso l'alto fino a quanto necessita. Poichè lo spazio della memoria di due processi non si sovrappone, ogni processo che può accedere ad un indirizzo di memoria, per esempio 0xbffff978, avrà accesso ad una differente locazione nella reale memoria fisica!. I processi accedono ad un indice chiamato 0xbffff978 che punta a qualche tipo di offset nella regione di memoria messa da parte per quel particolare processo. Per la maggior parte, un processo come il nostro "Ciao Mondo" non può accedere allo spazio di un altro processo, sebbene ci siano modi dei quali parleremo dopo.
Il kernel ha il suo spazio di memoria come gli altri. Poichè il modulo è un codice che può essere dinamicamente inserito e rimosso del kernel (a differenza di un semi-autonomo oggetto), il modulo condivide il codespace del kernel invece di averne uno tutto suo. Perciò, se il tuo modulo va in segfault, il kernel va in segfault. E se inizi a scrivere sopra i dati per via di un errore off-by-one, ti facendo una camminata sui dati del kernel (o il codice). Questo è peggio di come souni, quindi fai del tuo meglio per essere attento.
Tra l'altro, voglio evidenziare che la discussione sopra citata è vera per qualsiasi sistema operativo che usa un kernel monolitico[3]. Ci sono cose chiamate microkernel che hanno i moduli con i proprio codespace. GNU Hurd e QNX Neutrino sono due esempi di microkernel.
Driver dei device
Una classe di moduli sono i device driver, che mettono a disposizione funzionalità per hardware come una scheda per la TV o una porta seriale. Su Unix, ogni pezzo di hardware è rappresentato da un file posizionato in /dev
chiamato device file che fornisce la possibilità di comunicare con l'hardware. Il device driver fornisce la comunicazione tramite un programma utente. Quindi il device driver della scheda audio es1370.o potrebbe collegare il device file /dev/sound alla scheda audio Ensoniq IS1370. Un programma userspace come mp3blaster potrebbe usare /dev/sound senza neanche sapere che tipo di scheda audio è installata.
Numeri Major e Minor
Note
[1] E' uno strumento inestimabile per capire cose come a quali file un programma sta provando ad accedere. Mai avuto un programma che fallisce silenziosamente perchè non riesce a trovare un file? E' una PITA!
[2] Sono un fisico, non uno scienziato dei computer, Jim!
[3] Questo non è la stessa cosa come `creare tutti i tuoi moduli dentro il kernel', anche se l'idea è la stessa.