Compilare Linux-2.6

di Alessandro Rubini

Riprodotto con il permesso di Linux Magazine, Edizioni Master.

Una descrizione di come funzionano la configurazione e la compilazione della versione 2.6 del kernel.

Introduzione

In questo articolo, primo di una serie incentrata sul kernel 2.6, viene descritta la procedura di configurazione e compilazione, rivolgendo l'attenzione ai dettagli implementativi più che alle modalità d'uso. Le procedure e le implementazioni descritte sono state verificate sulla versione 2.6.0-test9, la più recente al momento della stesura del testo.

Riquadro 1 - Dove scaricare i sorgenti del kernel

Le versioni "vanilla" di Linux, cioè quelle rilasciate da Linus Torvalds, sono disponibili su ftp.kernel.org e sui mirror nazionali. Altri autori distribuiscono versioni modificate, per esempio aggiungendo funzionalità utili ma ancora sperimentali, oppure aggiungendo codice specifico a piattaforme hardware diverse da quelle direttamente supportate da Linus. Tali versioni sono di solito identificate da suffissi nel nome del kernel; per esempio, linux-2.6.0-test9-rmk1 è la versione di Russel King (rmk) contenente codice per piattaforma ARM non ancora integrato nei sorgenti di Linus.

Il luogo suggerito da cui scaricare i kernel di Torvalds è sicuramente il mirror italiano ftp.it.kernel.org:

      ftp://ftp.it.kernel.org/pub/linux/kernel/v2.6/
    

Il file "linux-2.6.0-test9.tar.bz2", di 33MB, una volta decompresso creerà una directory chiamata "linux-2.6.0-test9", di circa 200MB.

Configurazione del kernel

Una volta scaricati i sorgenti (si veda il riquadro per i dettagli), il primo passo necessario per poter compilare il proprio linux-2.6 è la configurazione, ovvero la scelta di quali funzionalità attivare o meno e quali driver includere nalla propria immagine di kernel. L'output della fase di configurazione è il file .config, un semplice file di testo che viene letto da make al momento di compilare il kernel.

La configurazione, come gli altri passi, viene invocata dal comando make, e può essere effettuata in diversi modi:

I vari meccanismi di configurazione, quindi, assumono che sul sistema sia disponibile un compilatore C e gli strumenti necessari per sviluppare con le librerie utilizzate (Qt, ncurses). Sicuramente il compilatore deve essere stato installato se si vuole compilare un kernel, ma le altre richieste potrebbero non essere soddisfatte sul vostro sistema. In particolare, per usare make menuconfig è necessario il pacchetto ncurses-dev (o equivalente) e per usare make xconfig è necessario il pacchetto qt-dev (o equivalente). Personalmente, non usando KDE o altro software basato su qt, ho dovuto scaricare e installare 22MB di software per poter provare make xconfig e verificare che tutto sommato non mi serve.

Riquadro 2 - Aggiornare tramite patch

Una volta scaricato un tar contenente i sorgenti del kernel, il modo migliore per passare alla versione successiva è scaricare il file di "patch", cioè la descrizione delle differenze tra due versioni successive, ovvero la "pezza" da applicare alla versione precedente per rinnovarla.

I file di patch si trovano nello stesso luogo da cui si scaricano i tar completi e, come i tar completi, sono compressi con gzip o bzip2.

Per applicare un file di differenze del kernel, occorre entrare nella directory dei sorgenti che si vogliono modificare e mandare il file sullo standard-input del comando "patch -p1". Per esempio:

      cd /usr/src/linux-2.6
      bzcat /path/to/patch-2.6.0-test9.bz2 | patch -p1

La definizione dei parametri di configurazione

Le domande poste durante la fase di configurazione dipendono spesso le une dalle altre. Per esempio, a chi specifica di non avere bisogno del supporto SCSI non verrà posta alcuna domanda relativa alle periferiche SCSI supportate. Tali dipendenze possono anche essere più complesse: per esempio una specifica scheda di rete PCI può essere selezionata solo se si sta compilando un kernel che abbia sia il supporto di rete sia quello per il bus PCI.

