Firewall Copertina Root |
Articoli
Il Boot di Linux
Ristampato con il permesso del Linux Journal
Questo articolo descrive i passi compiuti per avviare il kernel di Linux. Benché questo tipo di informazioni non sia rilevante per la funzionalità del sistema, risulta interessante vedere come le diverse architetture eseguono la fase di "boot".
Un computer è un apparato molto complesso, ed il suo sistema operativo è uno strumento elaborato che nasconde le complessità hardware per fornire un ambiente semplice e standardizzato all'utente finale. All'accensione del sistema, comunque, il software di sistema deve lavorare in un ambiente limitato, e deve caricare il kernel usando questo ambiente dalle scarse funzionalità. Di seguito viene descritta la fase di boot per tre diverse piattaforme: l'antiquato PC e i più recenti calcolatori basati su Alpha e Sparc. Il PC occuperà la maggior parte dello spazio in questo articolo perché è ancora la piattaforma più diffusa, ed anche perché è quella più difficile da avviare. Purtroppo in questo articolo del Kernel Korner non troverete alcun codice d'esempio, perché il linguaggio Assembly è diverso su ciascuna piattaforma ed è di difficile comprensione per la maggior parte dei lettori.
Per consentire al computer di poter fare qualcosa quando viene acceso, il sistema è progettato in modo che il processori inizi ad eseguire le istruzioni del suo firmware. Il firmware è il "software non rimovibile" che si trova nella ROM del sistema; alcune case produttrici lo chiamano BIOS (Basic Input-Output System) per sottolineare il suo ruolo, altri lo chiamano PROM o "flash" per accentuare la sua implementazione in hardware, altri ancora lo chiamano "console" per focalizzare l'attenzione sull'interazione con l'utente.
Solitamente il firmware verifica che l'hardware lavori correttamente, recupera una parte del kernel dalla memoria di massa e lo esegue. Questa prima parte del kernel deve caricare la parte rimanente ed inizializzare tutto il sistema. In questo articolo non tratterò le problematiche del firmware, e mi limiterò a considerare il codice del kernel, il cui sorgente è distribuito all'interno di Linux.
Al momento dell'accensione, il microprocessore x86 (anche i recenti Pentium Pro) è solo un processore a 16 bit che vede solo 1 MB di memoria. Questo ambiente è chiamato "modalità reale", ed esiste per esigenze di compatibilità con microprocessori più vecchi della stessa famiglia. Tutto ciò che costituisce un sistema completo è vincolato a risiedere in questo spazio d'indirizzamento: il firmware, il buffer video, lo spazio per le schede di espansione e un po' di RAM (i maledetti 640kB).
Per rendere le cose più difficili, il firmware del PC può caricare solo mezzo kilobyte di codice, e stabilisce la sua configurazione di memoria prima del caricamento di questo primo settore. Qualunque sia il supporto di memorizzazione usato per il boot, il primo settore della partizione di boot viene caricato in memoria all'indirizzo 0x7c00, dove l'esecuzione inizia. Quello che succede a 0x7c00 dipende dal boot loader usato. Di seguito analizzeremo tre situazioni: nessun boot loader, lilo, loadlin.
zImage
e bzImage
Sebbene sia abbastanza raro avviare il sistema senza un boot loader,
si può fare copiando il "raw kernel" (il file chiamato
zImage
) direttamente su un floppy. Un comando del tipo
``cat zImage > /dev/fd0
'' funzionerà perfettamente
su Linux, anche se su altri sistemi Unix l'unico modo sicuro per
scrivere sul floppy è usare il comando
dd
. L'immagine "raw" sul floppy così creata
può essere configurata usando il comando rdev
, ma
questo è al di fuori dell'argomento trattato qui.
Il file zImage
è l'immagine compressa del kernel
e si trova in arch/i386/boot
dopo aver compilato il
kernel con il comando make zImage
o make
boot
(il secondo comando è quello che preferisco,
perché funziona anche sulle altre piattaforme). Se abbiamo
costruito una "big zImage" invece, il file si chiama
bzImage
e si trova nella stessa directory.
Avviare un kernel x86 è un compito complesso a causa del
limite imposto alla memoria disponibile (in modalità reale. Il
kernel di Linux cerca di massimizzare l'uso dei 640 kB bassi
sposandosi più volte all'interno della memoria. Ma vediamo in
dettaglio i passi compiuti da un kernel zImage; i seguenti nomi di
file sono tutti relativi ad arch/i386/boot
.
bootsect.S
, ed è un file Assembly in
modalità reale.
setup.S
) si prende cura di alcune inizializzazioni
hardware e consente di cambiare la modalità testo di default
(video.S
). La selezione del modo testo è diventata
una opzione di compilazione dal kernel 2.1.9 in poi.
setup.S
passa in modalità
protetta (protected mode) e salta a 0x1000, dove risiede il
kernel. Tutta la memoria disponibile può essere finalmente
vista, ed il sistema può cominciare a funzionare.
I passi visti in precedenza rappresentavano tutta la fase di avvio
quando i kernel erano abbastanza piccoli da poter stare in mezzo
megabyte (negli indirizzi tra 0x10000 e 0x90000). Quando il kernel era
piccolo esso risiedeva a 0x1000, ma la continua aggiunta di
funzionalità lo ha portato a superare il mezzo mega: il codice
che si trova all'indirizzo 0x1000 non è più il vero
kernel Linux, ma piuttosto il codice relativo alla decompressione del
programma gzip
. I seguenti passi sono poi necessari per
decomprimere il vero kernel ed eseguirlo:
compressed/head.S
, ed il
suo ruolo è decomprimere il kernel: esso chiama la funzione
decompress_kernel()
, definita in
compressed/misc.c
, la quale chiama inflate()
che scrive il suo output all'indirizzo 0x100000 (un mega). La memoria
alta adesso viene vista, poiché il microprocessore è
definitivamente fuori dal suo limitato ambiente iniziale (il modo
reale).
../kernel/head.S
, fuori
dalla directory boot
. La fase di boot è ora
finita, e head.S
(il codice che si trova in 0x100000,
quello che si trovava in 0x1000 prima dell'introduzione del kernel
compresso) può completare l'inizializzazione del
microprocessore e chiamare start_kernel()
. Da questo
punto in poi, tutto il codice è scritto in C.
I passi descritti in precedenza valgono nell'assunzione che il kernel
compresso non occupi più di mezzo mega. Bench´ questa
ipotesi sia realizzata nella maggior parte dei casi, un sistema pieno
di device driver e di filesystem compilati staticamente nel kernel
può tranquillamente eccedere questo limite (questo sottolinea
ancora una volta l'importanza della modularizzazione del kernel). Ad
esempio, il limite può venir superato dai dischi di
installazione del sistema: questi kernel devono contenere molti driver
e superano facilmente il mezzo mega. Per poter avviare sistemi di
queste dimensioni occorre qualche nuovo trucco. La soluzione adottata
si chiama bzImage
, ed è stata introdotta dalla
versione 1.3.73 del kernel.
Un kernel bzImage
viene generato dal comando
"make bzImage
", invocato dalla directory principale dei
sorgenti del kernel. Questo tipo di immagine del kernel si avvia in
modo molto simile alla zImage
, con alcune piccole
differenze:
make boot
" genera ancora la
zImage
(almeno al momento in cui questo articolo viene
scritto, ma in futuro potrebbe cambiare).
setup.S
non sposta più il sistema a 0x1000
(4k), ma salta direttamente ad eseguire il codice all'indirizzo
0x100000 (1M), dopo essere passato in modalità protetta.
L'indirizzo di "un mega" è quello dove i dati sono stati
spostati dalla chiamata BIOS descritta nel passo precedente.
La regola per costruire le bzImage
si può trovare
nel Makefile
: essa interessa molti file contenuti in
arch/i386/boot
. Una bella caratteristica di
bzImage
è che quando kernel/head.S
viene eseguito non si accorgerà del lavoro addizionale, e tutto
continuerà come nel caso di zImage
.
La maggior parte degli utenti di Linux-x86 non avviano il kernel dal floppy, e usano piuttosto il Linux Loader (LiLo) dall'hard disk. Lilo sostituisce parte del processo descritto in precedenza, in modo da essere in grado di avviare un kernel sparso in tutto un disco. Questo consente all'utente di avviare un file di kernel da una partizione, senza utilizzare il floppy.
In pratica, Lilo usa i servizi del BIOS per caricare i singoli
settori dal disco e poi salta a setup.S. In altre parole, Lilo sistema
le cose in memoria come fa bootsect.S
per il raw kenrel;
in questo modo il meccanismo di avvio tradizionale può essere
completato senza problemi. Lilo è anche in grado di gestire la
linea di comando del kernel e questa è già una buona
ragione per evitare di avviare il raw kernel dal floppy.
Per avviare una bzImage
tramite Lilo, è
necessario disporre almeno della versione 18 di Lilo. Versioni
più vecchie non sono in grado di caricare segmenti di codice in
memoria alta, operazione necessaria per caricare immagini grosse.
Il principale svantaggio di Lilo sta nel suo uso del BIOS per caricare il sistema. Questo obbliga ad avere il kernel e altri file rilevanti in dischi che siano visti dal BIOS, e all'interno di questi solo nei primi 1024 cilindri (i BIOS più recenti aggirano questo limite giocando sporco con i parametri del disco, ma questo comporta che la tabella delle partizioni non rispecchi la geometria del disco: questo dischi non potranno più essere usati su calcolatori più vecchi). Come si vede, usando il firmware dei PC ci si rende facilmente conto di quanto tale architettura sia obsoleta.
Anche chi non usa Lilo, può apprezzare i file di documentazione distribuiti con il suo codice sorgente. Essi contengono molte informazioni interessanti sul processo di boot del PC e spiegano come fronteggiare quasi tutte le situazioni possibili.
setup.S
. E`
differente da Lilo in quanto non solo deve sottostare alle limitazioni
del BIOS, ma deve anche sbarazzarsi di una configurazione di memoria
prestabilita senza compromettente la stabilità del
sistema. D'altro canto, Loadlin non è limitato alla lunghezza
di mezzo kB perché non è un boot sector ma un completo
file di codice eseguibile.
La versione 1.6 del programma e le successive sono in grado di
caricare immagini bzImage
.
Loadlin è in grado di passare una linea di comando al kernel
e per questo è flessibile quanto Lilo; la maggior parte delle
volte un utente di Loadlin finirà per scrivere un file
linux.bat
che passi per passare una linea del comando
completa a Loadlin quando il comando linux
viene
invocato.
Loadlin può anche essere usato per trasformare un qualunque PC
connesso in rete in una macchina Linux: a questo fine è solo
necessario disporre di un'immagine del kernel predisposta per montare
la "partizione di root" via NFS, l'eseguibile Loadlin ed un file
linux.bat
che contenga i corretti indirizzi
Internet. Ovviamente serve anche un server NFS correttamente
configurato, ma ogni macchina Linux può adempire questo
compito. Per esempio, la seguente linea di comando commuta il PC della
mia ragazza alfred.unipv.it in una workstation:
loadlin c:\zimage rw nfsroot=/usr/root/alfred \
nfsaddrs=193.204.35.117:193.204.35.110:193.204.35.254:255.255.255.0:alfred.unipv.it
Come si può immaginare, il codice non è così semplice come può apparire: in realtà esso deve occuparsi di molti dettagli, come passare al kernel la linea di comando, ricordarsi quale tecnica di boot viene utilizzata e così via. Il lettore curioso può guardare il codice sorgente per saperne di più e leggere i commenti degli autori contenuti nel codice. Si trovano molte informazioni nei commenti e spesso sono anche divertenti da leggere.
Personalmente non credo che qualcuno avrà mai bisogno di modificare il codice di boot, in quanto le cose diventano molto più interessanti quando il sistema è completamente attivo: a quel punto si possono sfruttare tutte le potenzialità del microprocessore e tutta la RAM disponibile senza impazzire con problemi troppo di basso livello.
La piattaforma Alpha è molto più matura del PC e il suo firmware riflette questa maturità. La mia esperienza con Alpha è limitata al firmware ARC, che del resto è il più diffuso.
Dopo aver compiuto il solito riconoscimento dei dispositivi, il firmware visualizza un menu di boot che permette di scegliere cosa avviare. Il firmware è in grado di leggere una partizione del disco (ma solo una partizione FAT), in questo modo l'utente è in grado di avviare un file, senza bisogno di smanettare con il boot sector e dover costruire una mappa dei blocchi del disco.
Il file che viene avviato è di solito
linload.exe
, il quale carica Milo (il "Mini Loader", il
cui nome è uno scherzoso riferimento alla dimensione del
programma). Per poter avviare Linux tramite il firmware ARC occorre
avere una piccola partizione FAT sul disco rigido, per contenere
linload.exe
e Milo. Il kernel Linux non ha comunque
bisogno di avere accesso alla partizione, a meno che non si debba
aggiornare Milo, per cui il supporto per il filesystem FAT può
essere lasciato fuori dal kernel senza per questo avere problemi.
In pratica, l'utente può scegliere tra diverse possibilità: il menu di boot può essere configurato per avviare Linux di default, e Milo può addirittura essere trasferito nella memoria flash della macchina, in modo da poter fare a meno della partizione FAT. In ogni caso, alla fine il controllo viene passato a Milo.
Il programma Milo è in qualche modo una versione ridotta del
kernel Linux: contiene gli stessi device driver di Linux ed il
supporto per alcuni filesystem; a differenza del kernel però
non supporta la gestione dei processi e include il codice per
l'inizializzazione dell'Alpha. Milo è in grado di impostare ed
attivare la memoria virtuale, e può caricare un file sia da una
partizione ext2 che da un disco iso9660. Il "file" in questione viene
caricato all'indirizzo virtuale 0xfffffc0000300000
e
viene eseguito. L'indirizzo virtuale usato è quello dove deve
girare il kernel Linux: è improbabile che Milo sia usato per
caricare qualcosa che non sia Linux, con l'eccezione del programma
fmu
(flash management utility) usato per salvare Milo
nella flash ROM. fmu
viene compilato per partire dallo
stesso indirizzo virtuale del kernel ed è distribuito insieme a
Milo).
E` interessante notare che Milo include anche un piccolo emulatore 386 ed alcune funzionalità del BIOS del PC. Questo supporto è necessario per eseguire l'autoinizializzazione delle periferiche ISA/PCI (le schede PCI, sebbene pretendano di essere indipendenti dal microprocessore, usano il codice macchina Intel nelle loro ROM).
Ma, se Milo fa tutto di questo, cosa è lasciato al kernel Linux?
Molto poco, in effetti. Il primo codice del kernel ad essere eseguito
in Linux-Alpha è arch/alpha/kernel/head.S
, il
quale no fa altro che impostare alcuni puntatori e saltare a
start_kernel()
. In effetti, kernel/head.S
per Alpha è molto piè corto dell'equivalente sorgente
per x86.
Per chi non vuole usare Milo c'è un'altra alternativa, anche
se non molto conveniente. In arch/alpha/boot
risiedono i
sorgenti di un "raw loader" che viene compilato usando il comando
"make rawboot
" dalla directory principale dei sorgenti
Linux. Il programma è in grado di caricare un file da una
regione sequenziale di una periferica (il floppy o il disco rigido)
usando le chiamate del firmware.
In pratica, il raw loader svolge un compito simile a quello che
bootsect.S
svolge per la piattaforma PC, e questo obbliga
a copiare il kernel su di un floppy o una partizione raw. Dovrebbe
essere evidente come non ci siano veri motivi per provare questa
tecnica, che è piuttosto complessa e non offre la
flessibilità offerta da Milo. Personalmente non so neppure se
questo loader funzioni ancora: il "PALcode" usato da Linux è
esportato da Milo ed è diverso da quello ha esportato dal
firmware ARC. Il PALcode è una libreria di funzioni di basso
livello, usata dai microprocessori Alpha per implementare la
paginazione e altre operazioni di basso livello. Se il PALcode attivo
implementa operazioni diverse da quelle che il software si aspetta, il
sistema non può funzionare.
Avviare una macchina Sparc è simile ad avviare un Alpha dal punto di vista dell'utente, mentre è simile ad avviare un PC dal punto di vista software.
L'utente vede che il firmware carica un programma e lo esegue, il programma a sua volta può recuperare un file da una partizione del disco e decomprimerlo. Il "programma" in questione si chiama Silo, e può leggere un file sia da partizioni ext2 che ufs (SunOS, Solaris). A differenza di Milo (e similmente a Lilo), Silo può avviare anche un altro sistema operativo. Con Alpha non c'è bisogno questa funzionalità in quanto il firmware è già in grado di avviare diversi sistemi operativi: quando Milo esegue, la scelta è già stata fatta (ed è la Scelta Giusta).
Quando un calcolatore Sparc parte, il firmware carica un boot sector dopo aver eseguito la verifica dell'hardware e l'inizializzazione dei dispositivi. E` interessante notare come i dispositivi Sbus sono effettivamente indipendenti dalla piattaforma ed il loro programma di inizializzazione è codice Forth portabile, piuttosto che linguaggio macchina di un particolare microprocessore.
Il boot sector che viene caricato è quello che si trova in
/boot/first.b
nel filesystem Linux-Sparc, ed e' composta
da 512 byte. Tale settore viene caricato all'indirizzo 0x4000 ed il
suo ruolo è quello di recuperare dal disco
/boot/second.b
e metterlo all'indirizzo 0x280000 (2.5
MB); la scelta di questo indirizzo dipende dal fatto che le specifiche
della Sparc richiedono che almeno 3 MB di RAM siano mappati durante la
fase di boot.
Tutto il resto del lavoro viene fatto dal boot loader di secondo
livello: esso è linkato con libext2.a
per poter
accedere alle partizioni di sistema, e può quindi caricare
un'immagine del kernel dal filesystem Linux. second.b
può anche decomprimere l'immagine perché include
inflate.c
, dal programma gzip
.
Il codice di second.b
utilizza un file di configurazione
chiamato /etc/silo.conf
, la cui struttura è molto
simile al lilo.conf
dei PC. Siccome il file viene letto
durante la fase di boot, non occorre re-installare la mappa del kernel
quando se ne aggiunge uno nuovo (a differenza di quanto si fa sul PC).
Quando Silo mostra il suo prompt l'utente può di scegliere una
qualsiasi immagine del kernel (o una altro sistema operativo)
specificati in silo.conf, oppure si può specificare un percorso
completo (una coppia device/pathname) in modo da caricare un'altra
immagine di kernel senza dovere editare il file di configurazione.
Silo carica il file che viene avviata all'indirizzo 0x4000. Questo
significa che il kernel deve essere più piccolo di 2.5 MB: se
è più grande, Silo si rifiuterà di caricarlo per
non sovrascrivere la sua propria immagine. Nessun kernel per
Linux-Sparc concepibile attualmente può essere più
grande di questo limite, a meno di compilarlo con "-g
"
per avere le informazioni di debugging disponibili. In questo caso
bisogna usare il comando strip
per ridurre l'immagine
prima di passarla a Silo. Alla fine, Silo decomprime il kernel e lo
rimappa, posizionando l'immagine all'indirizzo virtuale
0xf0004000
. Il codice che viene eseguito dopo Silo
è (come si può immaginare)
arch/sparc/kernel/head.S
. Il sorgente include tutta le
tabelle di "trap" per il microprocessore ed il codice necessario per
preparare il computer e chiamare start_kernel()
. La
versione per Sparc di head.S
risulta abbastanza grande.
start_kernel()
in poi
Dopo che l'inizializzazione specifica per l'architettura è
completata, init/main.c
prende il controllo del
microprocessore (qualunque sia il processore). La funzione
start_kernel()
chiama subito setup_arch()
,
che è l'ultima funzione dipendente dall'architettura. A
differenza dell'altro codice, comunque, setup_arch()
può sfruttare tutte le caratteristiche del microprocessore, ed
è un codice molto più facile da comprendere rispetto a
quelli descritti in precedenza. La funzione è definita in
kernel/setup.c
sotto ciascuna architettura supportata.
strart_kernel()
, poi, inizializza tutti i sottosistemi
dei kernel (IPC, networking, buffer cache, ecc.). Dopo aver completato
l'inizializzazione, queste due linee completano la funzione:
kernel_thread(init, NULL, 0);
cpu_idle(NULL);
Il thread init
è il processo numero 1: esso monta
la partizione di root ed esegue /linuxrc
se
CONFIG_INITRD
è stato attivato in compilazione; la
funzione quindi esegue il programma init
. Se init non
viene trovato, allora viene eseguito /etc/rc
. In
generale, l'uso di /etc/rc
è sconsigliato, in
quanto init
è molto piè flessibile di uno
script di shell nel gestire la configurazione del sistema. In effetti,
la versione 2.1.32 del kernel ha rimosso l'invocazione di
/etc/rc
come obsoleta.
Se né init
né /etc/rc
possono essere eseguiti, o se terminano, allora la funzione esegue
/bin/sh
ripetutamente (ma dalla 2.1.21 in poi la shell viene
eseguita una volta solo). Questa funzionalità esiste solo come
salvaguardia in caso di problemi: se l'amministratore del sistema rimuove o
corrompe init per errore, o se viene tolto dal kernel il supporto per gli
eseguibili a.out, dimenticandosi che il vecchio init non è stato
ricompilato, allora si apprezzerà di avere almeno una shell
attiva dopo aver fatto reboot.
Il kernel non ha nulla da fare dopo aver lanciato il processo numero
1, e tutto il resto è gestito nello spazio utente (da init
,
/etc/rc
o /bin/sh
).
E il processo 0? Si è visto come il cosiddetto "idle task" esegue
cpu_idle()
: questa funzione chiama idle()
in un ciclo
senza fine. La funzione idle()
è dipendente dall'architettura
e, solitamente, si occupa di spegnere il microprocessore per ridurre i
consumi ed aumentare la durata del processore stesso.
di Alessandro Rubini traduzione di Andrea Mauro
Firewall Copertina Root |