Tratto da programmazione.it
Gustavo Duarte pubblica una serie di tre post, nei quali si occupa di hardware e memoria, dei processi di avvio e del ruolo del kernel nell'avvio del sistema operativo.
Prendiamo a pretesto ognuna di queste tre parti per fare qualche cenno ai rispettivi argomenti, che sono certamente interessanti anche se stanno un po' dietro le quinte, lavorando prima che si accendano le luci e si aprano le finestre. Forse non è un caso se il processo di boot è più accessibile nei brutti PC rispetto a quanto non avvenga nei colorati Mac.
La spiegazione sulla struttura di una scheda madre con processore Intel Dual Core viene introdotta da una eloquente immagine, in cui si notano subito alcuni dettagli, come ad esempio il collegamento diretto tra processore e northbridge, che presuppone appunto la separazione del chipset sulla scheda madre in due parti, un meccanismo che a quanto pare Intel abbandonerà, come ha già fatto AMD, con l'uscita dei processori Nehalem a partire dal prossimo autunno.
Al northbridge fanno capo memoria RAM e interfacce AGP/PCI, e quindi possiamo considerare il front-side bus come la via di accesso e collegamento tra processore e mondo esterno; il numero di PIN dedicati ai collegamenti determina la quantità di memoria accessibile in lettura o scrittura da parte del processore. Non tutta l'attività del computer avviene però nella memoria RAM, in quanto il processore comunica anche con dei componenti, come schede video o PCI, oltre alla memoria flash del BIOS, di diversa natura.
In tutti questi casi si parla di memory-mapped I/O, in quanto esiste una relazione biunivoca tra dispositivi e memoria, e quindi le richieste di risorse vengono opportunamente smistate nel northbridge in base agli indirizzi mappati. Anche qui un'immagine esemplifica la distribuzione media della memoria fisica e in particolare dell'area compresa tra 640 KB e 1 MB.
La traduzione degli indirizzi logici in indirizzi fisici dipende dalla modalità di funzionamento della CPU (reale o protetta a 32 o 64 bit), così come dipende da questo aspetto la quantità di memoria fisica accessibile, con il limite di 4 GB per i processori a 32 bit, che a sua volta viene limato per via dell'uso precedentemente descritto di almeno un GB di memoria da parte dei dispositivi presenti sulla scheda madre. La CPU in real mode può indirizzare solamente un megabyte di memoria fisica, mentre a 64 bit si arriva fino a 64GB, peraltro supportati da pochi chipset.
Queste informazioni possono forse bastare a chi vuol farsi un'idea del funzionamento di schede madri e processori recenti, mentre per chi vuole approfondire esiste un documento in formato PDF, di un centinaio di pagine, con un titolo significativo e un po' allarmante:What Every Programmer Should Know About Memory. Dopo la descrizione della struttura di scheda madre e processore, la seconda parte del trittico di Gustavo Duarte passa alla descrizione dell'avvio del computer, ma forse per scaramanzia, la prima nota che troviamo riguarda i guasti della CPU, che possono portare ad avere un sistema completamente spento, a parte le ventole, il cosiddetto stato zombie-with-fans, che può essere dovuto tra l'altro a qualche dispositivo USB non funzionante.
In un sistema multicore o multiprocessore, una CPU viene scelta come bootstrap processor o BSP, che si occupa di far girare il codice BIOS e quello di inizializzazione del kernel; gli altri processori (AP, application processor), verranno invece attivati in seguito dal kernel.
Un'osservazione importante riguarda la retrocompatibilità dei processori Intel, che nonostante gli anni trascorsi, possono ancora oggi comportarsi come i processori 8086 del 1978, cosa che fanno nella fase di avvio del computer, trovandosi nella cosiddetta modalità reale, senza paginazione della memoria. Nella fase di avvio, il registro EIP contiene l'indirizzo di memoria per le istruzioni che la CPU andrà a eseguire; si supera il limite di indirizzamento delle vecchie CPU Intel usando un puntatore nascosto chiamato reset vector per fare in modo che venga eseguita l'istruzione all'indirizzo 0xFFFFFFF0. Questa e altre istruzioni sono memorizzate grazie al chipset, che mantiene delle memory map, ovvero mappature con le istruzioni scritte nella flash memory del BIOS, riportate in una opportuna immagine, che presenta il contenuto delle più importanti aree di memoria nel corso dell'avvio del computer.
Dopo l'esecuzione del codice BIOS e l'inizializzazione di parte dell'hardware, inizia la fase detta POST, ovvero Power-on Self Test, che testa il funzionamento dell'hardware più importante: se non funziona la scheda video, ad esempio, il processo di avvio si interrompe e il computer emette alcuni beep; le codifiche di questi ed altri segnali sono elencati nella voce POST di Wikipedia.
Se la scheda video funziona, ma altri componenti vitali no, il blocco del POST è accompagnato da messaggi di testo sul video (basta staccare la tastiera per vedere); se invece tutto funziona, la fase di POST continua tra prove e inizializzazioni, comunicando all'utente l'elenco delle risorse comprensivo di interrupt, porte I/O e intervalli di memoria, anche se non è facile leggere tutti i messaggi prima che scompaiano dal video. Infine, i BIOS più recenti, che seguono le specifiche ACPI costruiscono diverse tabelle dati, usate più avanti dal kernel, che descrivono i dispositivi presenti nel computer. Al termine della fase di POST, il BIOS va alla ricerca del sistema operativo da avviare, seguendo l'ordine di ricerca impostato dal produttore ed eventualmente modificato dal proprietario del computer: anticamente si partiva dal floppy, ma oggi le cose possono andare diversamente. Anche in questo caso, è possibile che il procedimento finisca con un errore del tipo Non-System Disk or Disk Error, se non si trova un dispositivo adatto all'avvio del computer, come quando l'hard disk non funziona e, giustamente, non si trovano più i dischi di avvio, che fino a ieri sbucavano non richiesti da tutti i maledetti cassetti della scrivania.
Se invece il disco di avvio funziona, e se si tratta di un hard disk, il BIOS va a leggere il cosiddetto settore zero o Master Boot Record (MBR), che di solito contiene due componenti vitali, ovvero un programma di bootstrapping, caratteristico per ciascun sistema operativo, e, subito dopo, la tabella di partizione del disco; a questi dettagli non è interessato il BIOS, che si limita a caricare nella locazione di memoria 0x7C00 il contenuto del Master Boot Record ed eseguire le istruzioni contenute in quella locazione di memoria, che potrebbero essere le istruzioni di caricamento per Windows o Linux (LILO e GRUB per l'esattezza), o magari anche un virus: difficile in certi casi stabilire quale sia il caso peggiore.
Maggiormente standardizzata è invece la struttura della tabella delle partizioni, che inizia con un'area di 64 byte divisa in quattro entrate da 16 byte ciascuna, le quali a loro volta descrivono la divisione del disco, permettendo in questo modo di avere diversi volumi o di far girare diversi sistemi operativi sullo stesso disco. L'MBR di Microsoft legge la partition table e trova la sola partizione marcata come attiva, caricandone il settore di boot - che è il primo settore di una partizione, e non sempre dell'intero disco - per avviare il sistema. In questa fase, un eventuale messaggio del tipo "Tabella di partizione non valida" o "Missing Operating System non arriverebbe dal BIOS, ma dal codice MBR caricato dal disco.
I boot loader Linux come LILO e GRUB - non è una famosa coppia di comici - supportano diversi sistemi operativi, file system e configurazioni di avvio, e possono seguire un approccio diverso da quello tradizionale di Windows - trova la partizione attiva e di lì avvia il sistema - essendo più complessi anche come struttura, dal momento che sono in grado, dopo che l'utente ha scelto cosa avviare, di leggere il kernel a partire dalla partizione di avvio, caricarlo in memoria e saltare al codice di bootstrap del kernel medesimo. Lo stesso vale per NTLDR presente in Windows fino alla versione Server 2003.
Prima di concludere il post, lungo e forse un po' complesso ma interessante, Gustavo Duarte inserisce un'ultima divagazione sui trucchi, che permettono di caricare il kernel Linux di dimensioni superiori ai 640K, che è la RAM disponibile in modalità reale, con una specie di altalena tra questa modalità e quella protetta, altalena che consente di accedere alla memoria sopra 1MB pur trovandosi ancora nel BIOS. I più coraggiosi troveranno traccia di queste transizioni nel codice sorgente di GRUB. Siamo arrivati al punto della storia, nel quale il boot loader carica in memoria l'immagine del kernel, ovvero una copia esatta del file, che potrebbe essere ad esempio /boot/vmlinuz-2.6.22-14-server, a sua volta diviso in due parti: la prima, più piccola, contenente il codice in modalità reale e caricata in memoria sotto la barriera dei 640K, e il bulk del kernel, che gira invece in modalità protetta, al di sopra del primo megabyte di memoria.
L'articolo fa riferimento in particolare all'avvio di un sistema Linux x86, di cui illustra l'immagine della memoria in fase di avvio e le principali fasi del processo di inizializzazione del kernel, facendo riferimento ai sorgenti del kernel stesso. Si parte quindi da arch/x86/boot/heades.S che, dopo il codice legacy del boot sector, contiene i primi 15 byte dell'header in modalità reale e, a seguire, l'entry point per la modalità reale, un'istruzione di salto scritta direttamente in codice macchina, seguita dall'esecuzione di una istruzione assembly chiamata start_of_setup, che dopo aver azzerato il segmento bss o area delle variabili statiche, passa all'esecuzione del main.
Qui abbiamo una serie di operazioni, come il settaggio della memoria video, ma soprattutto il passaggio alla modalità protetta, che a sua volta richiede altre operazioni preliminari riguardanti interrupt e memoria. Il salto in modalità protetta è guidato da un'altra routine assembly; adesso non esiste più il limite dei 640K e si possono indirizzare fino a 4 GB di memoria. Si va quindi ad avviare e decomprimere il kernel a 32 bit, operazione che ha un riscontro anche a video durante l'avvio di Linux, e che è seguita dall'altro messaggio "Booting the kernel", ovvero si entra nella modalità protetta all'inizio del secondo megabyte di RAM all'indirizzo 0x100000.
La routine startup_32 contiene le inizializzazioni a 32 bit, comprensive di pulizia del segmento bss per il kernel in modalità protetta, prepara la tabella di memoria per i descrittori globali, costruisce le tabelle per la paginazione della memoria, inizializza uno stack, crea la tabella dei descrittori globali per la memoria, e infine salta a start_kernel(), funzione architecture-independent, che contiene una lunga lista di inizializzazioni per sottosistemi e strutture dati del kernel stesso. Queste fasi sono riassunte visivamente e descritte in dettaglio, e si intravedono comunque già nei commenti del codice C della funzione.
Tra le funzioni chiamate in questa fase, rest_init() determina la chiamata di kernel_init() e di schedule(), per poi andare a nanna con la chiamata di cpu_idle(), il thread inattivo per il kernel di Linux, che si toglie docilmente di mezzo quando c'è del lavoro da fare; kernel_init() determina anche l'inizializzazione di altre eventuali CPU: fino a questo momento ha lavorato solo il cosiddetto boot processor, come già ricordato nella prima parte di questa serie.
La sequenza di avvio si conclude con init_post(), che cerca di eseguire alcuni processi utente (/sbin/init, /etc/init, /bin/init, e /bin/sh); se nessuno di questi si avvia, abbiamo il cosiddetto kernel panic; solitamente invece il sistema parte con init, demone con PID 1, che legge il file di configurazione per vedere innanzitutto quali processi lanciare (magari X11 e sicuramente il login all'avvio).
Prima delle necessarie indicazioni bibliografiche, che chiudono questa visita turistica del kernel e dintorni, troviamo un cenno, con relativa immagine esplicativa, al processo di avvio in Windows; tra le differenze si sottolinea la presenza del codice kernel in modalità reale e di parte di quello in modalità protetta nel boot loader NTLDR.