Definire un formato di file che funzioni come input per i vari sistemi di configurazione non è impresa facile; con linux-2.2 e linux-2.4, infatti, capitava talvolta che l'introduzione di nuove opzioni di configurazione funzionasse con "make config" ma non, per esempio, con xconfig, in quanto ognuno dei tre meccanismi di configurazione aveva il suo interprete dei file di input: tre interpreti diversi perché scritti in tre linguaggi diversi.

Con Linux-2.6 la lettura dei file di input per la configurazione viene centralizzata in libkconfig, una libreria che viene usata da tutte e tre le interfacce utente che, ora, sono tutte scritte in C. L'interprete (o "parser") contenuto in libkconfig è scritto usando lex e yacc, i classici strumenti Unix per la creazione di interpreti (si veda il riquadro successivo). I file sorgente del parser si chiamano zconf.l (input di lex) e zconf.y (input di yacc).

Scopo del parser appena descritto è l'interpretazione di un file, solitamante chiamato Kconfig, che definisce i vari parametri di configurazione, le loro dipendenze, la descrizione dettagliata del loro significato. Un file Kconfig può richiedere la lettura di altri file, perciò in pratica troviamo uno o più di tali file in quasi ogni directory dell'albero di sorgenti (nella versione 2.6.0-test9 ci sono 207 file Kconfig, che definiscono un totale di 3727 opzioni di configurazione diverse).

La sintassi dei file Kconfig e l'organizzazione dei makefile del nucleo sono descritti nella directory Documentation/kbuild/, all'interno dei sorgenti del kernel.

Il nome del Kconfig principale viene passato sulla linea di comando ai tre programmi interattivi; per chi compila un kernel per piattaforma PC il nome del file è arch/i386/Kconfig. La stringa i386 viene determinata dal Makefile tramite l'esecuzione del comando uname -m, ma si può configurare un kernel per un'altra piattaforma semplicemente assegnando la variabile ARCH sulla linea di comando di make. Per esempio:

    make menuconfig ARCH=ppc

La variabile ARCH viene semplicemente usata come nome di subdirectory nell'albero arch/, deve quindi essere uno dei nomi di piattaforma supportati dal kernel mentre altri valori di ARCH generano un errore. In pratica, però, configurare un kernel per un'architettura diversa da quella nativa ha senso quasi solo in un contesto di cross-compilazione, descritto più avanti. L'unica eccezione è user-mode linux.

Riquadro 3 - Lex e yacc

Il programma "lex" è un generatore di analizzatori lessicali. Il suo file di input di solito usa l'estensione .l (per "lex", appunto) e l'esecuzione del programma genera un sorgente in C che contiene l'analizzatore lessicale richiesto. La versione GNU di lex si chiama flex.

Il programma "yacc" (yet another compiler compiler) è un generatore di analizzatori sintattici (quindi un "compilatore di compilatori"). Il nome del suo file di input solitamente termina in ".y", mentre il suo output è un sorgente C contenente l'analizzatore definito nel file di input. La versione di yacc inclusa nel progetto GNU si chiama bison.

Nei sorgenti del kernel, nella directory scripts/kconfig sono già presenti i file di output generati da flex e bison, per cui non è necessario avere installato i programmi per poter compilare un kernel. Chi modificasse la sintassi o il lessico dei file di configurazione avrebbe invece bisogno di lex e/o yacc per generare i nuovi analizzatori.

User-mode Linux (UML)

Le directory nell'albero arch/ contengono il codice sorgente relativo all'interfacciamento del kernel con i vari ambienti operativi in cui è stato portato. Si tratta del codice relativo all'avvio del sistema, alla gestione delle interruzioni, al controllo della MMU (memory management unit) per implementare la memoria virtuale. Quasi tutti tali ambienti operativi sono famiglie di processori: i386 (il PC), ppc (PowerPC, come nei Macintosh), arm (Acorn Risc Machine, un processore molto usato in campo embedded).

