I moduli del kernel Dove si descrive come funziona il caricamento dei moduli del kernel e come scrivere un semplice modulo caricabile dinamicamente, assumendo che il lettore sia già in grado di ricompilare un kernel Linux e abbia qualche competenza di programmazione in linguaggio C. * Introduzione La possibilità di compilare parti del kernel sotto forma di modulo non è certo una novità introdotta con Linux-2.6. Con la nuova versione vengono però introdotte alcune modifiche sostanziali nel formato in cui il codice modularizzato viene archiviato su disco e nel modo in cui viene caricato in memoria. Tali modifiche sono abbastanza significative da richiedere una nuova collezione di strumenti per caricare e scaricare i moduli dal kernel: mentre il pacchetto modutils è in grado di caricare moduli per tutte le versioni di kernel tra 2.0 e 2.4, per caricare moduli in Linux-2.6 occorre dotarsi del pacchetto module-init-tools. Le informazioni contenute in questo articolo sono state verificate con linux-2.6.0-test11 e module-init-tools-0.9.14 (riquadro 1). * Il concetto di modulo Un modulo del kernel è fondamentalmente un file oggetto, cioè un frammento di codice eseguibile che fa riferimento a funzioni e variabili esterne, oltre a dichiarare le funzione e le variabili che lui stesso definisce. Normalmente i file oggetto sono salvati su disco con l'estensione ".o" e vengono identificati dal comando "file" come "ELF relocatable". Durante la compilazione di applicazioni, i file oggetto generati da ciascun file sorgente vengono alla fine collegati ("linkati") in un unico eseguibile, risolvendo i riferimenti esterni di ogni file tramite i simboli esportati dagli altri file oggetto e dalle librerie di sistema. Quando si compilano i moduli per il kernel, invece, i file oggetto non vengono linkati, in quanto i riferimenti esterni del modulo si riferiscono a simboli (funzioni e variabili) che fanno parte del kernel e non possono essere risolti nello spazio utente. Ma a ben guardare, anche Linux è un file eseguibile generato dal collegamento di vari file oggetto. Se chiedete al comando "file" cosa sia il vmlinux che viene generato durante la compilazione del kernel, vi verrà detto che si tratta di un "ELF executable", proprio come "/bin/ls" o "/usr/bin/mozilla". I moduli, perciò, sono semplicemente parti di questo applicativo che vengono aggiunte al programma eseguibile durante il suo funzionamento. Per molti versi, un modulo del kernel assomiglia ai cosiddetti "plugin" che si possono caricare su molte applicazioni, da xmms ai vari browser per navigare in rete. Mentre, però, un plugin gira all'interno di uno specifico processo e di solito si limita a svolgere funzioni abbastanza circoscritte di elaborazione dati, un modulo del kernel esegue in un contesto privilegiato e può offrire funzionalità di base per la vita del sistema, come decodificare un filesystem, pilotare una tastiera, gestire una famiglia di protocolli di comunicazione. Se un errore in un plugin può portare alla morte prematura dell'applicazione o al salvataggio di un'istanza di dati errati, un errore in un modulo può far cadere l'intera macchina o corrompere un intero filesystem. * Problemi associati alla modularizzazione La possibilità di estendere e ridurre le funzionalità del kernel durante il suo funzionamento è spesso un grosso vantaggio (per esempio è grazie all'uso di moduli che le distribuzioni GNU/Linux possono offrire un kernel che funziona con quasi qualunque PC senza essere eccessivamente voluminoso), ma introduce una serie di problemi non indifferente, a causa del contesto privilegiato accennato in precedenza in cui girano il kernel ed i suoi moduli. Innanzitutto, la possibilità di modificare il kernel durante il suo funzionamento introduce un problema non indifferente di integrità del sistema in caso di intrusione: laddove la sicurezza, in senso informatico, è critica, conviene disabilitare preventivamente la possibilità di caricare moduli nel kernel. Questo si può fare disattivando l'opzione "CONFIG_MODULES" prima di ricompilare il kernel per la propria macchina. Un altro problema rilevante è rappresentato dalle corse critiche ("race condition") associate all'eliminazione di un modulo dal kernel. A causa degli accessi concorrenti al kernel da parte di più processi, non è immediata l'implementazione di un meccanismo di distacco di un modulo che sia immune da errori in tutte le situazioni possibili di uso. In effetti, quasi metà del codice in "kernel/module.c" è dedicata all'implementazione di questo meccanismo. Anche per questo motivo è possibile configurare il proprio sistema perché non permetta di eliminare un modulo una volta che questo sia stato caricato, tramite l'opzione "CONFIG_MODULE_UNLOAD". Tale opzione non è disponibile nelle versioni precedenti del kernel. La questione più rilevante da fronteggiare quando si ha a che fare con i moduli rimane comunque la compatibilità (o meglio incompatibilità) tra versioni differenti del kernel e impostazioni differenti ma incompatibili della stessa versione. Una buona parte dei meccanismi di base del kernel sono dichiarati nei file di header sotto forma di macro o funzioni "inline", soprattutto per quanto riguarda le primitive di gestione degli accessi concorrenti come semafori, spinlock e operazioni atomiche. Questo codice, unitamente alle informazioni relative alle strutture dati, viene a far parte di ogni file oggetto che ne fa uso. Se un file oggetto contiene il codice di un modulo, esso potrà essere collegato solo con un kernel che usa strutture dati e funzioni uguali, cioè quello i cui header sono stati usati per compilare il modulo. Inoltre, poiché alcune strutture dati e alcune funzionalità vengono istanziate in modo diverso a seconda di come è stato configurato il kernel, un modulo, nella sua forma compilata, può addirittura essere incompatibile con la stessa versione di kernel, se configurata diversamente. Questo accade per esempio tra codice compilato per macchine multiprocessore (CONFIG_SMP) piuttosto che monoprocessore. Nel caso monoprocessore alcune situazioni di concorrenza non possono verificarsi, perciò le strutture dati e le funzioni associate vengono compilate in maniera diversa nei due casi. Senza addentrarci nella descrizione di CONFIG_MODVERSIONS, vediamo come viene affrontato il problema della compatibilità tra il kernel e i suoi moduli nella versione 2.6, ma prima occorre spendere alcune parole sulla struttura di un file oggetto ELF. * Le sezioni ELF (riquadro 3) Un file oggetto, ma anche un eseguibile, in formato ELF è composto da una o più parti, dette sezioni, identificate da un nome, un po' come un file TAR è composto da vari file ciascuno con un nome diverso. L'intestazione associata ad ogni sezione ne specifica, oltre al nome e alla dimensione, anche alcune altre caratteristiche, come l'indirizzo di caricamento e alcuni attributi speciali. L'intestazione globale ELF specifica il tipo di file e la piattaforma cui si riferisce (per esempio i386 o PowerPC). Mentre molti formati binari antecedenti richiedevano espressamente che un file oggetto o eseguibile contenesse solo le sezioni chiamate .text, .data e .bss, il formato ELF permette la definizione di sezioni con un nome arbitrario e un contenuto arbitrario, la cui interpretazione è lasciata allo specifico contesto di uso del file. Gli autori di Linux hanno trovato da tempo modi intelligenti per sfuttare la flessibilità del formato ELF; mentre Linux-2.0 può essere compilato a scelta con un compilatore ELF o con uno precedente, tutte le versioni successive (a partire dalla 2.1.0) definiscono sezioni di output con nomi specifici e richiedono perciò un compilatore ELF. Gli header del kernel (in particolare <linux/init.h> e <linux/module.h>) utilizzano la direttiva "section" del compilatore per assegnare elementi del programma a specifiche sezioni ELF. Nel caso di vmlinux, le sezioni vengono usate come definito nel file "vmlinux.lds.S" (un file da leggere dopo un buon caffè). È in questo modo, per esempio, che tutto il codice di inizializzazione viene caricato in memoria consecutivamente e può essere liberato dopo aver avviato il sistema (questo succede quando il kernel stampa il messaggio "freeing init memory"). Nel caso dei moduli, le sezioni ELF speciali fanno parte del file oggetto e vengono usate durante il processo di caricamento. * La struttura di un file .ko Normalmente, il nome dei file oggetto termina con ".o", e così è sempre stato anche per i moduli dek kernel fino alla versione 2.4. Con Linux-2.6, anche per facilitare l'utente nell'identificarlo, i moduli usano il suffisso ".ko" (kernel object). Il file ".ko", in effetti, è il risultato del collegamento di due file: il vero e proprio file oggetto contenente il codice, chiamato ".o", e un file che contiene informazioni aggiuntive riguardo all'ambiente di compilazione del modulo, un file oggetto ELF il cui nome usa il suffisso ".mod.o". Per vedere le sezioni incluse in un file oggetto si può usare il comando "objdump -h" (section Headers), e si otterrà un risultato simile a quello mostrato nel riquadro 4, che si riferisce al modulo ide-scsi.ko . Le sezioni .init.text e .exit.text contengono il codice di inizializzazione e rimozione del modulo, la sezione .modinfo contiene informazioni sulle dipendenze del modulo, la sua licenza e la versione del kernel usata per compilarlo, "__extable" è usata dal kernel per gestire alcune eccezioni e ".gnu.linkonce.this_module" è un marcatore speciale che vedremo in seguito. * Scrittura e compilazione di un modulo Per meglio vedere come avviene la creazione di un modulo, compiliamo un modulo vuoto. I riquadri 5 e 6 mostrano il Makefile necessario per compilare il modulo e il sorgente, "empty.c". Questi due file vanno messi nella directory dove andremo a compilare, che per me si chiama /home/rubini/modules-2.6/src/ . Il Makefile è un po' più complicato di quanto ci si aspetterebbe semplicemente perché usa un costrutto condizionale di GNU make: se la variabile KERNELRELEASE non è definita (e non lo è) allora definisco LINUX e PWD, poi specifico che per fare tutto devo reinvocare make nella directory $LINUX specificando il valore della variabile SUBDIRS. A questo punto, il Makefile del kernel dirà a make di (ri)leggere questo file, ma a questo punto KERNELRELEASE è definita, quindi prendiamo il ramo else della condizione, dove viene semplicemente assegnata la variabile "obj-m". Il resto viene gestito automaticamente. Reinvocando make nella directory principale dei sorgenti del kernel, non dobbiamo preoccuparci di alcun dettaglio relativo alla piattaforma o alle opzioni da passare al compilatore, ma tale albero di sorgenti dovrà già essere stato compilato e dovrà corrispondere al kernel nel quale vogliamo caricare questo modulo. Quando invochiamo make per compilare empty.c possiamo passare parametri a make nelle variabili di ambiente o sulla linea di comando, come descritto il mese scorso. In più, possiamo assegnare la variabile LINUX per dire dove si trova il sorgente del kernel, nel caso in cui non si trovi in /usr/src/linux-2.6. Qui sotto riporto il comando di compilazione da me usato e le righe più importanti che vengono stampate durante la procedura: bash$ make LINUX=/opt/kernel/linux-2.6.0-test11 CC [M] /home/rubini/modules-2.6/src/empty.o Building modules, stage 2. MODPOST CC /home/rubini/modules-2.6/src/empty.mod.o LD [M] /home/rubini/modules-2.6/src/empty.ko È ora possibile invocare "insmod empty.ko", usando il comando insmod presente nel pacchetto module-init-tools. Il modulo si caricherà senza errori e senza dare segni di vita (poiché vuoto). Per rimuovere il modulo, eseguire "rmmod empty" -- senza il suffisso ".ko" in quanto ora non ci riferiamo ad un file ma ad un oggetto di nome "empty" che esiste all'interno del kernel. * Come vengono usati i contenuti del modulo Se avete già avuto a che fare con il pacchetto modutils, che implementa tutto il collegamento dinamico del modulo con il kernel, una cosa che colpisce di di module-init-tools è la sua ridotta dimensione, sia come sorgente sia come eseguibile. Il comando insmod, per esempio, occupa solo 6KiB. In effetti, il nuovo insmod non effettua il collegamento dinamico ma passa semplicemente il file oggetto a Linux-2.6, senza toccarlo. Il collegamento dinamico avviene all'interno del kernel, nel file kernel/module.c. Il lavoro effettuato nello spazio kernel risulta più semplice, in quanto il codice ha accesso diretto alle strutture dati (per esempio la tabella dei simboli) che precedentemente dovevano essere rese disponibili allo spazio utente; inoltre, l'accesso diretto alle strutture dati del kernel permette facilmente di gestire più meta-informazione riguardo al modulo di quanto non fosse possibile con il vecchio approccio. La chiamata di sistema che riloca il modulo e lo collega al kernel si chiama init_module, e la sua implementazione risiede nalla funzione sys_init_module(), che delega tutto il "duro lavoro" alla funzione load_module(), nello stesso file. Si tratta di codice molto ben leggibile, come tutto il codice di Rusty Russel (autore di questa implementazione) e come la maggior parte del codice del kernel. Scorrendo la funzione load_module è interessante notare come le sezioni di rimozione (come .text.exit vista precedentemente) non vengono caricate se CONFIG_MODULE_UNLOAD non è definito, risparmiando quindi un po' di memoria. Così pure si nota come il controllo di compatibilità tra il modulo e il kernel viene effettuato usando la stringa "vermagic" definita nella sezione modinfo del file oggetto. Tale stringa viene inclusa nel modulo dal file temporaneo empty.mod.c, generato durante la compilazione. Questo file include <linux/vermagic.h> dove la stringa viene generata in base al numero di versione del kernel, in base alle opzioni che possono generare incompatibilità (come CONFIG_SMP), in base alla versione di compilatore usata. Per chi volesse leggere la stringa risultante nella sua situazione specifica, il modo più veloce è leggere empty.mod.o oppure empty.ko con "objdump -s" (show), guardando il contenuto della sezione .modinfo. La sezione ".gnu.linkonce.this_module" viene identificata dalla funzione load_module e viene usata per verificare che il file oggetto sia effettivamente un modulo del kernel. * Conclusioni I meccanismi di compilazione e caricamento dei moduli in Linux-2.6 sono abbastanza diversi da quelli usati nelle precedenti versioni del kernel ma si tratta, in questo come in altri casi di incompatibilità, di realizzazioni decisamente più ordinate e più flessibili rispetto all'approccio precedente. Passare da un sistema basato su Linux-2.4 ad uno basato su Linux-2.6 può essere impegnativo, ma può essere interessante anche se la distribuzione che si usa non contiene ancora il pacchetto module-init-tools o le altre piccole cose necessarie al cambiamento. Sicuramente non è impegnativo come passare da a.out ad ELF. RIQUADRI <Riquadro 1 – module-init-tools> <Riquadro 2 - la programmazione ad oggetti> <Riquadro 3 - ELF e a.out> <Riquadro 4 – ide-scsi.ko> <Riquadro 5 – Makefile> <Riquadro 6 – empty.c> <Riquadro 1 – module-init-tools> Normalmente, per caricare un modulo del kernel (anche quando il caricamento avviene automaticamente) viene usato il comando "insmod" o il comando "modprobe". Il pacchetto "module-init-tools" contiene un'implementazione di questi comandi in grado di gestire i moduli della versione 2.6 del kernel, chiamando automaticamente la vecchia versione dei comandi (quella del pacchetto "modutils") se invocati in un sistema precedente. Il pacchetto può essere scaricato da ftp://ftp.it.kernel.org/pub/linux/utils/kernel/module-init-tools/ e si compila come la maggior parte dei pacchetti eseguendo: ./configure && make && make install Personalmente preferisco passare l'opzione "--prefix=/opt/module-init-tools" al comando configure, per non intaccare la coerenza dei file gestiti dalla distribuzione. Il pacchetto è parte di Debian unstable, dove può essere installato con "apt-get install module-init-tools". <Riquadro 2 - la programmazione ad oggetti> Le regole della buona programmazione dicono che metodi (funzioni) e istanze (dati) devono essere incapsulati dentro a strutture ("oggetti") che rimangano opache nei confronti del codice che le usa. Questo permette di modificare la struttura interna degli oggetti senza richiedere la riscrittura del codice che rimane esterno. Ma le regole della buona programmazione dicono anche che il codice deve essere efficiente, e i due requisiti sono spesso incompatibili. Il kernel Linux utilizza una buona incapsulazione di metodi e istanze nelle sue strutture di più alto livello (file, driver, aree di memoria) dove il guadagno in manutenibilità è notevole e i costi in prestazioni praticamente nulli, prediligendo l'efficienza (quindi macro e funzioni inline che accedono direttamente alle strutture dati) nelle operazioni di più basso livello, dove i costi di un approccio più canonico sarebbero molto rilevanti. Ma i problemi di compatibilità tra i moduli e le versioni del kernel non dipendono solo dal legame stretto tra il file oggetto e gli specifici header usati per compilarlo; il kernel è anche una realtà in continuo movimento, dove si implementano continuamente nuove astrazioni e nuove ottimizzazioni; modifiche frequenti a come il kernel parla con se stesso (cioè i suoi moduli) sono la norma, mantenendo la piena compatibilità delle chiamate di sistema, l'interfaccia del kernel con il mondo esterno. <Riquadro 3 - ELF e a.out> ELF (Executable and Linking Format) è il formato oggi più usato per la memorizzazione di file oggetto, programmi eseguibili e librerie dinamiche, sia su GNU/Linux sia sugli altri principali sistemi operativi. Solo lo spazio utente di uClinux non usa ELF, perché non è un formato ottimizzato per processori senza MMU. Su piattaforma IA32, si è passati ad ELF circa nel 1995, soppiantando il precedente formato cosiddetto "a.out", supportato anche dai kernel recenti se si carica il modulo "binfmt_aout.c". È ancora oggi possibile installare la distribuzione Mastodon ("The last a.out Linux distribution"), disponibile su http://www.pell.portland.or.us/~orc/Mastodon/ e rileggere l'ELF-HOWTO, che spiega come aggiornare la propria macchina a.out ricompilando tutto (a partire dal compilatore stesso) dai pacchetti sorgente. Un'esperienza interessante, per i più curiosi e per chi vuole per un paio d'ore sentirsi giovane come allora. <Riquadro 4 – ide-scsi.ko> Idx Name Size VMA LMA File off Algn 0 .text 000016d0 00000000 00000000 00000040 2**4 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .fixup 0000000a 00000000 00000000 00001710 2**0 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 2 .init.text 00000010 00000000 00000000 0000171c 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 3 .exit.text 00000010 00000000 00000000 0000172c 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 4 .rodata 00000880 00000000 00000000 00001740 2**5 CONTENTS, ALLOC, LOAD, READONLY, DATA 5 __ex_table 00000008 00000000 00000000 00001fc0 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA 6 .modinfo 00000060 00000000 00000000 00001fe0 2**5 CONTENTS, ALLOC, LOAD, READONLY, DATA 7 .data 00000180 00000000 00000000 00002040 2**5 CONTENTS, ALLOC, LOAD, RELOC, DATA 8 .gnu.linkonce.this_module 00000100 00000000 00000000 000021c0 2**5 CONTENTS, ALLOC, LOAD, RELOC, DATA, LINK_ONCE_DISCARD 9 .bss 00000004 00000000 00000000 000022c0 2**2 ALLOC 10 .comment 00000060 00000000 00000000 000022c0 2**0 CONTENTS, READONLY 11 .note 00000028 00000000 00000000 00002320 2**0 CONTENTS, READONLY <Riquadro 5 – Makefile> ifndef KERNELRELEASE LINUX ?= /usr/src/linux-2.6 PWD := $(shell pwd) all: $(MAKE) -C $(LINUX) SUBDIRS=$(PWD) modules clean: rm -f *.o *.ko *~ core .depend *.mod.c *.cmd else obj-m := empty.o endif <Riquadro 6 - empty.c> #include <linux/module.h> #include <linux/init.h> /* public domain */ MODULE_LICENSE("GPL and additional rights"); static int mtest_init(void) { return 0; } static void mtest_exit(void) { } module_init(mtest_init); module_exit(mtest_exit);