Il sito si è trasferito a qui the site has been moved here
|
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 correnti 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. Alcuni concetti di base: modalità reale e protetta Boot process step 0: stampa di un messaggio di boot Boot Process step 1: copia
del boot sector in 0x9000:0 Boot
Process step 2: definizione dello spazio per lo stack Boot Process step 3: stop del motore del floppy Boot
Process step 4: caricamento del codice di setup Boot
Process step 5: routine di debug e stampa del messaggio “Loading system” Boot Process step 6: jump al codice di setup Boot
Process step 7: caricamento del kernel in memoria Boot Process step 8: spostiamo il kernel all'indirizzo 0x0100:0 Boot
Process step 9: stampiamo un semplice messaggio di setup Boot Process step 10:
setting up global e interrupt descriptor tables Boot Process step 11:
abilita linea A20 Boot Process step 12: reset
coprocessore Boot Process step 13: PIC
programming Boot Process step 14:
switch alla modalità protetta Boot Process step 15: jump al codice kernel Boot Process step 16: build immagine del kernel
Clicca qui per scaricare il codice sorgente degli steps sopra riportati.
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 1° 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. 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 (2^46 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 tak 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).
·
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 1° 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).
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.
·
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 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. 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. 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 . .code16 .text .global _start _start: movb $0x0E, %ah
movb $'H', %al
// stampa il carattere 'H'
int 0x10
movb $'e', %al
// stampa il carattere 'e'
int 0x10
......
// stampa altri caratteri done: jmp done // 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”. 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 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 all
-> per una build completa el
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 all make disk 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 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 # DS = 0x07C0 movw $INITSEG, %ax movw %ax, %es # ES = 0x9000 movw $256, %cx # CX = 256
subw %si, %si
# SI = 0
subw %di, %di
# DI = 0 cld rep movsw # copia boot sector ljmp $INITSEG, $go # 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. 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).
Qui di
seguito riportiamo il codice per il setting dell'area stack.
....
go:
movw $0x4000-12, %di
movw %ax, %ds
movw %ax, %ss
// SS = 0x9000
movw %di, %sp
// SP = 0x4000-12
<stampa “Hello World” come l'esempio precedente>
.org 510
boot_flag:
.word 0xAA55 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 µPD765. 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.
Fig.
6
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 $0x3f2, %dx xorb %al, %al outb %al, %dx # out[0x3f2] = 0 .word 0x00eb, 0x00eb # breve delay
ret questa routine verrà invocata nel nostro main program subito dopo la
stampa del messaggio di “Hello World”. 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
# AH = 0 -> service 0x00
xorb %dl, %dl
# DL = 0 -> drive 0
int $0x13
# 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, come anticipata sopra. 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.
Qui di seguito riportiamo il codice assembler
utilizzato per fare il loading del codice di setup.
movb $0x02, %cl
movw $0x0200,
%bx
movb $0x02, %ah
movb $4, %al
int $0x13
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. 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
# ottieni la posizione del cursore in DH e DL
xorb %bh, %bh # BH = 0
int $0x10
movw $17, %cx
movw $0x0007,
%bx
movw $msg1, %bp
movw $0x1301,
%ax
int $0x10
....
msg1: .byte 13 10
# new line
.ascii “Loading system ” 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.
pushw %ax
# stampa il codice di errore sul video
call print_nl
# stampa unanew line sul video
movw %sp, %bp
call print_hex
# stampa AX sul video
popw
%ax
jmp load_setup # riprova di nuovo 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:
# loop infinito
jmp done
.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
Wow I am in setup 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 1° 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 testine. 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 quandi settori si dovrà leggere basta
calcolare la distanza tra bx e la fine del 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, 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 una 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
cat kernel >> 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ù. 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
# no interrupts
movb $0x80, %al
# disabilita NMI bootup outb %al, $0x70 # 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
# ES:DI = indirizzo destinazione addw $0x100, %ax cmpw $0x9000, %ax # if (AX == 0x9000) jump a end_move
jz
end_move
movw %bx, %ds
# DS:SI = indirizzo sorgente
addw $0x100, %bx
subw %di, %di
subw %si, %si
movw $0x800, %cx # copia 0x800 words (4096 bytes == 4Kb) rep movsw jmp do_move
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. 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 msg, %si
# DS:SI puntano a msg
call prtstr
# stampa il mesaggio
ret
prtstr: lodsb
# carica il carattere da stampare da DS:SI in AX
andb %al,%al
jz fin
# stampa finchè AL non è 0
call prnt1
# stampa
jmp prtstr
fin: ret
prnt1:
pushw %ax
pushw %cx
xorb %bh,%bh
movw
$0x01, %cx
movb $0x0e, %ah
int $0x10
popw %cx
popw %ax
ret
....
msg:
.byte 13, 10
.ascii "Setting up system"
.byte 0x0 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
# base == 0, limit == 0
lgdt gdt_48
# limit == 2048 -> 256 entries
....
idt_48:
.word 0
# idt limit = 0
.word 0, 0
# idt base = 0L
gdt_48:
.word 0x8000
# gdt limit=2048, 256 GDT entries
.long gdt +
SETUPSEG*0x10 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 0, 0, 0, 0
# dummy
.word 0xFFFF
# 4Gb - (0x100000*0x1000 = 4Gb)
.word 0
# base address = 0
.word 0x9A00
# code read/exec
.word 0x00CF
# granularity = 4096, 386
# (+5th nibble of limit)
.word 0xFFFF
# 4Gb - (0x100000*0x1000 = 4Gb)
.word 0
# base address = 0
.word 0x9200
# data read/write
.word 0x00CF
# 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 0xFFFF
# size segment = 4Gb
.word 0
# base address = 0
.word 0x9A00
# code read/exec
.word 0x00CF
# granularity = 4096, 386 il quale dice che il code segment ha come base
address 0x0, size 4Gb e accesso in read/exec. Il kernel data segment, invece, è definito
attraverso quest'altro descriptor.
.word 0xFFFF
# 4Gb - (0x100000*0x1000 = 4Gb)
.word 0
# base address = 0
.word 0x9200
# data read/write
.word 0x00CF
# 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. 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
# piccolo delay
inb
$0x64, %al # %al = in[0x64]
testb $0x1, %al
jz no_output
# if (bit %al[0] == 0) jump no_output
call delay
# piccolo delay
inb
$0x60, %al # %al = in[0x60]
jmp empty_8042
# jump a empty_8042
no_output:
testb $2, %al
# if (bit %al[1] == 1) jump a
# empty_8042
jnz empty_8042
ret
....
delay:
.word 0x00eb
ret 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 empty_8042
out[ 0x64 ] = 0xD1
# command: write to the output port
call empty_8042
out[ 0x60 ] = 0xDF # bit 1 is set -> A20 enabled
call
empty_8042 Con il primo comando specifichiamo che effettueremo una scrittura sulla porta di
output del controller. Con il secondo comando, invece, abiliteremo la linea A20
impostando a 1 il bit 1 dell'output port. 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 coprocessor
xorw %ax, %ax
outb %al, $0xf0 # out[0xF0] = 0
call delay
outb %al, $0xf1 # out[0xF1] = 0
call delay 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.
Chi ha configurato almeno una volta una scheda
audio sa che spesso questa viene associata all' IRQ5, 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 stessalinea
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).
·
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. vettrii 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 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 fine dell'interrupt;
| | | |
| |______
buffered mode;
| |
| | |_________
buffered mode;
| |
| |___________
1:
Special Fully Nested Mode (SFNM);
|
| |
0: SEQUENTIAL MODE;
| |
|_____________
sempre uguale a
0;
| |________________
sempre uguale a 0;
|___________________ 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;
| |
| | |
| |____ determina il
livello prioritario;
| |
| |
| |_______ determina il livello prioritario;
| |
| | |__________
sempre uguale a 0;
| |
| |_____________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.
movb $0x11, %al
# Comando ICW1 (out[0x20] = 0x11, Master)
outb %al, $0x20
call delay
outb
%al, $0xa0 # Comando ICW1. (out[0xA0] = 0x11, Slave)
call
delay 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 $0x20, %al
# Comando ICW2. (out[0x21]
= 0x20, Master)
outb %al, $0x21
call delay
movb
$0x28, %al # Comando ICW2. (out[0xA1] = 0x28, Slave)
outb %al, $0xa1
call delay 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 $0x04, %al
outb %al, $0x21
call delay
movb $0x02, %al
outb %al, $0xa1
call delay Il comando ICW4 specifica che l'End Of Interrupt
(EOI) verrà gestito dalla CPU attraverso il comando OCW2.
movb $0x01, %al
outb %al, $0x21
call delay
outb %al, $0xa1
call
delay Infine con il comando OCW1 mascheriamo tutti gli
interrupt sul controller slave, mentre sul Master li mascheriamo tutti eccetto
IRQ2, dove è collegato lo Slave.
movb $0xff, %al
outb %al, $0xa1
call delay
movb
$0xfb, %al
outb %al, $0x21 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 $0x0001, %ax
lmsw %ax
call delay 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 è 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
# DS = ES = FS = SS = GS = entry 0x10
movw
%ax,%ds
# in GDT contiene il riferiemnto
movw %ax,%es
# al data segment
movw
%ax,%fs movw %ax,%ss
movw
%ax,%gs
call start_kernel # 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). 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
# ljmp (0x08, 0x1000)
code32:
.long 0x1000
.word 0x08 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. 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. In questo articolo abbiamo visto step by step come
funziona il processo di boot nativo di Linux. Questo processo non è più
supportato a partire dalla release 2.6 visto che diventava molto complesso
gestire le varie geometrie dei dispositivi fisici. Quindi a partire da questa
release si delega ai boot loader il compito di effettuare questo processo. [1] INTEL CORPORATION - “80386 Programmer's
Manual” [2] http://www.programmazione.it
– Articolo sul sistema operativo Prometeus di Antonio Mazzeo [3]
Understanding Linux Kernel – Daniel P. Bovet & Marco Cesati – O' Reilly
|