La directory arch/um, a differenza di tutte le altre, non contiene il codice relativo ad un tipo di processore su cui può girare linux-2.6, quanto la definizione di un ambiente operativo "user mode". Si tratta, cioè, del codice per far eseguire il kernel Linux come programma applicativo all'interno di un sistema operativo già avviato. Questo permette di osservare facilmente il kernel tramite un debugger convenzionale e fare esperimenti con configurazioni strane senza riavviare la propria macchina e senza il rischio di cadute del sistema.

La cosa interessante di UML, guardando al processo di configurazione e compilazione, è che si tratta di codice indipendente dall'architettura che lo ospita: se si compila usando ARCH=um lavorando su PC si otterrà un eseguibile i386, se si compila su una macchina PowerPC si otterrà un eseguibile ppc altrettanto funzionale.

Il meccanismo usato per ottenere un eseguibile UML che corrisponda con la macchina ospite è ben documentato nel Makefile principale del kernel: la variabile SUBARCH viene ottenuta invocando uname -m,mentre ARCH=um deve essere specificata sulla linea di comando di make. ARCH viene usata normalmente per la compilazione, selezionando arch/um/Kconfig come file di configurazione principale e arch/um/Makefile come makefile di piattaforma; quest'ultimo però include arch/um/Makefile-$(SUBARCH) per le definizioni che devono cambiare da una piattaforma ospite all'altra. Al momento, il lavoro su user-mode Linux viene svolto solo su i386, ppc e ia64, come si può vedere dal contenuto della directory arch/um, anche se UML è ad oggi utilizzabile solo su x86 e solo con l'applicazione di modifiche apposite, disponibili su user-mode-linux.sourceforge.net oppure people.redhat.com/mingo/UML-patches/ .

La compilazione

I comandi per la compilazione del kernel non sono cambiati in maniera significativa rispetto alla versione 2.4 o 2.2 ma c'è una differenza: il semplice make, che prima creava solo il file eseguibile vmlinux ora crea anche il file avviabile per la specifica piattaforma e tutti i moduli. Il comando make bzImage, come nelle precedenti versioni, crea il file avviabile arch/i386/boot/bzImage, quello che viene passato a Grub o a Lilo all'avvio del sistema ma non i moduli. Anche se le immagini avviabili per altre piattaforme in genere non si chiamano bzImage, in genere si invocherà semplicemente make, senza argomenti, indipendentemente dalla piattaforma.

È interessante ricordare che il nome del file bzImage ("big zImage", ovvero "big compressed image"), come la sua struttura, risentono della storia passata del kernel Linux su piattaforma i386, un processore che all'avvio si rifiuta di vedere più di 640k di memoria e richiede sporchi trucchi per poter avviare un vero sistema operativo (il cui nucleo, oggi, supera spesso la dimensione di 1MB). Il nome "big zImage" indica il meccanismo di caricamento chepermette di caricare in memoria un'immagina compressa del kernel che eccede i 512kB disponibili nella memoria bassa; il nome zImage e la relativa procedura di avvio dell'immagine compressa erano stati introdotti prima di linux-1.0, quando alcune configurazioni del kernel iniziavano ad occupare più dei 512kB disponibili all'accensione del sistema.

Qualunque sia il comando adottato, la compilazione di linux-2.6 genera un output molto più parsimonioso rispetto al comportamento delle precedenti versioni. Invece di stampare nella loro interezza le righe di comando invocate, il sistema visualizza una breve riga di testo che riassume l'operazione in corso. In Figura 1 è riportato un estratto dei messaggi stampati durante la compilazione.

