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);