Le fasi del boot - parte 1

Da Guide@Debianizzati.Org.
Versione del 14 mag 2013 alle 21:15 di Mm-barabba (discussione | contributi) (Creata pagina con '==Introduzione== Credo che per il primo articolo della rivista non ci sia argomento migliore se non iniziare dall'inizio. Cosa succede quando accendiamo la nostra macchina? In ...')
(diff) ← Versione meno recente | Versione attuale (diff) | Versione più recente → (diff)
Vai alla navigazione Vai alla ricerca

Introduzione

Credo che per il primo articolo della rivista non ci sia argomento migliore se non iniziare dall'inizio.

Cosa succede quando accendiamo la nostra macchina? In questo articolo voglio presentare una panoramica più o meno dettagliata di quello che succede dal momento in cui premiamo il pulsante di accensione del nostro pc fino al cursore lampeggiante sul nostro terminale. Tutto questo si chiama fare il "booting " del PC.

La prima fase di avvio di un PC si definisce con il nome di bootstrapping.

Cenni storici

Il termine boot deriva dall'abbreviazione di bootstrap e storicamente è, metaforicamente parlando, preso dalla linguetta cucita posteriormente sugli stivali di pelle che permetteva a una persona di indossare i propri stivali senza un aiuto esterno [1].

Negli anni 50 premere il tasto di bootstrap di una macchina significava far leggere a un programma cablato, un programma di bootstrap da una scheda perforata senza un ulteriore aiuto dell'operatore.

Sebbene altre macchine precedentemente avessero iniziato ad usare il termine boot, la data più accreditata per i sistemi Unix è quella del 1971 con la prima edizione di "The Unix Programmer's Manual" del 3 Novembre.

Il termine bootstrap viene usato per la prima volta da IBM nel 1952 per le sua macchina 701. Con un bottone di accensione questa macchina leggeva la prima parola a 36 bit da una scheda perforata, da un lettore di schede, da un nastro magnetico o una unità a cilindri (predecessore del nostro odierno hard disk), metà parola veniva poi utilizzata per eseguire una istruzione che caricava altre istruzioni in memoria.

Le fasi del boot

L'avvio di un PC si esegue, a causa delle retrocompatibilità, ancora in un "old-fashioned mode", cioè in vecchio stile e i passi di avvio di Linux (i primi due punti sono comunque comuni indipendentemente dal sistema operativo) in una macchina x86 si possono riassumere in questi punti [3]:

1. Il BIOS si avvia e cerca il primo dispositivo che ha attivo il flag di boot.

2. Carica il settore di boot chiamato bootsector in una specifica locazione di memoria.

3. Bootsector (o un altro bootloader come LILO o GNU GRUB) carica setup, alcune routine per la decompressione e l'immagine del kernel compressa.

4. il kernel viene decompresso in modalità protetta.

5. Inizializzazione a basso livello viene fatta da routine in assembly.

6. Inizializzazione di alto livello in C

Questo il boot in a nutshell, vediamo ora di fare una panoramica più approfondita se non nel firmware del BIOS, non ancora disponibile al momento su tutti i pc (il progetto OpenBIOS si prefigge di creare una versione free per il firmware del BIOS [2]), almeno su quella del codice di GNU/Linux iniziando a vedere cosa fa un bootloader.

Cos'è un bootloader?

Quando un PC (i386) si avvia la prima cosa che il BIOS fa è quella di leggere l'istruzione che si trova all'indirizzo FFFF:0000, che corrisponde fisicamente alla memoria 0xFFFF0 (all'avvio i registri sono inizializzati a questi valori %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000, %eip = 0x0000FFF0). Questa locazione, che si trova vicino alla fine dell'area di memoria di sistema (per le macchine a 32 bit si intende), contiene un jump al programma di startup del BIOS che dopo aver fatto dei check (POST Power On Self Test), cerca il dispositivo di boot che contiene nel suo primo settore la "signature" di boot 0xAA55 (vedremo in seguito del codice scritto in C per creare un settore di boot).

Il bootloader è il programma che carica il kernel del nostro sistema operativo. Un bootloader normalmente è diviso in due fasi, la fase 1 e la fase 2 (in alcuni casi, come GRUB, esiste anche una fase intermedia chiamata 1.5).

Nel caso del bootloader di Linux (almeno fino alla versione 2.4) la fase 1 è quella che si occupa di caricare il codice che dovrà a sua volta caricare il kernel, mentre nel caso di altri bootloader, la fase 1 caricherà il codice del corrispettivo bootloader che ci permetterà poi di caricare il nostro kernel preferito. Questo codice si trova nel primo settore di 512 byte e non può superare i primi 446 byte a causa dei restanti 64bytes che definiscono la partition table (più i due byte di signature). Questo settore è comunemente conosciuto come MBR : Master Boot Record. La fase 2 di un bootloader caricherà il kernel e le routine di decompressione (naturalmente stiamo parlando del caso di un kernel Linux) fino a chiamare la nostra init che avvierà il sistema .


Anche se il kernel 2.6 ha abbandonato per ovvi motivi la prima fase del suo bootloader affidandosi a GRUB o LILO, presenterò qui una panoramica del boot concentrandomi sul bootloader di Linux dando un occhio al primo codice e poi alla versione del kernel 2.6.

I boot loader di Linux

I bootloader per Linux più conosciuti sono sicuramente LILO e GRUB . Come vedremo in seguito il protocollo di boot di Linux ha un identificativo che il bootloader deve passare in fase di setup del kernel.

Ecco qui una lista di bootloader capaci di avviare Linux [5] :

0  LILO
1  Loadlin
2  bootsect-loader
3  SYSLINUX
4  EtherBoot
5  ELILO
7  GRuB
8  U-BOOT
9  Xen

Presumibilmente (e immancabilmente) ce n'è uno, o meglio c'era fino alla versione 2.4 del kernel, bootsect-loader (credo), che è proprio quello di Linux stesso. Questo articolo mostra parte del codice del bootloader di Linux con qualche accenno storico alla versione 0.0.1 di Linux che servirà per introdurre poi il codice del kernel 2.6 .

Uno sguardo storico al primo codice

Permettetemi questa parafrasi: "All'inizio fu boot.s" :) . boot.s contiene il codice per il bootloader che avvierà il sistema e si preoccuperà di caricare in seguito head.s . Nel 1991 Linus Torvald aveva un HD di 40MB (basta guardare in config.h), 8MB di memoria e di sicuro un floppy drive da cui faceva il boot .

Presento qui delle parti di codice di boot.s (nei kernel piu' recenti si chiama bootsector.S) , presenterò solo qui la parte di bootloader, il resto sarà dedicato al vero startup del kernel 2.6 (il lettore che vuole approfondire lo può fare leggendo direttamente l'intero codice dal sorgente):

BOOTSEG = 0x07c0
INITSEG = 0x9000
SYSSEG  = 0x1000                        | system loaded at 0x10000 (65536).
ENDSEG  = SYSSEG + SYSSIZE

Qui sopra (esattamente come inizia boot.s) vengono definite le costanti degli indirizzi di memoria. La prima cosa che il bios fa quando trova un dispositivo di boot (in base alla signature) è quella di muovere i primi 512 byte all'indirizzo di BOOTSEG.

entry start
start:
       mov     ax,#BOOTSEG
       mov     ds,ax                   | ds,es different data segments registers
       mov     ax,#INITSEG
...

le prime linee di codice di boot.s spostano il codice del bootloader all'indirizzo #INITSEG. Il registro di stack %ss viene impostato a $INITSEG .

mov ss,ax mov sp,#0x400 | arbitrary value >>512

si noti come lo stack pointer viene valorizzato a un valore arbitario maggiore di 512 byte. Siamo realmente all'inizio della fase di boot, ricordiamoci che non siamo in protected-mode ma ancora in real address, la memoria è completamente lineare.

Una volta che il codice è stato spostato a #INITSEG, il codice stampa il messaggio di "Loading system ..." .

msg1:
       .byte 13,10
       .ascii "Loading system ..."
       .byte 13,10,13,10

Bene il codice del bootloader si trova in memoria e ora deve caricare il sistema e "qualcuno" lo dovrà avviare.

Prima di procedere, qui troviamo una delle prime differenze tra il primo kernel e i kernel recenti. Se nella prima versione del kernel Linus mise insieme la parte di bootloader con la parte di "setup" (ovviamente era una distinzione inesistente ai tempi) che si occupa di passare in modalità protetta, di chiamare la init (main.c) ecc..., ai giorni nostri chi si occupa di caricare in memoria la parte di setup e l'immagine del sistema è proprio il bootloader, come detto, quindi come vedremo in seguito il bootloader in avvio caricherà il setup (setup.S) e l'immagine del sistema .

       mov     ax,#SYSSEG
       mov     es,ax           | segment of 0x010000
       call    read_it
       call    kill_motor

read_it è la parte di codice che attiva il floppy e carica in memoria (in SYSSEG) i settori restanti sul disco che contengono il sistema (più avanti capiremo come la memoria viene mappata).

read_it: ...
       | ENDSEG  = SYSSEG + SYSSIZE
       cmp ax,#ENDSEG          | have we loaded all yet?
       jb ok1_read
       ret

ENDSEG non appare da nessuna parte nel codice di boot.s. La dimensione del sistema viene calcolata nel Makefile ad anteriori:

boot/boot:      boot/boot.s tools/system
       (echo -n "SYSSIZE = (";ls -l tools/system | grep system \
               | cut -c25-31 | tr '\012' ' '; echo "+ 15 ) / 16") > tmp.s
       cat boot/boot.s >> tmp.s
       $(AS86) -o boot/boot.o tmp.s
       rm -f tmp.s
       $(LD86) -s -o boot/boot boot/boot.o

Una volta caricato il codice in memoria se non ci sono problemi kill_motor viene chiamata, il nome è autoesplicativo.

Il sistema viene poi spostato nell'area di memoria 0x1000 (cosa che, come detto, oggi viene fatta da Setup.S arch/386/boot/compressed/{head.S,misc.c} che chiama le routine di decompressione del kernel) La dimensione massima che l'immagine del kernel può avere (zImage), può essere al massimo di 512K (0x80000) [6] .

| first we move the system to it's rightful place
       mov     ax,#0x0000
       cld                     | 'direction'=0, movs moves forward
do_move:
       mov     es,ax           | destination segment
       add     ax,#0x1000
       cmp     ax,#0x9000
       jz      end_move
       mov     ds,ax           | source segment
       sub     di,di
       sub     si,si
       mov     cx,#0x8000
       rep
       movsw
       j       do_move

dopo aver impostato i segmenti di memoria con la gdt si passa in modalità protetta :

| Well, now's the time to actually move into protected mode. To make
| things as simple as possible, we do no register set-up or anything,
| we let the gnu-compiled 32-bit programs do that. We just jump to
| absolute address 0x00000, in 32-bit protected mode.
       mov     ax,#0x0001      | protected mode (PE) bit
       lmsw    ax              | This is it!
       jmpi    0,8             | jmp offset 0 of segment 8 (cs)

a questo punto il sistema è pronto e il codice passa l'esecuzione a head.s che contiene il codice di startup 32 all'indirizzo assoluto 0x00000000.

D'ora in avanti possiamo abbandonare la parte storica di Linux. I kernel recenti (dalla versione 2.6) non usano più il bootsector di Linux ma si affidano ai piu' potenti GRUB o LILO, per cui la parte che segue tratta il codice che si può trovare in un kernel 2.6 (il kernel 2.4 supporta ancora il boot da floppy ed infatti si può trovare il suo bootloader qui arch/i386/boot/bootsect.S). Lascio ai curiosi la lettura del codice che è leggermente piu' complessa di quella del primo Linux .

kernel decompression e kernel space

Il lavoro di bootloader viene oggi lasciato ad altri bootloader come LILO o GNU GRUB che sono più versatili e che caricano in memoria setup.S (è qui che troviamo tante cose interessanti, fate riferimento a [6] THE REAL-MODE KERNEL HEADER) e bvmlinux, tutto procede poi come descritto di seguito.

Supponendo di aver costruito una immagine di bzImage (__BIG_KERNEL__ Nota: bImage è stata abbandonata a partire dal protocollo di boot 2.02 [6]) ecco cosa troviamo nel Makefile in linux/arch/i386/boot/Makefile :

bzImage         $(OBJCOPY) compressed/bvmlinux compressed/bvmlinux.out
                tools/build -b bbootsect bsetup compressed/bvmlinux.out $(ROOT_DEV) \
                        > bzImage

A questo punto setup.S esegue:

1. alcune inizializzazioni hardware (calcola la memoria estesa presente nel sistema, tastiera, MCA, hard disk e mouse)

2. verifica se il BIOS supporta l'APM

3. inizializza l'A20

4. Prepara per la modalita' protetta code32_start e' inizializzato a 0x1000 per zImage, o 0x100000 per bzImage. Il valore di code32 verra' utilizzato passando il controllo a linux/arch/i386/boot/compressed/head.S .

5. Cambia in modalita' protetta.

A questo punto il controllo passa alla routine che "esploderà" l'immagine del kernel in memoria al primo mega (0x100000) e a questo punto head.S salterà al vero inizio del kernel (arch/i386/kernel/head.S che e' l'inizio del kernel decompresso).

Riporto i campi dell'header di setup.S per mostrare come il bootloader e il setup interagiscano per passarsi le informazioni della command line o dell'indirizzo dove si trova l'initrd . setup si aspetta che alcuni valori siano valorizzati dal boot loader alla sua partenza :

Offset  Proto   Name            Meaning
/Size
0200/2  2.00+   jump            Jump instruction
0202/4  2.00+   header          Magic signature "HdrS"
0206/2  2.00+   version         Boot protocol version supported
0208/4  2.00+   realmode_swtch  Boot loader hook
020C/2  2.00+   start_sys       The load-low segment (0x1000) (obsolete)
020E/2  2.00+   kernel_version  Pointer to kernel version string
0210/1  2.00+   type_of_loader  Boot loader identifier
0211/1  2.00+   loadflags       Boot protocol option flags
0212/2  2.00+   setup_move_size Move to high memory size (used with hooks)
0214/4  2.00+   code32_start    Boot loader hook
0218/4  2.00+   ramdisk_image   initrd load address (set by boot loader)
021C/4  2.00+   ramdisk_size    initrd size (set by boot loader)
0220/4  2.00+   bootsect_kludge DO NOT USE - for bootsect.S use only
0224/2  2.01+   heap_end_ptr    Free memory after setup end
0226/2  N/A     pad1            Unused
0228/4  2.02+   cmd_line_ptr    32-bit pointer to the kernel command line
022C/4  2.03+   initrd_addr_max Highest legal initrd address

kernel/head.S fa alcune inizializzazioni (tra cui copia anche i parametri della command line) e la prima CPU chiama start_kernel() che si trova in init/main.c (che è scritta in C) .

init/main.c ( __init start_kernel(void) ) chiama le seguenti routine per fare alcune operazioni (riporto le principali chiamate seguendone l'ordine):

1. Si assicura che solo una CPU abbia il controllo.

2. Esegue alcune operazioni hardware.

3. Stampa a video il banner di Linux.

4. Inizializza i trap.

5. Inizializza gli IRQ.

6. Inizializza lo scheduler.

7. Inizializza i timers.

8. Inizializza i softirq.

9. Fa il parsing della linea di comando (che ricordo è stata copiata fino a qui).

10. Inizializza la console.

11. Inizializza il sottosistema di caricamento dei moduli.

12. Abilita gli interrupt.

13. Chiama mem_init() che calcola alcuni valori della memoria e stampa a video il messaggio "Memory: ..."

14. kmem_cache_init(), inizializza la cache del kernel.

15. Chiama il fork_init

16. Prepara alcuni buffer per il VFS ecc... .

17. Se supportato inizializza il procfs.

18. Fa il fork di init e inizializza il massimo numero di threads in base alla memoria disponibile.

19. Controlla se ci sono delle patch specifiche per il processore, bus ecc... su cui il kernel è in esecuzione.

20. crea un kernel thread init () che esegue un programma passato via init= boot parametre o prova ad eseguire uno dei seguenti programmi /sbin/init, /etc/init, /bin/init, /bin/sh.

User space

Arrivati a questo punto dopo la creazione del thread di init il kernel si mette nel loop di idle con il pid=0. Quello che succede dopo dipende dal nostro programma di init.

Il nostro sistema è ora pronto ad eseguire i nostri programmi .

Conclusioni

Molte cose sono cambiate dal lontano 1991, anche se sostanzialmente la struttura è rimasta pressoché invariata, nel boot di Linux ma è divertente vedere come i commenti originali di Linus Torvalds sono rimasti invariati e al più dei nuovi se ne sono aggiunti man mano che lo sviluppo del codice aggiungeva nuovi componenti . Questo articolo è stato solo una piccola introduzione anche se ha sfiorato alcuni concetti abbastanza a fondo e spero di aver reso un'idea chiara delle fasi boot. Nella realtà uno studio completo del boot di Linux richiede molto tempo se si prende in considerazione un kernel odierno. Per questo motivo ho iniziato presentando il primo codice scritto dal Torvalds per evitare di dilagare troppo, cosa che non ho potuto fare quando sono passato al codice recente. Chi è curioso e vuole approfondire lo può fare iniziando a seguire alcuni link qui sotto e a leggere dall'inizio il codice naturalmente.

Fonti

[1] Booting From Wikipedia, the free encyclopedia

http://en.wikipedia.org/wiki/Boot_loader

[2] Campaign for Free BIOS

http://www.fsf.org/campaigns/free-bios.html

[3] Tigran Aivazian - Linux Kernel Internals 2.4

è facilmente trovabile in Internet una versione liberamente scaricabile.

[4] Feiyun Wang - Linux-i386-Boot-Code-HOWTO

http://tldp.org/HOWTO/Linux-i386-Boot-Code-HOWTO/setup.html

[5] H. Peter Anvin - THE LINUX/I386 BOOT PROTOCOL

file:///usr/src/linux/Documentation/i386/boot.txt

[6] Alessandro Rubini - Booting the kernel

http://www.linux.it/~rubini/docs/boot/boot.html