Istruzioni per l'uso del nucleo
Introduzione
Il programma è composto dai moduli sistema, io e utente.
Tutti e tre i moduli sono caricati in memoria da un boot loader
(useremo lo stesso usato per gli esempiI/O).
Il modulo io contiene i processi esterni delle periferiche (in questa versione, solo
la tastiera, il video in modalità testo, gli hard disk a interruzione di programma e bus mastering).
Altre periferiche possono essere aggiunte modificando il modulo
di io stesso.
Il modulo utente contiene il programma che il nucleo dovrà eseguire.
Il sistema verrà eseguito su un emulatore (QEMU).
1. Scaricare i sorgenti
Se non ancora fatto, installare la libreria libce seguendo le istruzioni.
Se non ancora fatto, installare QEMU seguendo le istruzioni.
Scaricare il file nucleo-7.1.2.tar.gz e
scompattarlo in una directory qualsiasi:
tar xvf nucleo-7.1.2.tar.gz
Verrà creata la directory nucleo-7.1.2, contenente tutti i file necessari.
Portarsi nella directory nucleo-7.1.2:
cd nucleo-7.1.2
2. Scrivere un programma utente
I programmi utente vanno scritti in C++ nel file utente/utente.cpp e compilati.
Nello scrivere i programmi utente tenete presente che potete usare esclusivamente
le funzioni di libreria dichiarate in utente/lib.h e le primitive
di sistema dichiarate in include/sys.h e include/io.h. È possibile
anche usare le funzioni di libce.h che non accedono direttamente all'I/O o ai
registri privilegiati del processore.
Alcuni programmi di esempio si trovano nella directory utente/examples.
Per provare gli esempi copiate il relativo file rinominandolo in utente/utente.cpp.
Una volta preparato il file utente/utente.cpp procedete a compilare tramite
make
Se tutto funziona, verranno creati (o ricreati) i file:
- build/sistema: il modulo sistema, risultato della compilazione di sistema/sistema.s e sistema/sistema.cpp, caricabile dal boot loader;
- build/utente: il modulo utente, risultato della compilazione di utente/utente.cpp, utente/utente.s e utente/lib.cpp;
- build/io: il modulo I/O, risultato della compilazione di io/io.cpp e io/io.s;
In ogni caso, se lanciate make senza aver fatto modifiche, il programma
capirà che non è necessario ricompilare niente e ve lo dirà.
3. Avviare il sistema
Lanciare lo script boot nella directory nucleo:
boot
Lo script avvia la macchina virtuale in modo che esegua il boot loader. Il boot loader abiliterà il modo a 64 bit,
caricherà il programma build/sistema e salterà all'etichetta start (in sistema/sistema.s).
I messaggi che si vedono sul terminale da cui si è lanciato boot sono i messaggi che arrivano dalla porta
seriale della macchina virtuale. I primi sono inviati dal boot loader; poi, quando il boot loader salta al nucleo, i
messaggi che si vedono sono quelli che, nel codice del nucleo, sono passati alla funzione flog.
4. Utilizzo del debugger
Per usare il debugger è sufficiente passare il parametro -g allo script boot:
boot -g
Quindi, da un altro terminale, ci si deve portare nella directory che contiene il nucleo e scrivere:
debug
Verrà avviato il debugger gdb insieme ad alcune estensioni utili per il debugging del nucleo.
A questo punto è possibile inserire breakpoint, eseguire le istruzioni una alla volta, esaminare le variabili etc.
gdb fornisce un help in-linea che si può richiamare tramite il comando help.
Alcuni comandi utili:
- break: (abbreviabile in b) inserisce un breakpoint in un punto del programma. L'istruzione a cui
fermarsi può essere specificata come un numero di riga nel file corrente, oppure
con la sintassi nome-file:numero-di-riga. Al posto del numero di riga si
può usare anche un nome di funzione. Si noti che
gdb tenta di inserire il breakpoint dopo il prologo della funzione. Se la funzione non ha
un prologo standard (come alcune delle nostre che manipolano lo stato del processore
a basso livello), gdb si confonde e inserisce il breakpoint in punti che
non hanno molto senso. In genere non è un probema, ma se si vuole fermarsi
alla primissima istruzione di una funzione di questo tipo conviene usare la sintassi
con il numero di riga, oppure la sintassi
break *indirizzo, dove indirizzo è l'indirizzo dell'istruzione.
- continue, step, step-instruction, next, finish, advance:
(abbreviabili in c, s, si, n, fin, adv).
Vari modi per proseguire l'esecuzione.
- Con c si prosegue fino al prossimo
breakpoint (o fino alla terminazione),
- con s si esegue una istruzione del linguaggio sorgente e
- con si una istruzione del linguaggio macchina.
- Con n si esegue una istruzione, ma senza entrare in eventuali funzioni che l'istruzione chiama.
- Con fin si prosegue fino alla fine della funzione corrente.
- adv richiede un parametro che è il punto del programma fino a dove si vuole avanzare
(va specificato come per i breakpoint).
Si noti che n usa una euristica
per capire quando l'esecuzione di una funzione è terminata tenendo conto di
eventuali chiamate ricorsive. Questa euristica fallisce per la nostra carica_stato.
Se si vuole scavalcare una call carica_stato conviene usare adv numero-linea,
specificando il numero della linea (del file sorgente) successiva alla call. Il numero di linea può anche essere specificato come un numero relativo alla linea corrente (per es. adv +1).
- print (abbreviabile in p): stampa il risultato di una espressione
(per esempio, il contenuto di una variable definita nel programma).
L'espressione può anche contenere chiamate a funzioni
definite nel programma. Si noti che le funzioni vengono eseguite nel contesto corrente
della macchina virtuale. Nel nostro caso, se siamo nel contesto utente, chiamare funzioni di livello
sistema potrebbe casare una eccezione di protezione nella macchina virtuale.
- watch espressione: permette di fermare l'esecuzione ogni volta che espressione cambia
valore; si noti che l'esecuzione si ferma subito dopo che l'espressione ha cambiato valore, quindi
l'istruzione responsabile del cambiamento non sarà quella mostrata dall'instruction pointer, ma quella
immediatamente precedente.
- cond numero-del-breakpoint espressione: modifica un breakpoint esistente in modo
che l'esecuzione si fermi solo quando espressione è vera; si può ottenere lo stesso
effetto anche quando si crea il breakpoint, aggiungendo "if espressione" dopo il normale comando
di break.
- set: consente di assegnare valori a variabili del programma o di gdb (si veda
avanti). La sintassi è set variabile = espressione.
L'espressione può essere specificata come per print.
- call: consente di chiamare una funzione definita nel programma. La funzione
va chiamata con sintassi C++, incluse le parentesi aperte e chiuse nel caso di funzioni
senza argomenti. Valgono le stesse considerazioni fatte per print.
Con il comando set è possibile anche definire variabili di gdb. Il nome
delle variabili di gdb deve cominciare con $. Una volta definite possono essere
usate come le altre, ma non è possibile prenderne l'indirizzo (quindi, in particolare
non possono essere passate come argomento a funzioni del programma che vogliono
un riferimento).
4.1. Estensioni al debugger
Nella cartella debug/nucleo.py sono definite delle estensioni al debugger specifiche
per il nucleo.
Le estensioni definiscono alcuni nuovi comandi che si spera facilitino la comprensione
del funzionamento del nucleo, e magari anche la soluzione degli esercizi.
I comandi sono i seguenti:
- context: questo comando viene invocato automaticamente ogni volta che il programma
si blocca (per un breakpoint o single-step) e il controllo ritorna a gdb, ma può
essere anche invocato esplicitamente. Mostra una serie di informazioni relative allo
stato corrente della macchina:
- [backtrace]: lo stack delle funzioni chiamate (con la funzione più
recente in cima);
- [sorgente]: 10 righe del file sorgente intorno alla prossima istruzione da
eseguire (evidenziata in reverse);
- [variabili] il contenuto di tutti i parametri della funzione
corrente e delle sue variabili locali (si noti che alcune di queste potrebbero
mostrare errori fino a quando non vengono inizializzate);
- [code processi] le liste esecuzione, pronti e p_sospesi, più lo
stato dei semafori la cui coda non è vuota.
- [esecuzione] alcuni campi del descrittore del processo attualmente puntato da esecuzione;
- [protezione] lo stato di esecuzione del processore:
- il livello di privilegio (utente o sistema);
- se le interruzioni esterne mascherabili sono abilitate o disabilitate;
- se le istruzioni io-sensitive sono permesse o vietate;
- il contenuto del registro cr3;
Se il file sorgente corrispondente all'instruction pointer attuale è un file assembly,
[variabili] è sostituito da [registri] e [pila]. [registri] mostra il contenuto dei registri
generali (in esadecimale) e del registro RFLAGS, decodificato. [pila] mostra la parte superiore della
pila corrente. La pila è mostrata a righe di 8 byte, rappresentate in esadecimale con il byte
meno significativo a destra. Per ogni riga viene mostrato l'indirizzo della riga, la riga stessa e
l'offset del suo byte meno significativo rispetto a RBP. In alcuni casi vengono anche mostrate informazioni aggiuntive
sul contenuto della riga, per esempio se si tratta di una delle 5 parole quadruple salvate dal meccanismo
delle interruzioni (non sempre il debugger è in grado di capirlo).
- process: informazioni sui processi. Sottocomandi;
- dump [id]: mostra lo stato salvato del processo id
(o del processo puntato da esecuzione se id viene omesso).
Lo stato mostra: le cinque parole quadruple salvate in cima alla pila sistema;
il contenuto dei registri salvati nel descrittore di processo; la prossima
istruzione che il processo eseguirà quando verrà rimesso in esecuzione;
- list [system|user|all]: elenca tutti i processi
o solo quelli di sistema o solo quelli utente, in base all'argomento; se l'argomento
è omesso viene assunto all.
- semaphore [waiting]: mostra lo stato di tutti i semafori allocati
o, se viene passato l'argomento waiting, soltanto di quelli la cui coda non è vuota;
- a_p: mostra il contenuto delle righe non vuote del vettore a_p; per ogni riga non vuota viene
mostrata parte del descrittore del relativo processo esterno.
Altri comandi sono forniti dalla libce e possono essere usati anche per il nucleo:
- idt [gate]: mostra il contenuto della riga gate della IDT (o di tutte le righe non vuote della IDT, se gate
è omesso); per ogni riga mostra:
- il numero del gate in esadecimale, tra parentesi quadre;
- se il livello della routine è sistema (sys) o utente (usr);
- se il gate è di tipo interrupt (intr) o trap (trap);
- l'indirizzo della routine (in esadecimale);
- tra parentesi tonde, il modulo e il nome della routine separati da due punti (uno, l'altro o entrambi potrebbero essere omessi se non noti).
Inoltre, ciascuna riga è colorata in rosso se il gate non è accessibile da livello di privilegio corrente, e verde altrimenti.
- apic: mostra il contenuto delle righe non vuote della redirection table e dei registri IRR e ISR dell'APIC.
Per ogni riga non vuota della redirection table viene mostrato:
- Il numero del piedino di richiesta dell'APIC, tra parentesi quadre;
- Se la linea è considerata attiva sul livello alto (polarity=high) o basso (polarity=low);
- Se il riconoscimento è sul fronte (mode=edge) o sul livello (mode=level);
- Il vettore di interruzione, su due cifre esadecimali;
- La scritta masked tra parentesi tonde, se le richieste sono mascherate (se sono abilitate non viene scritto niente).
I registri IRR e ISR sono mostrati in esadecimale a gruppi di 16 bit (4 cifre esadecimali) separati da due punti; ogni
gruppo rappresenta quindi una classe di priorità. La priorità aumenta da destra a sinistra.
- vm maps|path|table|tree: informazioni sulla memoria virtuale. Sottocomandi:
- maps [id]: Mostra lo stato dello spazio di indirizzamento del processo id (o di quello puntato
da esecuzione se l'argomento è omesso); vengono mostrati gli indirizzi virtuali (in esadecimale e percorso-ottale)
di ogni pagina di livello 1 e un riassunto dei relativi byte di accesso, omettendo tutte le pagine i cui byte di accesso sono identici
a quelli dell'ultima pagina mostrata; unmapped indica che la pagina non ha una traduzione; altrimenti viene mostrato lo stato dei vari bit
(si noti che si tratta dello stato complessivo che si ottiene osservando tutti i byte di accesso nel percorso di traduzione):
- S oppure U: accesso consentito solo da livello sistema o anche da livello utente;
- W oppure R: accesso consentito in lettura/scrittura o solo lettura;
- PWD: Page Write Through settato;
- PCD: Page Cache Disable settato;
- PS: Page Size settato (l'indirizzo fa parte di una pagina di grandi dimensioni);
- A: qualche bit A è settato;
- D: il bit D è settato;
Inoltre, ciascuna riga è colorata in rosso se il corrispondente intervallo di indirizzi non è accessibile
dal livello di privilegio corrente, in giallo se è accessibile solo in lettura, e in verde se è completamente accessibile.
- path indirizzo[@root]: mostra il percorso di traduzione di indirizzo
a partire dalla tabella radice di indirizzo root (o di cr3 se root è omesso).
Il comando mostra root e, per ogni tabella presente dal livello 4 a livello 1, mostra le tre cifre ottali usate
per accedere al descrittore di quel livello, lo stato dei bit R/W, U/S, A e D e, se l'entità successiva
è presente, il suo indirizzo fisico;
- table indirizzo: mostra tutti i descrittori non completamente nulli della tabella che si trova a indirizzo;
il formato è simile a quello usato dal sotto-comando path;
- tree [indirizzo]: mostra tutto il sottoalbero presente avente come radice la tabella che si trova a indirizzo;
il formato è simile a quello usato dal sotto-comando path.
Inoltre, alcuni tipi vengono mostrati in modo speciale:
- le variabili di tipo natb, natw, etc. vengono mostrate sia in decimale che in esadecimale;
- le variabili di tipo vaddr vengono mostrate in esadecimale e formato-percorso, in cui
l'indirizzo è scomposto in 4 parti, separate da trattini: la prima parte è U (utente) se i 16 bit più significativi
sono tutti 1, S (sistema) se sono tutti 0 e ? altrimenti (indirizzo non normalizzato); seguono i quattro indici dei vari livelli
(tre cifre ottali ciascuno).
- le variabili di tipo tab_entry sono mostrate in esadecimale e poi decodificate, mostrando i flag attivi del byte di accesso;
se il flag P è settato viene mostrata anche una freccia seguita dall'indirizzo fisico a cui l'entrata punta, altrimenti viene scritto unmapped.
- i riferimenti a tab_entry vengono mostrati come i tab_entry, ma preceduti da una chiocciola;
- i puntatori a des_proc sono mostrati in esadecimale, seguiti da una freccia e alcuni campi del descrittore a cui puntano.