Figura 1 - Output della compilazione

    ostro% make bzImage
    CHK     include/linux/version.h
    UPD     include/linux/version.h
    SYMLINK include/asm -> include/asm-i386
    HOSTCC  scripts/split-include
    HOSTCC  scripts/conmakehash
    [...]
    CC      arch/i386/kernel/process.o
    CC      arch/i386/kernel/semaphore.o
    CC      arch/i386/kernel/signal.o
    AS      arch/i386/kernel/entry.o
    CC      arch/i386/kernel/traps.o
    [...]
    CC      fs/partitions/check.o
    CC      fs/partitions/msdos.o
    LD      fs/partitions/built-in.o
    CC      fs/proc/task_mmu.o
    CC      fs/proc/inode.o
    [...]
    LD      arch/i386/boot/compressed/vmlinux      
    OBJCOPY arch/i386/boot/vmlinux.bin
    HOSTCC  arch/i386/boot/tools/build
    BUILD   arch/i386/boot/bzImage
    Root device is (3, 1)
    Boot sector 512 bytes.
    Setup is 4736 bytes.
    System is 1297 kB
    Kernel: arch/i386/boot/bzImage is ready

Per chi volesse vedere l'intera riga di comando usata per compilare ogni file sorgente, è possibile aggiungere l'assegnamento V=1 nell'invocazione di make: per esempio, make bzImage V=1. Come alternativa si può assegnare il valore "1" alla variabile di ambiente KBUILD_VERBOSE; si noti però che assegnare la variabile di ambiente V non ha alcun effetto sulla compilazione del kernel.

L'uso di una variabile che viene specificata con due nomi diversi a seconda del contesto, è realizzato nel Makefile principale, che verifica dove è stata definita la variabile V, se presente, e utilizzandone il valore solo se è stato definito sulla riga di comando. Questo meccanismo, usato altre due volte all'interno del Makefile, permette di dare un nome univoco alle variabili di ambiente che influenzano la compilazione del kernel, permettendo però di modificare il comportamento di make scrivendo direttive molto più brevi sulla riga di comando di make. Questo perché, si sa, il programmatore è pigro.

La più interessante di queste variabili di ambiente credo sia KBUILD_OUTPUT, abbreviabile come x"O" sulla riga di comando. La variabile specifica la directory di output del processo di compilazione, istruendo make a non modificare in alcun modo l'albero di sorgenti del kernel. KBUILD_OUTPUT si può usare solo a condizione che la directory dei sorgenti sia pulita, non contenga cioè file di output di precedenti compilazioni.

La variabile KBUILD_OUTPUT permette in pratica di compilare lo stesso albero di sorgenti con configurazioni diverse o addirittura per piattaforme diverse, senza dover replicare tutto l'albero dei sorgenti per ogni compilazione (come invece era necessario fare con le versioni precedenti del kernel). Naturalmente usando KBUILD_OUTPUT è anche possibile compilare un albero di sorgenti sul quale non si abbia il permesso di scrittura, per esempio un CD o un disco condiviso in rete tra più macchine. Data la mole dei sorgenti, evitarne copie inutili è sicuramente un grosso vantaggio in termini di spazio su disco.

Le dipendenze

Una interessante novità introdotta nel sistema di compilazione di linux-2.6 è la gestione automatica delle dipendenze. Con "dipendenza" si indica la direttiva con cui si dice al comando make che un certo file di output deve essere rigenerato se un altro file è stato modificato successivamente; si tratta cioè della dichiarazione che il contenuto di un certo file (di output) dipende dal contenuto di un altro file.

Mentre la dipendenza di un file oggetto dal file sorgente C con lo stesso nome è immediata e gestita internamente dal comando make, la dipendenza dai file di header ("intestazione") è più problematica, in quanto un file sorgente C può includere decine di header, e ciascuno di questi includerne altri. Nelle precedenti versioni del kernel, occorreva invocare "make dep" al fine di creare in ogni directory la dichiarazione di tutte le dipendenze di ogni file di output presente in quella directory. Questo meccanismo manuale può dare dei problemi nel momento in cui l'albero dei sorgenti viene modificato (applicando delle patch, per esempio) e ci si dimentica di rigenerare le dipendenze: una prima patch e una prima ricompilazione possono funzionare senza problemi, ma se questa patch ha cambiato le dipendenze dei file dagli header, una successiva modifica all'albero potrebbe non scatenare tutte le ricompilazioni necessarie.

