Il Boot di Linux: come le diverse architetture eseguono la fase di "boot".

Creato il 20 settembre 2010 da Linuxlandia
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.

Il calcolatore all'accensione.

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.

Il PC.

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.

Avviare 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.

  • Il primo settore (eseguito a 0x7c00) sposta se stesso all'indirizzo 0x90000 e carica in sequenza alcuni settori ulteriori di codice, ottenendoli dal dispositivo usato per il boot tramite funzioni del firmware. La parte rimanente del kernel viene poi caricata all'indirizzo 0x10000. Questo comporta che la dimensione massima consentita sia di mezzo mega di dati (ma questa è l'immagine compressa). Il codice del boot sector si trova in bootsect.S, ed è un file Assembly in modalità reale.
  • Poi, il codice all'indirizzo 0x90200 (definito in 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.
  • Più tardi, tutto il kernel viene spostato da 0x10000 (64K) a 0x1000 (4K). Questo spostamento sovrascrive i dati del BIOS immagazzinati in RAM e, da questo momento in poi, non sarà più possibile eseguire chiamate al BIOS. La prima pagina fisica non viene toccata perché è la cosiddetta "pagina zero" (zero-page), usata per la gestione della memoria virtuale.
  • A questo punto 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:

  • Il codice a 0x1000 è 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).
  • Dopo la decompressione, head.S salta al vero inizio del kernel. Il codice attinente si trova in ../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 vari movimenti di dati che avvengono durante la fase di boot sono rappresentati in figura.

La figura è anche disponibile in postscript come boot-lj.ps.

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:

  • Quando il sistema viene all'indirizzo 0x10000, una piccola routine di supporto viene invocata ogni volta che un blocco da 64 kB viene letto dal disco. La routine di supporto sposta i blocchi di dati nella memoria alta usando una speciale chiamata del BIOS. Solo i BIOS abbastanza recenti implementano questa funzionalità, e per questo il comando "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.
  • Il decompressore, che si trova all'indirizzo "un mega" scrive l'immagine del kernel decompresso in memoria bassa finché questa non è piena, poi scrive in memoria alta dopo l'immagine compressa. I due pezzi sono in seguito riassemblati all'indirizzo 0x100000 (un mega). Sono necessari molti spostamenti di memoria per eseguite questo lavoro correttamente, ma non è il caso di scendere in ulteriori dettagli.

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.

Lilo.

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.

Loadlin.

Se si vuole avviare il Sistema Operativo (maiuscolo) da un altro sistema operativo (minuscolo), allora Loadlin è lo strumento da usare. Il programma è simile a Lilo in quanto carica il kernel da una partizione del disco e quindi salta a 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:

Ulteriori informazioni.

 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.

L'Alpha.

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.

La Sparc.

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.

Da 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/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.


Se ti è piaciuto l'articolo , iscriviti al feed cliccando sull'immagine sottostante per tenerti sempre aggiornato sui nuovi contenuti del blog:



Potrebbero interessarti anche :

Possono interessarti anche questi articoli :