Linux Native Boot Process

Transcript

Linux Native Boot Process
Linux Native Boot Process
by Salvatore D'Angelo
Table of contents
1
Introduzione............................................................................................................................................................................. 2
2
Alcuni concetti di base: modalità reale e protetta.................................................................................................................... 2
3
Software di test.........................................................................................................................................................................6
4
Boot process step 0: stampa di un messaggio di boot..............................................................................................................6
5
Boot Process step 1: copia del boot sector in 0x9000:0...........................................................................................................7
6
Boot Process step 2: definizione dello spazio per lo stack...................................................................................................... 8
7
Boot Process step 3: stop del motore del floppy......................................................................................................................9
8
Boot Process step 4: caricamento del codice di setup............................................................................................................10
9
Boot Process step 5: routine di debug e stampa del messaggio Loading system...................................................................12
10
Boot Process step 6: jump al codice di setup....................................................................................................................... 13
11
Boot Process step 7: caricamento del kernel in memoria.....................................................................................................14
12
Boot Process step 8: spostiamo il kernel all'indirizzo 0x0100:0..........................................................................................16
13
Boot Process step 9: stampiamo un semplice messaggio di setup....................................................................................... 16
14
Boot Process step 10: setting up global e interrupt descriptor tables...................................................................................17
15
Boot Process step 11: abilita linea A20................................................................................................................................18
16
Boot Process step 12: reset coprocessore............................................................................................................................. 19
17
Boot Process step 13: PIC programming............................................................................................................................. 19
18
Boot Process step 14: switch alla modalità protetta............................................................................................................. 22
19
Boot Process step 15: jump al codice kernel........................................................................................................................22
20
Boot process step 16: build immagine del kernel.................................................................................................................24
21
Conclusioni...........................................................................................................................................................................24
22
Download source code......................................................................................................................................................... 25
23
Bibliografia...........................................................................................................................................................................25
Open Community (C) 2004
Copyright © 2004 The Open Community Group. All rights reserved.
Linux Native Boot Process
1. Introduzione
Questo articolo introduce il processo di boot adottato da Linux su architetture Intel. Esso guiderà il lettore, step by step, nella
creazione di un floppy di boot capace di avviare una immagine di kernel che, nel nostro caso, sarà una semplice funzione C che
stampa il messaggio "Hello Kernel". Gli esempi riportati in questo articolo sono prevalentemente scritte in linguaggio
assembly e C, per cui è prerequisita la loro conoscenza. Poichè andremo a lavorare direttamente con l'hardware del PC,
verranno illustrati anche i concetti base della gestione della memoria nei processori i386 (e superiori), il funzionamento dei
chips PIC 8259 e del keyboard controller 8042.
Poichè l'obiettivo di questo articolo è prevalentemente didattico, il processo di boot sarà spiegato in una forma semplificata
rispetto alle versioni di Linux 2.4, diciamo che come riferimento sono state prese le prime versioni di Linux molto più semplici
da comprendere. Il lettore una volta compresi i concetti base potrà provare ad estendere le funzionalità del suo boot loader.
2. Alcuni concetti di base: modalità reale e protetta
Un microprocessore a 32 bit della famiglia i386 (o superiori) ha due modalità di funzionamento: reale e protetta. La prima fu
introdotta per mantenere la compatibilità con le applicazioni che giravano sui vecchi processori a 16 bit i86/i286. Nella
seconda modalità, invece, il microprocessore lavora pienamente a 32 bit e supporta un set di istruzioni molto più ampio.
In modalità reale sono disponibili 20 linee di indirizzamento che consentono di indirizzare fino a 1 Mb. Ogni singolo byte della
memoria viene indirizzato attraverso una coppia di puntatori SEGMENTO:OFFSET ciascuno a 16 bit. Questa coppia di
puntatori vengono combinati dal processore in un particolare registro interno di 20 bit, ottenendo di conseguenza l'indirizzo
lineare del byte da indirizzare in memoria.
Per poter indirizzare la memoria sono disponibili alcuni registri elencati qui di seguito.
CS: punta al segmento codice da eseguire. Questo registro in combinazione con il registro IP punta alla prossima istruzione da
eseguire. Entrambi i registri sono modificabili solo attraverso istruzioni di jump e di chiamate a procedure.
DS: punta al segmento dati. Spesso durante operazioni di copia di blocchi di dati, questo registro viene utilizzato in coppia con
il registro SI (source index) per indicare la cella di partenza dei dati sorgente.
ES: extra segment. Registro di segmento ausiliario, spesso utilizzato in coppia con il registro DI (destination index) durante la
copia di blocchi di memoria per denotare la cella di partenza dei dati destinazione.
SS: punta allo stack segment. Lo stack viene utilizzato per il salvataggio dei registri. I moderni compilatori utilizzano lo stack
per salvare i parametri passati a una procedura. In genere questo registro viene utilizzato in coppia con il registro SP per
puntare al top dello stack.
Oltre a questi registri il processore i386 utilizza i seguenti registri general purpose: AX, BX, CX e DX. Per ognuno di questi
registri a 16 bit è possibile accedere agli 8 bit più significativi o meno significativi attraverso i registri a 8 bit: AH-AL, BH-BL,
CH-CL e DH-DL.
Ad esempio se AH=0x10 e AL=0x10 allora AX=0x1010.
Ipotizziamo di avere la coppia DS:SI con i seguenti valori, DS=8000h e SI=8F00h, allora per ottenere l'indirizzo lineare a cui
la coppia di registri punta, basta operare in questo modo.
Si moltiplica DS per 10h e si somma poi l'offset: 80000h+8F00h = 88F00h.
Spiegata questa semplice modalità di indirizzamento, andiamo ad osservare come si presenta il primo Mb di memoria dopo
l'accensione.
La coppia di registri CS:IP punta all'indirizzo F000h:FFF0h, che è anche l'entry point del BIOS. Quest'indirizzo di
conseguenza contiene un'istruzione di JUMP al codice effettivo del BIOS.
Open Community (C) 2004
Page 2
Linux Native Boot Process
Il BIOS (Basic Input Output System) non è altro che un insieme di routine software che fornisce il supporto per gestire le
periferiche del computer. Oltre a fornire questa interfaccia, il compito del BIOS è quello di verificare il corretto funzionamento
dei dispositivi hardware essenziali in un computer, e di segnalare eventuali errori all'utente. Una volta che il diagnostico di
sistema termina, viene invocato l'interrupt software 19h, meglio conosciuto come interrupt di boot-strap.
Compito di questo interrupt è quello di caricare dai dispositivi di memoria di massa il codice di boot e stampare un messaggio
di errore se questo non viene trovato.
In modalità protetta il processore i386 (o superiori) hanno piene funzionalità a 32 bit, tuttavia il modo in cui vengono gestite le
cose cambia completamente. Per comprendere le potenzialità di un processore i386 in modalità protetta, si consideri la
seguente frase estratta da [1].
"L'80386 è un potente microprocessore a 32 bit ottimizzato per i sistemi operativi multitasking e progettato per applicazioni
che necessitano di prestazioni molto elevate. Il processore può indirizzare fino a 4 Gb di memoria fisica e 64 Tb (246 bytes) di
memoria virtuale. I mezzi di gestione della memoria onchip comprendono i registri per la conversione dell'indirizzo, un
avanzato hardware multitasking, un meccanismo di protezione e il sistema di paginazione della memoria virtuale. Speciali
registri di debugging forniscono breakpoint di dati e di codice perfino nel software basato su ROM".
Queste poche righe dovrebbero dare un idea delle novità introdotte da questi tipi di microprocessori.
Il nuovo processore è dotato di un set di registri general purpose chiamati EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDI che
sono la versione a 32 dei noti registri a 16 bit AX, BX, CX, DX, BP, SP, SI, DI. Infatti è possibile accedere ai primi 16 bit del
registro semplicemente usando il vecchio nome e così via anche per le due diverse parti a 8 bit.
Tra i registri messi a disposizione dal microprocessore, ci sono quelli di segmento che sono sempre CS, DS, ES, SS a cui si
aggiungono FS e GS. In modalità protetta questi registri conterranno il selettore di un determinato indirizzo logico. Più avanti
introdurremo il concetto di indirizzo logico e di selettore. Per ora ci basta tener presente che SS e ESP conterranno il valore
dello stack pointer, mentre CS e EIP conterranno il valore del program counter.
Ci sono poi alcuni registri flags e altri utilizzati nella gestione della memoria tra cui figurano:
• GDTR, Global Descriptor Table Register;
• LDTR, Local Descriptor Table Register;
• IDTR, Interrupt Descriptor Table Register;
• TR, Task Register.
Esistono poi dei registri di controllo come CR0, CR2 e CR3. CR0 contiene i flags di controllo del sistema, che controllano o
indicano le condizioni applicabili al sistema nel suo complesso e non ad un singolo task. Il registro CR2 viene impiegato per
gestire gli errori di pagina qualora vengano utilizzate tabelle di pagine per convertire gli indirizzi. Il registro CR3 consente al
processore di individuare l'elenco di tabelle di pagine per il task corrente.
Infine ci sono registri di test e debugging che però non prenderemo in considerazione in questa sede.
Ora concentriamo la nostra attenzione su come venga gestita la memoria in modalità protetta.
In questa modalità di funzionamento un indirizzo è costituito da una coppia (selettore, offset) detto indirizzo logico. La Fig. 1
riporta il formato di un selettore. E' possibile notare un indice di 13 bit che identifica una entry in una GDT o LDT. C'è poi un
Table Indicator (TI) che indica se l'indice fa riferimento a una GDT (TI=0) o LDT (TI=1), (quest'ultima non è presa in
considerazione in questo articolo). Infine, ci sono due bits che rappresentano il Requestor Privilege Level (RPL) che in Linux
può essere 0 (kernel mode) o 3 (user mode).
Page 3
Open Community (C) 2004
Linux Native Boot Process
Selector
Il registro GDTR contiene il base address della GDT, per cui sommando il suo contenuto con quello del selettore moltiplicato
per 8 (visto che ogni entry occupa 8 bytes) si ottiene una entry nella GDT che contiene un descrittore di segmento il cui
formato è riportato in Fig. 3. Questo descrittore contiene il base address di un segmento nell'area di indirizzamento lineare che,
sommato all'offset dell'indirizzo logico ci restituisce un indirizzo lineare, come mostra la Fig. 2.
Global Descriptor Table
Osservando la Fig. 3 si può constatare che il descrittore è composto da un certo numero di campi di cui diamo una descrizione
solo per quelli che ci interessano ai fini del nostro articolo.
- LIMIT I due campi Limit 0-15 e Limit 16-19 costituiscono il limite del segmento. Il limite indica la dimensione del
segmento. Concatenando i due campi contenuti nel descrittore otteniamo un indirizzo a 20 bit. Questo indirizzo può assumere
un valore da 0 a 2^20-1.
- GRANULARITY Nel descrittore questo bit è indicato con la lettera G ed indica in che modo deve essere interpretato il
campo LIMIT. Qualora questo bit non sia settato, il contenuto di LIMIT viene interpretato come unità in byte, quindi si ha un
limite massimo di 1Mb, mentre se il bit G è settato il limite sarà considerato come il numero di unità da 4 Kb, ed in tal caso
avremo un max di 4 Gb (come avviene nel nostro caso).
- BASE Questo valore identifica la locazione del segmento entro lo spazio d'indirizzi lineare da 4 Gb. In parole semplici,
identifica l'indirizzo assoluto in memoria dove si trova il primo byte del segmento.
- DPL Questi 2 bit indicano il livello di privilegio del descrittore e di conseguenza i relativi permessi concessi al codice o ai
dati in esso contenuti. Nel nostro caso utilizzeremo solo la modalità 0 (kernel mode) e 3 (user mode).
Open Community (C) 2004
Page 4
Linux Native Boot Process
GDT Descriptor
La prima entry della GDT viene generalmente settata con 8 bytes nulli (cioè uguali a 0) al fine di una corretta gestione dei page
fault. La seconda e terza entry, in genere, sono riservati al code e data segment del kernel.
Come ultimo aspetto riguardo la modalità protetta, vediamo invece come viene gestita la tabella degli interrupt.
Il registro IDTR contiene l'indirizzo della tabella dei vettori di interrupt. In modalità reale questa tabella è costituita da vettori
di 4 bytes per un numero massimo di 256 elementi, collocata all'indirizzo di memoria 0x0. In modalità protetta, ogni task può
avere una sua tabella di interrupt. Gli elementi possono essere fino ad un massimo di 256 elementi, ed ognuno di essi è
rappresentato da una struttura di 64 bit come riportato in Fig. 4.
IDT Descriptor
Analizziamo ora in dettaglio i campi di un Interrupt Descriptor.
- SELECTOR: insieme ad OFFSET fornisce l'indirizzo logico dell'handler di interrupt.
- OFFSET: insieme ad SELECTOR fornisce l'indirizzo logico dell'handler di interrupt.
- Bit P: indica la presenza o meno dell'interrupt.
- DPL: indica il livello di privilegio dell'interrupt.
- Bit T: indica se si tratta di una trap (T=1) oppure di un interrupt (T=0).
Quando si verifica un errore, la CPU in modo protetto lo comunica al sistema operativo generando una eccezione. Le eccezioni
sono delle interruzioni che mandano in esecuzione i relativi interrupt.
Pertanto, i primi 17 interrupt sono riservati al codice di sistema operativo.
IDT[00] Errore di divisione
IDT[01] Eccezione di debugging
IDT[02] Non usato
IDT[03] Breakpoint/Debugging
IDT[04] Overflow
IDT[05] Check limits
IDT[06] Codice operativo non valido (istruzione sconosciuta)
IDT[07] Coprocessore non disponibile
IDT[08] Doppio difetto
IDT[09] Superamento del segmento di coprocessore
IDT[0A] TSS non valido
IDT[0B] Segmento non presente
IDT[0C] Eccezione di stack
IDT[0D] Protezione generale
Page 5
Open Community (C) 2004
Linux Native Boot Process
IDT[0E] Difetto di pagina
IDT[0F] Non usato
IDT[10] Errore di processore
I rimanenti interrupt possono essere definiti dal sistemista o dall'applicativo.
3. Software di test
Gli esempi riportati in questo articolo sono stati testati su una distribuzione Linux RedHat 8.0 dove è disponibile l'utility make,
il compilatore gcc, l'assemblatore as ed il linker ld.
4. Boot process step 0: stampa di un messaggio di boot
Questa sezione illustrerà come creare un floppy di boot che permette la stampa del messaggio "Hello World" all'avvio del
computer. Quando un computer parte e da BIOS viene configurato il floppy come drive di boot, esso cerca di caricare il primo
settore nella locazione 0:0x07C0. E' importante che questo settore al primo byte contenga già una istruzione eseguibile e che
l'ultima word sia uguale a 0xAA55 (identificativo di un settore di boot).
Quindi per stampare il messaggio di "Hello World" al boot basta utilizzare il servizio 0x0E dell'interrupt 0x10 che consente la
stampa di un carattere (caricato nel registro AL) a video .
In [4] è possibile trovare la specifica di tutte le routines di interrupt del BIOS più comuni.
.code16
.text
.global _start
_start:
movb $0x0E, %ah
movb $'H', %al
int 0x10
movb $'e', %al
int 0x10
......
done:
jmp done
// stampa il carattere 'H'
// stampa il carattere 'e'
// stampa altri caratteri
// loop infinito
.org 510
boot_flag: .word 0xAA55
Il programma termina con un loop infinito, questo serve per evitare che la CPU esegua codice invalido. Si noti come i bytes
finali 510 e 511 siano, rispettivamente, uguali a 0xAA e 0x55.
Una volta scritto il programma in un file che chiameremo bootsect.S è possibile compilarlo attraverso i seguenti comandi.
as -o bootsect.o bootsect.S
ld -Ttext 0x0 -s oformat binary -o bootsect bootsect.o
Il primo comando crea un object file partendo dal codice in assembler. Il secondo comando crea l'immagine di boot chiamata
bootsect. L'opzione -Ttext 0x0 indica che 0x0 è l'indirizzo di partenza per i segmenti code, data e bss. L'opzione -oformat
specifica il formato di output prodotto da ld.
A questo punto per creare il dischetto di boot basta eseguire il seguente comando.
dd if=bootsect of=/dev/fd0 bs=512
Spegniamo ora il computer e inseriamo il dischetto nel floppy driver (assicuriamoci che il BIOS sia configurato in modo tale
da consentire il boot da dischetto). Durante lo startup della macchina ecco che per magia compare il messaggio "Hello World".
Open Community (C) 2004
Page 6
Linux Native Boot Process
A questo punto per velocizzare il processo di sviluppo abbiamo bisogno di due cose, un sistema più rapido del reboot per
testare il nostro codice e un Makefile.
Il primo problema si risolve semplicemente scaricando da sourceforge (http://sourceforge.net/projects/bochs) il tool bochs che
è un emulatore Intel open source.
Vediamo, invece, come creare il nostro Makefile.
Chi lavora in ambiente Unix sa che l'utility make viene utilizzata per la build automatizzata di progetti sotware. In pratica se la
build di un programma richiede l'inserimento di molti comandi questa utility consente allo sviluppatore di definire semplici
comandi con cui gestire lo sviluppo del proprio progetto. Il nostro Makefile dovrà fornire tre semplici comandi.
make -> per una build completa del progetto
make disk -> per creare l'immagine di boot
make clean -> rimozione di tutti i file generati in fase di build
Qui di seguito è riportato il Makefile utilizzato nel nostro progetto.
AS=as
LD=ls
all: bootsect image
bootsect: bootsect.o
$(LD) -Ttext 0x0 -s oformat binary -o $@ $<
bootsect.o: bootsect.S
$(AS) -o $@ $<
disk: image
dd if=image of=/dev/fd0 bs=512
image: bootsect
cat bootsect > image
clean:
rm bootsect
rm image
rm *.o
Si provi ora a buildare il nostro software con i comandi di build nel seguente ordine:
make clean
make
make disk
5. Boot Process step 1: copia del boot sector in 0x9000:0
Il primo passo del processo di boot è quello di spostare il boot sector dalla locazione 0x07C0:0 a 0x9000:0. Questo per evitare
che, quando si caricherà il kernel nello step 7, questi vada a sovrascrivere il codice correntemente in esecuzione.
I registri DS:SI punteranno alla cella iniziale dei dati sorgenti, cioè all'indirizzo 0:0x07C0. I registri ES:DI punteranno, invece,
all'indirizzo iniziale del blocco destinazione che, nel nostro caso, sarà 0x9000:0. Per fare la copia si utilizzeranno le istruzioni
assembler:
CLD
REP
MOVSW
con CLD si stabilisce che ad ogni word copiata il registro DI venga incrementato. REP indica che l'istruzione successiva deve
Page 7
Open Community (C) 2004
Linux Native Boot Process
essere ripetuta per un numero di volte pari al contenuto del registro CX (che nel nostro caso conterrà 256 word= 512 bytes).
MOVSW muove una word da DS:SI a ES:DI.
Dopo questa operazione si fa un jump alla locazione 0x9000:go che conterrà l'istruzione successiva da eseguire nel boot sector.
Qui di seguito riportiamo il codice che effettua la copia sopra citata. Per testare che la copia sia avvenuta con successo e che il
jump non abbia generato problemi, stampiamo in questa nuova regione il messaggio "Hello World".
BOOTSEG=0x07C0
INITSEG=0x9000
.code16
.text
.global _start:
_start:
movw $BOOTSEG, %ax
movw %ax, %ds
movw $INITSEG, %ax
movw %ax, %es
movw $256, %cx
subw %si, %si
subw %di, %di
cld
rep
movsw
ljmp $INITSEG, $go
# DS = 0x07C0
#
#
#
#
ES
CX
SI
DI
=
=
=
=
0x9000
256
0
0
# copia boot sector
# jump a 0x9000:go
go:
<stampa Hello World come l'esempio precedente>
.org 510
boot_flag: .word 0xAA55
Compiliamo il nostro programma e creiamo il disco di boot con i comandi make illustrati nello step precedente.
6. Boot Process step 2: definizione dello spazio per lo stack
Negli steps che seguiranno, si effettueranno chiamate a procedure, per cui è importante definire uno spazio per lo stack
settando opportunamente i registri SS e SP.
Abbiamo visto che il boot sector occupa uno spazio compreso tra 0x9000:0 e 0x9000:0x01FF. A partire dall'indirizzo
0x9000:0x0200 nei prossimi steps caricheremo 4 settori (2048 bytes) che conterrano del codice di setup. Per cui la regione di
memoria compresa tra 0x9000:0x0200 e 0x9000:0x09FF sarà riservato al codice di setup. Dopo questa regione definiamo la
regione dello stack che si estenderà fino a 0x9000:(0x4000-12).
Il motivo per cui i primi 12 bytes non fanno parte dello stack è che questi nel vero codice di boot di Linux servono a contenere
la Disk Parameter Table utilizzata per ottimizzare la lettura dei settori.
L'unica cosa che per ora ci interessa sapere è che la regione stack parte dall'indirizzo 0x9000:(0x4000-12) e si estende verso
l'alto fino all'indirizzo 0x9000:0x0A00 (vedi Fig. 5).
Open Community (C) 2004
Page 8
Linux Native Boot Process
Memory Layout
Qui di seguito riportiamo il codice per il setting dell'area stack.
....
go:
movw
movw
movw
movw
$0x4000-12, %di
%ax, %ds
%ax, %ss
%di, %sp
// SS = 0x9000
// SP = 0x4000-12
<stampa Hello World come l'esempio precedente>
.org 510
boot_flag: .word 0xAA55
7. Boot Process step 3: stop del motore del floppy
In questo step introduciamo una procedura che ci consente di effettuare lo stop del motore del floppy dopo la lettura dei dati da
disco. Questo ci consentirà di attivare il kernel in una situazione consistente dal punto di vista del floppy. In questa sezione non
entreremo in dettaglio circa l'hardware del floppy, bensì vedremo solo le cose necessrie per i nostri scopi.
Di solito i PC utilizzano il controller disco NEC mPD765. I PC AT possono includere anche il controller 82072A, mentre i
PS/2 usano un Intel 82077A. Questo controller gestisce un certo numero di registri tra cui il Digital Output Register (DOR) che
è un registro a 8 bits a sola scrittura, disponibile all'indirizzo 0x3F2, che si occupa della gestione dei motori dei vari floppy
drives disponibili.
Page 9
Open Community (C) 2004
Linux Native Boot Process
FDC Controller Register
In Fig. 6 è possibile osservare il formato di questo registro.
I bit DR1, DR0 selezionano il floppy driver a cui inviare il comando di stop del motore. La selezione del drive ha senso solo se
il motore è attivo. Il bit REST quando è posto a 1 attiva il controller, mentre se vale 0 esegue il reset del controller. Quando si
effettua lo start del motore di un floppy drive si può decidere di associare ad esso una linea di DMA con relativo canale IRQ.
MOTA, MOTB, MOTC, MOTD controllano lo start/stop per i floppy drive A, B, C e D. Se il bit MOTx è 1, allora il motore
del floppy drive x viene avviato, altrimenti viene spento.
Visto che il nostro obiettivo è quello di spegnere il motore di tutti i floppy drive disponibili, allora bisogna scrivere il valore 0
nel registro DOR.
out[0x3F2] = 0
Qui di seguito riportiamo il codice della routine chiamata kill_motor.
kill_motor:
movw
xorb
outb
.word
ret
$0x3f2, %dx
%al, %al
%al, %dx
0x00eb, 0x00eb
# out[0x3f2] = 0
# breve delay
questa routine verrà invocata nel nostro main program subito dopo la stampa del messaggio di "Hello World".
8. Boot Process step 4: caricamento del codice di setup
Il codice di setup segue sul floppy di boot il bootsector, ed esso occupa 4 settori (2048 bytes) di disco. Quindi i settori 2-5 della
traccia 0 contengono tale codice. Quest'ultimo deve essere caricato in memoria nel segmento 0x9000 subito dopo il boot
sector, per cui a partire dall'offset 0x0200 (512 appunto). Per effettuare questa copia utilizzeremo il servizio 0x02 dell'interrupt
0x13 che consente di copiare n settori dalla locazione di disco (drive, head, track, sector) in memoria a partire dall'indirizzo
puntato dai registri ES:BX. Una volta eseguita questa copia, per eseguirlo verrà effettuato un semplice jump alla locazione
iniziale del codice di setup (0x9000:0x0200).
Prima di effettuare la copia è necessario eseguire un reset del controller del floppy attraverso il servizio 0x00 dell'interrupt
0x13, come mostra il codice seguente.
load_setup:
xorb %ah, %ah
xorb %dl, %dl
int $0x13
# AH = 0 -> service 0x00
# DL = 0 -> drive 0
# reset FDC
Dal codice si intuisce che il valore del servizio deve essere inserito nel registro AH, mentre il floppy drive da resettare va posto
nel registro DL.
Dopo questa semplice operazione avviene la vera e propria copia.
Open Community (C) 2004
Page 10
Linux Native Boot Process
Il servizio 0x02 dell'interrupt 0x13 consente di copiare n settori di disco in memoria. Le specifiche di questo servizio sono le
seguenti.
DH = drive da cui vengono letti i dati (0 nel nostro caso)
DL = testina da cui parte la copia (0 nel nostro caso)
CH =traccia da cui parte la copia (0 nel nostro caso)
CL = settore da cui parte la copia (2 nel nostro caso)
ES:BX = indirizzo di memoria destinazione
AH = numero servizio (0x02)
AL = settori da copiare (4 nel nostro caso)
La routine di interrupt ritorna un codice nel registro AX che rappresenta il risultato della copia. Questo valore è 0 se la copia è
avvenuta con successo, altrimenti conterrà un opportuno codice di errore.
Per semplicità in questo step faremo stampare a video la stringa "Error" se avviene un errore durante la copia, nel prossimo
step vedremo come effettuare il dump del codice di errore.
Memory Layout
Qui di seguito riportiamo il codice assembler utilizzato per fare il loading del codice di setup.
Page 11
movb
movw
movb
movb
int
$0x02, %cl
$0x0200, %bx
$0x02, %ah
$4, %al
$0x13
Open Community (C) 2004
Linux Native Boot Process
jnc ok_load_setup
stampa il messaggio Error
jmp
load_setup
# riprova di nuovo
ok_load_setup:
stampa il messaggio Setup loaded
call kill_motor
done:
jmp done
La Fig. 7 mostra come appare la memoria dopo quest'ulima operazione di copia.
9. Boot Process step 5: routine di debug e stampa del messaggio Loading system
Dopo aver caricato il codice di setup, stampiamo il messaggio Loading system utilizzando il servizio 0x13 dell'interrupt 0x10.
Questo perchè quando verrà passato il controllo al codice di setup, questo inizializzerà alcune componenti di sistema e poi
provvederà a caricare il kernel in memoria. Ricordiamo al lettore che nel nostro caso il kernel è una semplice routine C che
stampa il messaggio "Hello Kernel".
Il servizio 0x13 consente di stampare a video una intera stringa. Le specifiche di questo servizio sono le seguenti.
CX= numero caratteri della stringa (compreso lo 0 finale)
BH=pagina video (0 nel nostro caso)
BL=attributi di stampa (7 nel nostro caso)
ES:BP=indirizzo stringa
DH=riga dove scrivere la stringa
DL=colonna dove scrivere la stringa
AH= numero servizio (0x03)
Per avere in DH DL la posizione corrente del cursore, utilizzeremo il servizio 0x03 dell'interrupt 0x10.
Qui riportiamo il nuovo codice da aggiungere al file bootsect.S subito dopo il codice di caricamento dei settori di setup.
Ovviamente è ora possibile rimuovere il codice che stampava il messaggio Setup loaded!!.
movb
$0x03, %ah
xorb
%bh, %bh
int $0x10
movw
movw
movw
movw
int
....
# ottieni la posizione del cursore in DH e DL
# BH = 0
$17, %cx
$0x0007, %bx
$msg1, %bp
$0x1301, %ax
$0x10
msg1:
.byte 13 10
.ascii Loading system
# new line
Aggiungiamo a questo punto al nostro codice due routine che potrebbero tornare utile in fase di debugging: print_all e
print_hex. La prima stampa le prime 5 word al top dello stack che dovrebbero contenere: codice errore, AX, BX, CX e DX;
Questa routine è utile per verificare il valore dei registri in un dato punto del codice o al termine di una routine di interrupt. La
seconda routine stampa la word puntata da SS:BP in formato esadecimale. Evitiamo di riportare in tale sede il codice perchè
l'introduzione di queste routine è facoltativa e necessita solo di conoscenze assembler. Il lettore potrà trovare il codice + nei
files sorgenti relativi a questo step.
Ora che abbiamo a disposizione queste routine di debugging anzichè stampare il messaggio Error come facevamo nello step 4,
se il caricamento del codice di setup falliva, stampiamo il codice di errore in AX.
Open Community (C) 2004
Page 12
Linux Native Boot Process
pushw
call
movw
call
popw
%ax
print_nl
%sp, %bp
print_hex
%ax
# stampa il codice di errore sul video
# stampa unanew line sul video
jmp
load_setup
# riprova di nuovo
# stampa AX sul video
10. Boot Process step 6: jump al codice di setup
In questo step introduciamo un nuovo file che chiameremo setup.S, esso conterrà il codice di setup del boot loader. Questo
codice, come già anticipato sopra, dovrà occupare 4 settori (2048 bytes). Per ora nel file setup.S effettuiamo la stampa di un
semplice messaggio "Wow I am in setup", mentre in bootsect.S dobbiamo effettuare un jump al codice di setup.
Questo è il codice di setup.S.
.code16
.text
.global _start
_start:
stampa il messaggio Wow I am in setup
done:
jmp done
# loop infinito
.org 2048
# size 4 settori
Nel file bootsect.S, invece, aggiungiamo subito dopo lo stop del floppy motor un jump all'indirizzo di inizio del setup code
(0x9000:0x0200).
ljmp $INITSEG, $0
E' necessario, a questo punto, modificare il Makefile in modo tale che venga buildato anche setup.S e che il relativo object file
venga concatenato al boot sector nel file immagine.
all: bootsect setup image
.....
setup: setup.o
$(LD) -Ttext 0x0 -s --oformat binary -o $@ $<
setup.o: setup.s
$(AS) -o $@ $<
setup.s: setup.S
$(CPP) -traditional $< -o $@
La regola per creare il file immagine, invece, è la seguente.
image: bootsect setup
cat bootsect > image
cat setup >> image
Buildiamo il nostro nuovo codice, e avviamo il nostro dischetto di boot. Dovrebbero comparire i seguenti messaggi.
Loading system
Page 13
Open Community (C) 2004
Linux Native Boot Process
Wow I am in setup
11. Boot Process step 7: caricamento del kernel in memoria
Prima di effettuare il jump al codice di setup, il codice di boot provvede a caricare il codice del kernel nella locazione
0x1000:0. La lettura avviene utilizzando sempre il servizio 0x02 dell'interrupt 0x10 che abbiamo già esaminato nello step 4. La
lettura avviene una traccia per volta. Visto che il nostro dischetto di boot lavora prevalentemente con floppy da 1.44Mb,
avremo che per ogni traccia leggeremo 18 settori (solo per la prima traccia leggeremo 13 settori, visto che il boot sector e i
settori di setup sono già stati letti).
E' importante ricordare, inoltre, che la lettura delle tracce avviene in questo modo.
Per il drive 0, vengono lette prima le tracce 0 per entrambe le testine, poi la traccia 1 per entrambe le testine e così via, questo
ovviamente serve a ridurre al minimo il movimento delle parti meccaniche. Per i floppy 1.44Mb abbiamo 2 traccie per testina,
per cui la lettura delle tracce avviene nel seguente ordine:
drive=0, testina=0, traccia=0
drive=0, testina=1, traccia=0
drive=0, testina=0, traccia=1
drive=0, testina=1, traccia=1
drive=0, testina=0, traccia=2
drive=0, testina=1, traccia=2
......
Come abbiamo più volte detto il nostro kernel è una semplice routine C che stampa il messaggio "Hello Kernel". Questo kernel
non è altro che un file eseguibile a.out che chiameremo kernel la cui size è definita in bootsect.S attraverso la variabile
SYSIZE. Questa variabile deve contenere la size del kernel in CLICKS (16 bytes). In pratica se la size del kernel è x, allora
SYSIZE=(x+15)/16.
Visto che read_it è una routine abbastanza complessa, riportiamo in questo articolo solo la versione in pseudo codice,
rimandando il lettore ai listati sorgenti allegati all'articolo per i dettagli implementativi.
head -> testina corrente
track -> traccia corrente
sread -> settore corrente
check iniziali
rp_read:
se tutti i bytes del kernel sono stati letti dal disco,
allora stop;
altrimenti vai a ok1_read;
ok1_read:
es contiene il segmento che conterrà la copia della
traccia corrente;
bx contiene l'offset di memoria dove si inizierà a
copiare;
se bx non ha superato i limiti dei 64 Kb allora vai a
ok2_read;
è stato superato il limite di 64 Kb, per cui meno settori
devono essere letti, per sapere quanti settori si dovrà leggere
basta calcolare la distanza tra bx e la fine segmento e dividere
per 512.
ok2_read:
leggi la traccia (o parte di essa) attraverso il servizio
0x02 dell'interrupt 0x13;
se la traccia non è stata letta completamente vai a
ok3_read;
se per la testina n solo una traccia è stata letta allora
vai a ok4_read;
entrambe le tracce sono state lette per la testina n,
Open Community (C) 2004
Page 14
Linux Native Boot Process
quindi head = 1 - n
ok4_read:
per la testina n entrambe le tracce sono state lette,
quindi
head = 1 -n
track = track+1
ok3_read:
update sread
se non abbiamo superato i limiti di 64 kb allora vai a
rp_read;
sono stati superati i limiti di 64 Kb, per cui incrementa
il registro ES e a BX assegna 0.
salta a rp_read;
Questa routine deve essere invocata nel main program prima dell'invocazione alla routine kill_motor.
Abbiamo visto come la costante SYSIZE tiene traccia della size del kernel in CLICKS. E' chiaro che in fase di sviluppo la size
del kernel varia in continuazione e ad ogni compilazione aggiornare questo valore potrebbe essere dispendioso. Per evitare ciò
modifichiamo il Makefile in modo tale che questa venga calcolata a compile time in base alla size del kernel. Ecco la modifica
suggerita:
bootsect.o: bootsect.s
(echo -n "SYSSIZE = ("; \
echo -n `ls -gG kernel | cut -c16-24`; \
echo "+ 15 ) / 16") > tmp.s
cat bootsect.s >> tmp.s
mv tmp.s bootsect.s
$(AS) -o $@ $<
Per avere un minimo di kernel, scriviamo un file main.c e dentro definiamo la routine start_kernel (entry point del kernel) che
effettua un loop infinito.
void start_kernel(void) {
while(1) ;
}
Aggiorniamo il Makefile affinchè da questa semplice file C venga creato un file eseguibile (il nostro kernel).
all: kernel bootsect setup image
......
kernel: main.o
$(LD) -e stext -Ttext 0x1000 -s --oformat binary head.o \
main.o -o $@
main.o: main.c
$(CC) -Wall -O -fstrength-reduce -fomit-frame-pointer \
-c $< -o $@
image:
cat bootsect > image
cat setup >> image
Nel processo di build comparirà un messaggio di warning come il seguente:
ld: warning: cannot find entry symbol stext; defaulting to 00001000
per ora il lettore non si deve preoccupare di questo messaggio, negli step successivi, quando completeremo l'implementazione
del kernel, questo messaggio non apparirà più.
Page 15
Open Community (C) 2004
Linux Native Boot Process
12. Boot Process step 8: spostiamo il kernel all'indirizzo 0x0100:0
A questo punto del processo di boot il kernel viene copiato dall'indirizzo 0x1000:0 a 0x0100:0. A questo punto vi chiederete:
ma perchè il kernel non è stato copiato direttamente lì? La risposta è semplice, se avessimo copiato il kernel direttamente
all'indirizzo 0x0100:0 avremmo coperto tutte l'area BIOS e, quindi, anche le routine di interrupt, tra cui la 0x13, cosa che non
ci avrebbe consentito più la lettura da disco.
La prima cosa da fare in questo step è quello di disabilitare gli interrupt e NMI bootup, per evitare che da questo punto in poi
avvenga un qualche tipo di interruzione.
cli
movb
outb
$0x80, %al
%al, $0x70
# no interrupts
# disabilita NMI bootup
# out[0x80] = 0x70
A questo punto inizia la vera e propria fase di copia, dove DS:SI punta all'area sorgente, mentre ES:DI a quella di destinazione.
La copia avviene a blocchi di 4Kb.
do_move:
movw
%ax, %es
addw
$0x100, %ax
cmpw
$0x9000, %ax
jz end_move
movw
%bx, %ds
addw
$0x100, %bx
subw
%di, %di
subw
%si, %si
movw
$0x800, %cx
rep
movsw
jmp do_move
# ES:DI = indirizzo destinazione
# if (AX == 0x9000) jump a end_move
# DS:SI = indirizzo sorgente
# copia 0x800 words (4096 bytes == 4Kb)
end_move:
....
Si osservi che questo codice copia il kernel fino a coprire l'area da 0x1000 a 0x90000 per un totale di 572 Kb che rappresenta
anche la size max del nostro kernel.
13. Boot Process step 9: stampiamo un semplice messaggio di setup
Per questo messaggio di setup usiamo il classico servizio 0x0E dell'interrupt 0x10. Stamperemo una stringa finchè non viene
trovato il carattere null (0). Questo è il codice da aggiungere al nostro processo di boot. Questo step è opzionale.
leaw
call
ret
msg, %si
prtstr
prtstr:
lodsb
andb
jz
call
jmp
# DS:SI puntano a msg
# stampa il mesaggio
# carica il carattere da stampare da
# DS:SI in AX
%al,%al
fin
prnt1
prtstr
# stampa finchè AL non è 0
# stampa
fin:
ret
prnt1:
pushw
pushw
%ax
%cx
Open Community (C) 2004
Page 16
Linux Native Boot Process
xorb
movw
movb
int
popw
popw
ret
....
%bh,%bh
$0x01, %cx
$0x0e, %ah
$0x10
%cx
%ax
msg: .byte 13, 10
.ascii "Setting up system"
.byte 0x0
14. Boot Process step 10: setting up global e interrupt descriptor tables
La prima cosa da fare in questo step è quello di caricare in IDTR e GTDR i base address e le size delle tabelle IDT e GDT.
Il seguente codice mostra come ciò viene fatto nel nostro codice.
lidt idt_48
lgdt gdt_48
....
# base == 0, limit == 0
# limit == 2048 -> 256 entries
idt_48:
.word
.word
0
0, 0
gdt_48:
.word
.long
0x8000
# gdt limit=2048, 256 GDT entries
gdt + SETUPSEG*0x10
# idt limit = 0
# idt base = 0L
Si osservi come la GDT sia stata definita con 256 entries e con base address pari all'indirizzo corrispondente alla label gdt del
codice di setup (infatti SETUPSEG*0x10 è l'indirizzo assoluto dove inizia il codice di setup e gdt è la label da dove parte la
definizione della tabella).
gdt:
.word
.word
.word
.word
.word
0, 0, 0, 0
0xFFFF
0
0x9A00
0x00CF
.word
.word
.word
.word
0xFFFF
0
0x9200
0x00CF
#
#
#
#
#
#
#
#
#
#
#
dummy
4Gb - (0x100000*0x1000 = 4Gb)
base address = 0
code read/exec
granularity = 4096, 386
(+5th nibble of limit)
4Gb - (0x100000*0x1000 = 4Gb)
base address = 0
data read/write
granularity = 4096, 386
(+5th nibble of limit)
Cerchiamo di capire ora cosa significano questi valori. Dalla label gdt inizia la GDT che come abbiamo visto, contiene 256
entries. La prima entry generalmente è usata nella gestione dei page fault, per cui contiene, in genere, valori dummy. Da qui si
spiega perchè le prime 4 word (primo descriptor) della GDT sono uguali a 0. Il secondo e terzo descriptor, in genere,
descrivono il code e data segment del kernel.
Il kernel code segment è definito attraverso il seguente descriptor.
.word
.word
.word
.word
0xFFFF
0
0x9A00
0x00CF
#
#
#
#
size segment = 4Gb
base address = 0
code read/exec
granularity = 4096, 386
il quale dice che il code segment ha come base address 0x0, size 4Gb e accesso in read/exec.
Page 17
Open Community (C) 2004
Linux Native Boot Process
Il kernel data segment, invece, è definito attraverso quest'altro descriptor.
.word
.word
.word
.word
0xFFFF
0
0x9200
0x00CF
#
#
#
#
#
4Gb - (0x100000*0x1000 = 4Gb)
base address = 0
data read/write
granularity = 4096, 386
(+5th nibble of limit)
Differisce dal precedente per il fatto che l'accesso è di tipo read/write.
Si noti come entrambi le aree hanno base 0x0 e size pari a 4Gb. Questo ci semplificherà le cose, perchè tutti gli indirizzi a 32
bit che utilizzeremo, saranno indirizzi assoluti.
Le restanti entries della GDT sono lasciati ai processi utenti, per cui non verranno al momento definiti.
15. Boot Process step 11: abilita linea A20
In origine il processore 8088 aveva 20 linee di indirizzamento, con cui poteva gestire uno space address di 1 Mb. Se un
programma cercava di far riferimento ad un indirizzo maggiore di 1 Mb un wrap around veniva applicato. Con l'introduzione
dei processori 286 le linee di indirizzamento furono portate a 24 dando così la possibilità di indirizzare fino a 16 Mb. Per
mantenere la compatibilità verso i vecchi processori di default questi operavano con 20 linee di indirizzamento e la nuova
modalità doveva opportunamente essere abilitata via software. Questa nuova funzionalità era ed è tuttora pilotabile attraverso il
controller 8042 della testiera.
Nell'abilitare la linea A20 è importante che la coda della tastiera non contenga dati da elaborare, a tale scopo viene utilizzata la
routine empty_8042 che ritorna non appena tale coda è vuota.
empty_8042:
call
delay
inb $0x64, %al
testb
$0x1, %al
jz no_output
call
delay
inb $0x60, %al
jmp empty_8042
no_output:
testb
$2, %al
# piccolo delay
# %al = in[0x64]
#
#
#
#
if (bit %al[0] == 0) jump no_output
piccolo delay
%al = in[0x60]
jump a empty_8042
# if (bit %al[1] == 1) jump a
# empty_8042
jnz empty_8042
ret
....
delay:
.word
ret
0x00eb
Si osservi come questo codice controlla che i 2 bits del registro di stato siano 0. Nel caso in cui il bit 0 non sia nullo (quindi ci
sono dati in coda), questi vengono rimossi leggendo dalla porta 0x60 gli scan codes o i keybord data.
A questo punto si può abilitare la linea A20 ricordandoci di assicurare che prima di ogni write command la 8042 queue sia
vuota.
call
out[
call
out[
call
empty_8042
0x64 ] = 0xD1
empty_8042
0x60 ] = 0xDF
empty_8042
# command: write to the output port
# bit 1 is set -> A20 enabled
Con il primo comando specifichiamo che effettueremo una scrittura sulla porta di output del controller. Con il secondo
Open Community (C) 2004
Page 18
Linux Native Boot Process
comando, invece, abiliteremo la linea A20 impostando a 1 il bit 1 dell'output port.
16. Boot Process step 12: reset coprocessore
Il reset del coprocessore si effettua semplicemente scrivendo 0 sulle porte di I/O 0xF0 e 0xF1 come riporta il codice seguente.
# Reset
xorw
outb
call
outb
call
coprocessor
%ax, %ax
%al, $0xf0
delay
%al, $0xf1
delay
# out[0xF0] = 0
# out[0xF1] = 0
17. Boot Process step 13: PIC programming
Il compito del controller PIC 8259 è quello di notificare la CPU di eventi provenienti da dispositivi hardware attraverso delle
linee chiamate IRQs. Quando un evento viene notificato al PIC, questi provvederà poi ad avvertire la CPU che attiverà
l'opportuna routine di handling. Se durante il processamento di un evento arriva un altro evento, questi viene messo in coda.
Nel caso di arrivo di due eventi contemporanei, essi vengono gestiti in base ad una priorità ben precisa. In un PC con
processore i386 o superiore, troviamo due controller PIC 8259 collegati in cascata, ognuno che gestisce 8 linee IRQ
(IRQ0-IRQ7), chiamati master e slave. Per comodità chiameremo IRQ0-IRQ7 le linee IRQ del controller master e
IRQ8-IRQ15 quelle dello slave. La Fig. 8 mostra il collegamento tra i due controller PIC che avviene attraverso le linee IRQ2
e IRQ12.
La seguente tabella illustra i dispositivi che generalmente sono collegati ai due controller.
PIC 8259 Master
IRQ0 System Timer
IRQ1 Keyboard
IRQ2 Collegamento al controller slave
IRQ3 COM2/COM4
IRQ4 COM1/COM3
IRQ5 LPT2
IRQ6 FDC
IRQ7 LPT1
PIC 8259 Slave
IRQ8 Real Time Clock Chip
IRQ9 Networking adapter
IRQ10 Non usato
IRQ11 Non usato
IRQ12 Collegamento al controller master
IRQ13 Floating Processor Unit
IRQ14 HDC
IRQ15 Non usato
Chi ha configurato almeno una volta una scheda audio sa che spesso questa viene associata all' IRQ 5, questo perchè
generalmente ad un PC non si collega una seconda stampante, per cui questa associazione non crea alcun conflitto. E'
fondamentale, tuttavia, che due dispositivi hardware realmente collegati al PC non utilizzino la stessa linea IRQ, altrimenti si
genera un conflitto che provocherà malfunzionamenti nel nostro sistema.
La comunicazione tra la CPU ed i due controller avviene attraverso 4 porte I/O (2 per ogni PIC).
Page 19
Open Community (C) 2004
Linux Native Boot Process
PIC 8259 Master-Slave
Le porte sono la 0x20 e 0x21 per il controller master, e la 0xA0 e 0xA1 per il controller slave. Il PIC accetta due tipi di
comandi:
• Initialization Command Words (ICW);
• Operation Command Words (OCW).
I comandi sono 7 e vengono spediti uno in coda all'altro seguendo l'ordine numerico.
Comando ICW1 (porta 0x20 o 0xA0)
Bits 7
6
5
4 3 2 1 0
| | | | |_
| | | |____
| | |_______
| |
| |__________
|_____________
1: comando ICW4 verrà inviato;
1: master; 0: slave;
1: dim. vettori interrupt è 8; 0: dim. vettori
intr. è 4;
1: level triggered (PS/2); 0: edge triggered;
1: comando ICW1;
Comando ICW2 (porta 0x21 o 0xA1)
Bits 7 6 5 4 3 2 1 0
|__|__|__|__|__________ specifica il vettore di interrupt per ogni
IRQ partendo da IRQ0. Ad esempio, se questo
valore è 0x20, allora IRQ0 -> 0x20,
IRQ1 -> 0x21 e così via.
Comando ICW3 (porta 0x21 o 0xA1)
Bits 0-7. Indica a quale IRQ è collegato il canale slave sul canale master e viceversa. Su architetture AT i due controller sono
Open Community (C) 2004
Page 20
Linux Native Boot Process
collegati attraverso IRQ2 e IRQ12.
Comando ICW4 (porta 0x21 o 0xA1)
Questo comando viene inviato se e solo se il bit 0 di ICW1 è impostato a 1.
Bits 7 6 5 4 3 2 1 0
|__|__| | |__| | |_
| |
| |____
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|_______
| |_____________
|
|________________
sempre uguale a 1;
questo bit seleziona il metodo con cui si termina
un interrupt. Se vale 1, viene impostato in
AUTO MODE, ovvero il PIC imposta il relativo
bit dopo aver inviato al processore il segnale
di interrupt, mentre in NORMAL MODE il
processore deve comunicare attraverso OCW2 la
dell'interrupt;
buffered mode;
1: Special Fully Nested Mode (SFNM);
0: SEQUENTIAL MODE;
sempre uguale a 0;
Comando OCW1 (porta 0x21 o 0xA1)
Bits 0-7 Ognuno di questi bit corrisponde allo stato del relativo IRQ. Se il bit è settato, i segnali provenienti dall'IRQ vengono
ignorati, altrimenti se sono azzerati vengono elaborati.
Comando OCW2 (porta 0x20 o 0xA0)
Bits 7 6 5 4 3 2 1 0
| | | |__| |__|__|_
| | |
|__________
| | |________________
| |
| |___________________
|
|______________________
determina il livello prioritario;
sempre uguale a 0;
1: se si comunica la terminazione di un
interrupt;
0: priorità Rotate one;
1: priorità è data dai bits 0-2;
1: viene impostata la priorità in base al bit
6, altrimenti non viene modificata;
Comando OCW3 (porta 0x20 o 0xA0)
Comando inviato al PIC per leggere lo stato dell'ISR, dell'IRR e della MHI (Mask Hardware Interrupt).
Bits 7 6 5 4 3 2 1 0
| | | | | | | |_
| | | | | | |____
| | | | | |
| | | | | |_ _____
| | | | |__________
| | | |_____________
| | |________________
| |___________________
|
|
|______________________
impostato secondo necessità (vedi bit 1);
1: bit 0 specifica quale registro leggere (0 ISR,
1 IRR);
1: PIC in modalità POLLING;
sempre uguale a 1;
sempre uguale a 0;
impostato secondo necessità (vedi bit 6);
1: bit 5 controlla la modalità della maschera;
(1 ON, 0 OFF) e tutte le richieste vengono
elaborate secondo privilegi;
non usato
Dopo questa breve panoramica siamo pronti ad esaminare come i due controller PIC vengono programmati nel nostro piccolo
progetto. Innanzittutto inviamo il comando ICW1 ad entrambi i controller.
Page 21
movb
outb
call
outb
call
$0x11, %al
%al, $0x20
delay
%al, $0xa0
delay
# Comando ICW1 (out[0x20] = 0x11, Master)
# Comando ICW1. (out[0xA0] = 0x11, Slave)
Open Community (C) 2004
Linux Native Boot Process
Questo comando ci dice che ICW4 verrà inviato e che la size di un elemento in un vettore di interrupt è 4 bytes. Viene poi
inviato il comando ICW2 con i quali specifichiamo i vettori di interrupt associati a ciascun IRQ. Nel nostro caso IRQ0 -> 20h,
IRQ1 -> 21h, ..., IRQ8 -> 28h e così via.
movb
outb
call
movb
outb
call
$0x20, %al
%al, $0x21
delay
$0x28, %al
%al, $0xa1
delay
# Comando ICW2. (out[0x21] = 0x20, Master)
# Comando ICW2. (out[0xA1] = 0x28, Slave)
Con il comando ICW3 specifichiamo su quale IRQ del Master è collegato il controller Slave e viceversa. Con il codice
seguente specifichiamo che il controller Slave è collegato su IRQ2 del Master, mentre quest'ultimo è collegato su IRQ4
(IRQ12) dello Slave.
movb
outb
call
movb
outb
call
$0x04, %al
%al, $0x21
delay
$0x02, %al
%al, $0xa1
delay
Il comando ICW4 specifica che l'End Of Interrupt (EOI) verrà gestito dalla CPU attraverso il comando OCW2.
movb
outb
call
outb
call
$0x01, %al
%al, $0x21
delay
%al, $0xa1
delay
Infine con il comando OCW1 mascheriamo tutti gli interrupts sul controller slave, mentre sul Master li mascheriamo tutti
eccetto IRQ2, dove è collegato lo Slave.
movb
outb
call
movb
outb
$0xff, %al
%al, $0xa1
delay
$0xfb, %al
%al, $0x21
18. Boot Process step 14: switch alla modalità protetta
A questo punto siamo pronti per passare alla modalità protetta. Per fare ciò basta settare a 1 il bit 0 (PE) della Machine Status
Word (MSW) che è parte del registro CR0.
Qui di seguito riportiamo il codice utilizzato per effettuare lo swicth alla modalità protetta.
movw
lmsw
call
$0x0001, %ax
%ax
delay
19. Boot Process step 15: jump al codice kernel
Come prima cosa in questo step, scriviamo il codice del kernel con il suo header, per poi ritornare al codice di setup (setup.S)
ed effettuare il jump ad esso.
Come già detto all'inizio di questo articolo, il nostro kernel stampa il messaggio "Hello Kernel". Sappiamo che i servizi BIOS
non sono più disponibili, per cui per scrivere il messaggio dovremo scrivere direttamente nell'area di memoria su cui è
Open Community (C) 2004
Page 22
Linux Native Boot Process
mappato il video. Questa area di memoria inizia all'indirizzo lineare 0xB8000. Il codice seguente nel file main.c stamperà il
suddetto messaggio in rosso.
void start_kernel(void) {
unsigned char *vid_mem=(unsigned char *)(0xb8000);
*vid_mem++ = 'H';
*vid_mem++ = 0x06;
*vid_mem++ = 'e';
*vid_mem++ = 0x06;
*vid_mem++ = 'l';
*vid_mem++ = 0x06;
*vid_mem++ = 'l';
*vid_mem++ = 0x06;
*vid_mem++ = 'o';
*vid_mem++ = 0x06;
....
while(1);
}
A questo punto definiamo un nuovo file head.S che conterrà l'header per il codice kernel. Fondamentalmente questo file
contiene del codice di inizializzazione che verrà eseguito prima che il controllo possa passare al kernel. Questo codice carica
nei registri DS, ES, SS, FS e GS l'entry della GDT (0x10) che contiene il descrittore del segmento dati.
Riportiamo qui di seguito il codice di questo file.
.text
.globl stext
.align 4,0x90
stext:
startup_32:
cld
movl
$0x10,%eax
movw
movw
movw
movw
movw
call
%ax,%ds
%ax,%es
%ax,%fs
%ax,%ss
%ax,%gs
start_kernel
#
#
#
#
DS = ES = FS = SS = GS = entry
0x10
in GDT contiene il riferiemnto
al data segment
# call the kernel
done:
jmp done
Si noti la chiamata alla routine principale del kernel.
Con l'introduzione di questo file è necessario anche la modifica del Makefile. La modifica deve essere tale che da head.S
venga generato un object file head.o da linkare insieme a main.o per formare l'immagine del kernel.
kernel: head.o main.o
$(LD) -e stext -Ttext 0x1000 -s --oformat binary head.o \
main.o -o $@
head.o: head.s
$(AS) -o $@ $<
head.s: head.S
$(CPP) -traditional $< -o $@
Si osservi come nel comando utilizzato per la creazione dell'immagine del kernel, specifichiamo esplicitamente il suo offset
0x1000 che coincide con la label stext (definita nel file head.S).
Page 23
Open Community (C) 2004
Linux Native Boot Process
A questo punto come ultimo passo, dobbiamo aggiungere in setup.S il codice per effettuare il jump alla prima istruzione del
kernel.
.byte 0x66, 0xea
code32:
.long 0x1000
.word 0x08
# ljmp (0x08, 0x1000)
L'indirizzo logico del kernel è (0x08, 0x1000), dove 0x08 è il selettore e 0x1000 è l'offset. Il valore del selettore ci dice che
bisogna prendere l'entry della GDT che parte dal byte 0x08 (cioè la seconda entry, visto che ogni entry è 8 bytes). Questa entry
conterrà il descrittore del segmento codice del kernel, il cui base address è 0x0. Quindi sommando quest'ultimo valore con
l'offset, avremo che il kernel avrà un indirizzo lineare pari a 0x1000.
L'istruzione di jump deve essere una istruzione a 32 bit, mentre il file setup.S è compilato come codice a 16 bit. Per fare in
modo che una istruzione 32 bit possa essere eseguita durante l'esecuzione di codice a 16 bit, si utilizza una particolare
funzionalità dei processori i386 nel quale si fa precedere al codice operativo dell'istruzione da eseguire (0xEA per il ljmp) il
codice 0x66.
20. Boot process step 16: build immagine del kernel
Questo step serve ad introdurre una piccola utility per la build dell'immagine del kernel presente fin dalla release 0.01 di
Linux. Questa utility si chiama build ed è un semplice programma C che fa quello che fino ad ora abbiamo fatto con il
comando cat nel Makefile. Questo piccolo programma semplicemente prende in input il bootsector, il file di setup e il codice di
sistema e spara su stdout l'immagine del kernel. Quindi volendo creare con questa utility un file immagine basta scrivere
l'istruzione riportata si seguito.
./tools/build bootsect setup kernel > image
E' chiaro che una utility di questo tipo è molto più versatile del comando cat perchè consente di effettuare molti controlli
ad-hoc e avere numerose opzioni di build. Ad esempio, è possibile controllare che il boot sector sia effettivamente 512 bytes e
che gli ultimi bytes siano effettivamente 0xAA55 ed altri controlli di questo tipo. Noi utilizzeremo questa utility anche per
scrivere nel boot sector la size del kernel nella variabile syssize in un modo più affidabile rispetto alla regola del Makefile:
bootsect.o: bootsect.s
(echo -n "SYSSIZE = ("; echo -n `ls -gG kernel | cut -c16-24`; \
echo "+ 15 ) / 16") > tmp.s
cat bootsect.s >> tmp.s
mv tmp.s bootsect.s
$(AS) -o $@ $<
che si basa sull'output di un comando shell.
Questa utility si preoccuperà anche di aggiungere il padding al codice di setup (cioè quello che noi facevamo con l'istruzione
finale .org 2048 in setup.S, che ora andrà rimossa).
Vista la semplicità di quest'utility evitiamo di riportare il codice sorgente, rimandando il lettore direttamente al codice allegato
all'articolo.
21. Conclusioni
In questo articolo abbiamo visto step by step come funziona il processo di boot nativo di Linux. Attualmente esso non è più
supportato nella release 2.6 dove si delega ai boot loaders la gestione di questo task. Il motivo per cui è stata fatta questa scelta
è che i moderni dispositivi di massa hanno geometrie molto diverse tra loro, per cui diventa dispendioso gestire questo tipo di
problematiche all'interno del sistema operativo. Nonostante ciò ritengo questo argomento didatticamente ancora valido, perchè
illustra molti concetti utili per chi vuole avvicinarsi al mondo dei sistemi operativi.
Open Community (C) 2004
Page 24
Linux Native Boot Process
22. Download source code
Clicca qui (../downloadarea.html) per scaricare il codice.
23. Bibliografia
[1] INTEL CORPORATION - 80386 Programmer's Manual
(http://www7.informatik.uni-erlangen.de/~msdoerfe/embedded/386html/toc.htm)
[2] Articolo sul sistema operativo Prometeus
Antonio Mazzeo
http://www.programmazione.it
[3] Understanding Linux Kernel Daniel P. Bovet and Marco Cesati O' Reilly
[4] Ralf Brown's Interrupt list http://www.ctyme.com/rbrown.htm
Open Community (C) 2004
Page 25
Open Community (C) 2004