Il meccanismo introdotto nella nuova versione del kernel elimina del tutto il passaggio "make dep", in quanto usa il compilatore stesso per generare i file di dipendenza, tramite l'opzione -DM di gcc. In questo modo, le dipendenze associate ad un file vengono rigenerate ogni volta che questo viene compilato. Nel caso di linux-2.6, le dipendenze associate ad ogni file oggetto, insieme alla riga di comando usata per compilarlo, vengono salvate in un file nascosto nella stessa directory del file oggetto (un oggetto vt.o, per esempio, sarà controllato dal file .vt.o.cmd); tale file viene poi letto da make durante la compilazione, per decidere i comandi da invocare in questa directory.

Cross-compilazione

La "cross-compilazione" è la procedura usata per compilare un eseguibile che giri su una piattaforma diversa da quella dove gira il compilatore. L'esempio più comune oggi è la generazione di kernel e applicazioni per macchine palmari (di solito basate su processore ARM), lavorando su piattaforma i386. Per creare file eseguibili per un'altra piattaforma occorre installare (o compilare) sulla macchina ospite, in questo caso i386 un insieme di strumenti per la cross-compilazione: compilatore, linker, assemblatore e altri. Questi strumenti sono programmi eseguibili al cui nome usuale viene aggiunto un prefisso uguale per tutti; per esempio "arm-linux-gcc", "arm-linux-ld" eccetera.

Il Makefile del kernel fornisce il supporto per la cross-compilazione in maniera immediata: la variabile CROSS_COMPILE, se assegnata, indica il prefisso da usare per invocare gli strumenti di compilazione. Nel caso preso come esempio, quindi, occorretà specificare nella variabili di ambiente o sulla riga di comando di make CROSS_COMPILE=arm-linux- (comprensivo di trattino finale).

La variabile CROSS_COMPILE, però, non può modificare tutte le istanze del comando gcc, in quanto durante la creazione del kernel è necessario compilare alcuni programmi da eseguire immediatamente, sulla macchina ospite. A tal fine il Makefile del kernel distingue tra le invocazioni di CC (il cross-compilatore) e HOSTCC (il compilatore nativo della macchina ospite). L'utilizzo delle due variabili è riportato dai messsaggi stampati durante la compilazione, come si vede in Figura 1.

L'unica differenza di linux-2.6 rispetto a linux-2.4 riguardo la cross-compilazione è la possibilità di assegnare CROSS_COMPILE nelle variabili di ambiente, in quanto nelle precedenti versioni occorreva specificarlo sulla riga di comando di make, oppure modificare il Makefile con un editor. Il costrutto di GNU-make usato è ?=, di assegnazione condizionale: la variabile viene assegnata dal Makefile al suo valore di default solo se non precedentemente definita (per esempio come variabile di ambiente).

I moduli

La compilazione dei moduli rimane immutata rispetto alle precedenti versioni: make modules e make modules_install sono i comandi usati per compilare ed installare i moduli, anche se come abbiamo visto make modules è oggi invocato automaticamente dal semplice make senza argomenti. Il meccanismo di compilazione dei moduli, comunque, è cambiato notevolmente rispetto a linux-2.4, sia per come vengono compilati i moduli sia per come vengono caricati. Una descrizione dettagliata del funzionamento dei moduli con linux-2.6 sarà l'argomento trattato nel prossimo numero.

Alessandro Rubini

La copia letterale e la redistribuzione su qualsiasi supporto di questo articolo nella sua integrità è permessa, purchè questa nota sia conservata.