Assembly Avanzato con MASM
Capitolo 2: Il BIOS - Basic Input Output System
Non appena si fornisce l'alimentazione elettrica ad un computer appartenente alla
famiglia hardware dei PC 80x86, viene attivato un procedimento standard il
cui scopo è quello di svolgere le seguenti importantissime fasi:
- autodiagnosi e inizializzazione dell'hardware
- installazione delle ISR per l'interfaccia a basso livello con l'hardware
- lancio del sistema operativo
Tutto questo lavoro viene svolto da un software (spesso scritto in Assembly)
che risiede su una apposita memoria ROM denominata ROM BIOS e disposta
fisicamente in un chip del computer; la sigla BIOS è l'acronimo di Basic
Input Output System (sistema primario per le operazioni di I/O con
l'hardware).
Gli aspetti relativi al BIOS assumono quindi una notevole importanza
generale, non solo per i cosiddetti "programmatori di sistema" che si dedicano alla
scrittura dei SO; analizziamo quindi in dettaglio le tre fasi elencate in
precedenza.
2.1 Il POST
La delicatissima fase di autodiagnosi e inizializzazione dell'hardware assume, come
è facile intuire, una importanza vitale per il computer; il software del BIOS
che svolge tale fase, prende il nome di POST che è l'acronimo di Power On
Self Test (autodiagnosi all'accensione del PC).
Prima di analizzare le fasi che caratterizzano il POST, dobbiamo occuparci di
un aspetto molto interessante che ci permette di capire la tecnica attraverso la
quale il BIOS prende il controllo all'accensione del computer; a tale proposito,
è necessario premettere che esistono alcune differenze tra vecchi e nuovi modelli di
CPU (e tra vecchie e nuove architetture dei PC).
Il primo aspetto da considerare riguarda il fatto che una qualsiasi CPU della
famiglia 80x86, viene sempre inizializzata in modalità reale; come sappiamo,
in tale modalità le CPU utilizzano un address bus a 20 linee
attraverso il quale possono indirizzare un massimo di 220 byte,
pari ad 1 MiB di RAM (cioè, tutti gli indirizzi fisici compresi tra
00000h e FFFFFh).
Affinché i programmi che girano in modalità reale possano accedere ai servizi del
BIOS, è necessario quindi che il BIOS stesso venga mappato in un'area
della RAM compresa tra 00000h e FFFFFh; la posizione di tale
area è stata stabilita attraverso una convenzione con i produttori dei BIOS.
Nei vecchi PC di categoria XT, equipaggiati con CPU di classe
8088, 8086 e 80186, al BIOS viene riservata un'area da
8 KiB posizionata proprio alla fine del primo MiB di RAM tra gli
indirizzi fisici FE000h e FFFFFh; a tali indirizzi fisici possiamo
associare, ad esempio, gli indirizzi logici compresi tra FE00h:0000h e
FE00h:1FFFh.
A partire dai PC di categoria AT, equipaggiati con CPU di
classe 80286 o superiore, al BIOS viene riservata un'area da 64
KiB posizionata proprio alla fine del primo MiB di RAM tra gli indirizzi fisici
F0000h e FFFFFh (la cosiddetta regione F); a tali indirizzi
fisici possiamo associare, ad esempio, gli indirizzi logici compresi tra
F000h:0000h e F000h:FFFFh.
In sostanza, la parte della RAM riservata al BIOS si comporta, in
realtà, come una vera e propria ROM; per i programmi che intendono usufruire
dei servizi del BIOS, tale area risulta quindi accessibile solamente in
lettura!
La Figura 2.1, già presentata nella sezione Assembly Base, mostra graficamente
la posizione della ROM BIOS nella RAM (PC di classe AT).
Un'altra convenzione relativa alla famiglia hardware dei PC 80x86 ha stabilito
che la prima istruzione in assoluto che la CPU esegue all'accensione (o al
riavvio) del computer, deve trovarsi rigorosamente all'inizio dell'ultimo paragrafo
di memoria indirizzabile dalla CPU stessa; tutto dipende, quindi, dall'ampiezza
dell'address bus.
Una CPU (come la 8086) con address bus a 20 linee, può
indirizzare un massimo di 1 MiB di RAM compresa tra gli indirizzi fisici
00000h e FFFFFh; in tal caso, l'ultimo paragrafo di memoria è quello
compreso tra gli indirizzi fisici FFFF0h e FFFFFh.
La prima istruzione eseguita dalla CPU all'accensione (o al riavvio) del
computer si viene a trovare quindi all'indirizzo fisico FFFF0h; a tale
indirizzo fisico possiamo associare, ad esempio, l'indirizzo logico FFFFh:0000h.
Per soddisfare la convenzione appena enunciata, i registri di "indirizzamento" delle
CPU con address bus a 20 linee si "autoinizializzano" in questo
modo:
CS = FFFFh, IP = 0000h, SS = 0000h, SP = 0000h, DS = 0000h, ES = 0000h
Di conseguenza, all'accensione (o al riavvio) del computer, la CPU tenta di
eseguire l'istruzione che si trova all'indirizzo logico CS:IP=FFFFh:0000h; se
vogliamo sapere quale istruzione è presente a tale indirizzo possiamo servirci, ad
esempio, del programma DEBUG disponibile in ambiente DOS. La Figura 2.2
illustra l'output prodotto dal comando u (unassemble) di DEBUG su un
vecchio PC dotato di CPU 8088.
Come possiamo notare, all'indirizzo logico FFFFh:0000h è presente l'istruzione:
JMP F000:E05B
Si tratta quindi di un FAR jump all'indirizzo logico F000h:E05Bh; tale
indirizzo logico può essere scritto anche come FE00h:005Bh e quindi appartiene
proprio all'area di memoria da 8 KiB riservata alla ROM BIOS dei PC
di classe XT!
Come si può intuire, all'indirizzo logico FE00h:005Bh inizia il codice del
BIOS relativo al POST; da questo momento parte quindi la fase di
autodiagnosi e inizializzazione dell'hardware.
Una CPU (come la 80386, 80486, 80586) con address bus
a 32 linee, può indirizzare un massimo di 232=4 GiB di RAM
compresa tra gli indirizzi fisici 00000000h e FFFFFFFFh; in tal caso,
l'ultimo paragrafo di memoria è quello compreso tra gli indirizzi fisici FFFFFFF0h
e FFFFFFFFh.
Questa situazione fa nascere subito un dubbio legato al fatto che un PC con
address bus a 32 linee può anche non disporre di 4 GiB di RAM;
come è possibile allora che la CPU possa eseguire una istruzione che si trova
all'indirizzo fisico FFFFFFF0h?
Il problema è stato risolto facendo in modo che tale indirizzo venga mappato, non in
RAM, bensì su una memoria EPROM; all'interno della EPROM, il
produttore del PC può inserire tutte le necessarie istruzioni di
inizializzazione.
Per poter accedere all'indirizzo fisico FFFFFFF0h, la CPU si
"autoinizializza" in una particolare modalità chiamata big real mode; in
pratica, la CPU lavora in modalità reale, ma può gestire componenti
Offset a 32 bit in modo da poter indirizzare sino a 4 GiB di
memoria fisica!
Come vedremo nella apposita sezione di questo sito, in big real mode o in
protected mode, un registro di segmento non contiene una componente
Seg, bensì un cosiddetto selettore di segmento la cui struttura è
illustrata in Figura 2.3.
Per il momento ci interessa sapere che i 13 bit del campo INDICE
contengono l'indice di uno tra i possibili 213=8192 elementi
di una apposita tabella presente in memoria; ciascun elemento prende il nome
di descrittore di segmento e, come si intuisce dal nome, contiene la
descrizione completa del segmento di programma a cui il selettore di
segmento fa riferimento.
Una delle informazioni contenute nel descrittore di segmento è il cosiddetto
base address che specifica l'indirizzo fisico a 32 bit da cui inizia
il relativo segmento di programma; i vari indirizzi fisici a cui la CPU deve
accedere si ottengono sommando il base address al contenuto a 32 bit
del registro puntatore utilizzato. Nel caso, ad esempio, dell'indirizzo specificato
dalla coppia DS:ESI, la CPU accede al descrittore di segmento
associato a DS e somma il relativo base address con il contenuto di
ESI.
Premesso che per indicare il base address associato ad un determinato
SegReg la Intel utilizza una sintassi del tipo CS.BASE,
DS.BASE, etc, in fase di accensione (o riavvio) del computer i registri di
"indirizzamento" di una CPU con address bus a 32 linee si
"autoinizializzano" in questo modo:
In sostanza, CS contiene il selettore F000h che punta ad un segmento
di programma con base address pari a FFFF0000h, mentre EIP vale
0000FFF0h; di conseguenza, all'accensione (o al riavvio) del computer, la
CPU tenta di eseguire l'istruzione che si trova all'indirizzo logico:
CS.BASE + EIP = FFFF0000h + 0000FFF0h = FFFFFFF0h
Come è stato spiegato in precedenza, tale indirizzo è mappato in una EPROM la
quale contiene le istruzioni di inizializzazione della CPU; in particolare,
tali istruzioni stabiliscono anche se la CPU debba poi passare in modalità
reale o protetta.
2.1.1 La sequenza del POST
La prima istruzione eseguita dalla CPU cede quindi il controllo al POST;
a questo punto inizia la fase di autodiagnosi e inizializzazione dell'hardware. Questa
fase comporta un numero piuttosto lungo di controlli e verifiche e non può essere certo
dettagliatamente descritta in un tutorial; del resto, il dovere primario di un vero
programmatore Assembly è quello di studiare tutta la documentazione tecnica
relativa agli argomenti che ha intenzione di approfondire.
In particolare, se vogliamo avere una panoramica dettagliata su tutto ciò che accade
durante il POST, possiamo fare riferimento agli appositi manuali tecnici forniti
dai produttori dei BIOS. Su Internet è possibile reperire una vasta raccolta di
documentazione; in particolare, si consiglia di effettuare il download dei documenti
biospostcode.pdf (PhoenixBIOS - POST Tasks and Beep Codes) e
biosawardpostcode.pdf (AwardBIOS - Post Codes & Error Messages).
Eventuali problemi hardware rilevati dal POST, vengono adeguatamente segnalati
all'utente attraverso diversi metodi; in generale, qualunque problema viene segnalato
sul monitor attraverso un apposito messaggio di errore. Se il problema riguarda la parte
video, allora il POST si serve di appositi segnali acustici inviati
all'altoparlante di sistema; i due documenti descritti in precedenza, illustrano tutte
le informazioni relative a questi aspetti.
Ad ogni controllo svolto dal POST, corrisponde un ben preciso codice di
errore da 1 byte; prima di dare inizio ad un nuovo controllo, il POST
invia il relativo codice di errore ad un dispositivo di I/O raggiungibile
attraverso la porta hardware 80h (chiamata Manufacturing Diagnostic
Port).
In caso di errore con conseguente blocco del computer, la porta 80h contiene
quindi il codice del test che ha rilevato il problema; in genere, queste informazioni
sono riservate ai centri di assistenza tecnica, i quali dispongono di apparecchiature
capaci di visualizzare (attraverso un display) l'ultimo codice prodotto dal POST.
Se tutto procede correttamente, l'ultimo codice di errore inviato dal POST è
quello relativo alla fase di boot (lancio del SO); a seconda del tipo
di BIOS installato sul proprio computer, tale codice può assumere valori del
tipo 00h, F7h, FFh.
Se vogliamo conoscere il valore esatto, possiamo servirci, ad esempio, del comando:
i 80
(input from port) fornito dal programma DEBUG; si tenga presente comunque che,
se si lavora con un emulatore DOS, tale valore potrebbe essere privo di senso.
2.2 Installazione delle ISR del BIOS
Terminata la diagnostica e l'inizializzazione dell'hardware, parte la seconda fase del
BIOS che consiste nella installazione in memoria di una numerosa serie di
procedure Assembly; lo scopo di tali procedure è quello di permettere ai programmi
di interfacciarsi a basso livello con l'hardware del computer.
Si tratta di procedure di utilità generale, che diventano indispensabili soprattutto
quando si opera in condizioni estreme; un caso del genere si presenta, ad esempio,
quando si deve scrivere un cosiddetto bootloader (il programma che carica in
memoria il SO).
Per capire il procedimento utilizzato dal BIOS per l'installazione di queste
procedure, è necessario premettere che, durante il POST, viene anche effettuato
il test e l'inizializzazione di un particolare dispositivo chiamato PIC o
Programmable Interrupt Controller (controllore programmabile delle interruzioni);
come è stato spiegato nella sezione Assembly Base e come vedremo in dettaglio
nel prossimo capitolo, il compito del PIC è quello di raccogliere e smistare
tutte le richieste che arrivano dalle periferiche che vogliono dialogare con la
CPU.
Ciascuna richiesta che arriva da una periferica, viene chiamata Interrupt
Request (richiesta di interruzione) o IRQ; al momento opportuno, la
CPU interrompe il programma in esecuzione (da cui il nome "interruzione") e
soddisfa la richiesta attraverso la chiamata di una apposita procedura che, proprio per
questo motivo, viene definita Interrupt Service Routine (procedura di servizio
per le interruzioni) o ISR.
La situazione appena descritta si riferisce alle cosiddette interruzioni
hardware; si tratta cioè di interruzioni dovute a IRQ provenienti dalle
periferiche. Esistono però anche le cosiddette interruzioni software, così
chiamate in quanto provocate dai programmi attraverso l'istruzione INT
(analizzata nella sezione Assembly Base); anche in questo caso, la
CPU soddisfa la richiesta attraverso la chiamata di apposite ISR.
Molte delle ISR, vengono installate proprio dal BIOS prima della fase
di boot; altre ISR possono essere in seguito installate dal SO o
dai programmi.
L'installazione delle ISR, avviene attraverso un procedimento che risponde a
precise convenzioni; in particolare, l'indirizzo logico Seg:Offset di una
qualsiasi ISR deve essere posizionato in un'area della RAM compresa
tra gli indirizzi fisici 00000h e 003FFh (a cui possiamo associare, ad
esempio, gli indirizzi logici compresi tra 0000h:0000h e 0000h:03FFh).
Come si nota in Figura 2.1, tale area viene denominata Interrupt Vectors Area
(area riservata ai vettori di interruzione); tale nome deriva dal fatto che ciascun
indirizzo logico Seg:Offset di una ISR viene indicato con il termine
Interrupt Vector (vettore di interruzione).
Ciascun vettore di interruzione punta quindi ad una ISR che si trova nella
RAM; molti dei vettori di interruzione installati dal BIOS puntano a
delle ISR che si trovano nell'area della RAM riservata alla ROM
BIOS del computer!
Tornando alla Figura 2.1 notiamo che l'area riservata ai vettori di interruzione
occupa complessivamente:
003FFh - 00000h + 1 = 400h byte = 1024 byte
Ogni vettore di interruzione (cioè, ogni indirizzo logico Seg:Offset) occupa
4 byte, per cui in questi 1024 byte trovano posto:
1024 / 4 = 256 = FFh vettori di interruzione
Per convenzione, i vari vettori di interruzione sono numerati, nell'ordine, 00h,
01h, 02h e così via, sino al n. FFh; come già sappiamo, molti dei
vettori di interruzione possono essere chiamati dai programmi (interruzioni software)
attraverso l'istruzione INT (che esegue una FAR call alla relativa
ISR).
Come si può facilmente immaginare, alcuni dei vettori di interruzione sono riservati
rigorosamente al BIOS; altri vettori di interruzione sono riservati al
DOS, mentre altri ancora sono disponibili per i programmi.
Se si vuole conoscere l'elenco completo dei 256 vettori di interruzione, si
può fare riferimento alla apposita
tabella disponibile in questo
sito; per quanto riguarda i vettori di interruzione riservati al BIOS, si
consiglia di scaricare da Internet il documento userman.pdf (PhoenixBIOS
User's Manual).
Per una descrizione estremamente dettagliata di tutti i vettori di interruzione e dei
servizi ad essi associati, si consiglia di consultare la celebre Ralf Brown's
Interrupt List; a tale proposito, si veda la sezione
Siti con argomenti correlati di questo sito.
2.2.1 Esempio pratico per le ISR del BIOS
Consultando il manuale utente del proprio BIOS, si possono ricavare informazioni
sulle varie ISR disponibili; attraverso tali ISR si possono scrivere una
enorme quantità di procedure estremamente compatte ed efficienti.
Vediamo un esempio pratico che si riferisce all'output di una stringa sul video (e che
ci tornerà molto utile nel seguito); a tale proposito, ci serviamo di una ISR
fornita dalla INT 10h - Video BIOS Services (servizi BIOS per il video).
Il servizio n. 13h della INT 10h prende il nome di Write string e
permette di visualizzare una stringa sullo schermo; tale servizio è descritto dalla
Figura 2.4.
La Figura 2.4 ci permette di osservare che ciascuna ISR può fornire numerosi
servizi; tali servizi possono essere selezionati attraverso un apposito valore
passato, in genere, nel registro AH.
Le ISR spesso richiedono uno o più argomenti (entry arguments) che
devono essere passati attraverso i registri generali; gli stessi registri generali
vengono utilizzati dalle ISR per contenere eventuali valori di ritorno
(exit arguments).
Prima di vedere un esempio pratico, analizziamo il concetto di attributo video;
con tale termine si indica il colore di sfondo (background) e di primo piano
(foreground) del testo da stampare.
Il BIOS inizializza la scheda video in modalità testo (o alfanumerica); in
tale modalità, lo schermo viene suddiviso in una matrice di celle disposte su
25 righe (numerate da 0 a 24) e 80 colonne (numerate da
0 a 79).
Ad ogni cella vengono riservati 2 byte di memoria; il primo byte contiene il
codice ASCII del carattere da
stampare, mentre il secondo byte contiene, appunto, gli attributi video del
carattere stesso.
La Figura 2.5 illustra la struttura del byte degli attributi video.
Sia per lo sfondo, sia per il primo piano, possiamo ottenere differenti colori
attivando (1) o disattivando (0) le tre componenti fondamentali
Red (rosso), Green (verde) e Blue (blu), per un totale di
23=8 colori; per ciascun colore, il bit IN (intensità)
permette di selezionare una intensità alta (1) o bassa (0), per
cui i colori complessivi diventano 2*8=16.
Su alcuni BIOS (soprattutto quelli meno recenti) il bit in posizione
7 viene definito BL o blinking (lampeggiamento); tale bit
permette di attivare (1) o disattivare (0) il lampeggiamento dello
sfondo.
Tornando alla Figura 2.4, nel registro AL bisogna inserire un valore che
rappresenta la modalità di scrittura.
I valori 0 e 1 indicano che la stringa è composta da soli caratteri,
mentre gli attributi video (uguali per tutti i caratteri) vengono specificati dal
registro BL; la posizione del cursore viene aggiornata solo per AL=1.
I valori 2 e 3 indicano che la stringa è composta da una sequenza di
coppie (carattere, attributo) con la possibilità quindi di specificare un attributo
video differente per ogni carattere (il valore in BL viene ignorato); la
posizione del cursore viene aggiornata solo per AL=3.
Fatte queste premesse, supponiamo di avere un blocco dati referenziato da DS
e contenente le seguenti informazioni:
A questo punto, nel blocco codice del programma possiamo richiedere la visualizzazione
della stringa MyString con le seguenti istruzioni:
Volendo creare una stringa formata da coppie (carattere, attributo), possiamo scrivere,
ad esempio:
MyString db 'T', 1Eh, 'e', 1Fh, 's', 04h, 't', 01h
In tal caso, bisogna ricordare che la lunghezza della stringa comprende i soli
caratteri, per cui dobbiamo scrivere:
strLen equ ($ - MyString) / 2
Nel seguito del capitolo e nei capitoli successivi vedremo altri esempi relativi
ai servizi del BIOS e sarà anche chiarito il concetto di pagina video.
2.3 La BDA - BIOS Data Area
Numerosissime informazioni ricavate dal BIOS durante il POST, vengono
memorizzate in una apposita area della RAM accessibile a tutti i programmi;
tale area prende il nome di BDA o BIOS Data Area (area dati del
BIOS) e, come si vede in Figura 2.1, per convenzione deve essere compresa tra
gli indirizzi fisici 00400h e 004FFh (a cui possiamo associare, ad
esempio, gli indirizzi logici compresi tra 0040h:0000h e 0040h:00FFh).
La Ralf Brown's Interrupt List contiene una descrizione dettagliata della
BDA nel file memory.lst; si veda la sezione
Siti con argomenti correlati di questo sito.
Per leggere il contenuto della BDA possiamo utilizzare un metodo diretto che
consiste nel caricare il valore 0040h in un registro di segmento (ad esempio,
ES) in modo da poter scrivere poi istruzioni del tipo:
mov ax, [es:OffsetBDA]
Vediamo un esempio pratico basato sul fatto che all'offset 0010h della
BDA è presente una WORD contenente una serie di informazioni di
sistema relative all'hardware installato sul computer; come si ricava anche dal
manuale utente del BIOS, il significato di tale WORD è illustrato
in Figura 2.6.
In base a quanto esposto in Figura 2.6, l'istruzione:
mov ax, [es:O010h]
carica in AX la Equipment Information WORD della BDA (ovviamente,
ES=0040h)!
In alternativa al metodo appena illustrato, possiamo utilizzare la INT 11h del
BIOS; la chiamata di questo vettore di interruzione restituisce in AX
le identiche informazioni visibili in Figura 2.6.
2.4 La memoria CMOS
Prima che inizi la fase di boot per il lancio del SO, l'utente ha
la possibilità di premere un apposito tasto che gli permette di accedere al menu
di configurazione del BIOS; a seconda del modello di PC, il tasto
da premere può essere [Del] o [Canc] (assemblati), [F1] o
[F10] (HP/Compaq), [F2] (tasto standard) o un altro tasto
generalmente evidenziato mediante un messaggio sul monitor.
Il menu di configurazione del BIOS, presente solo sui PC di classe
AT o superiore, è riservato ad utenti piuttosto esperti; infatti, attraverso
tale menu è possibile modificare le impostazioni hardware del proprio PC!
Un esempio pratico riguarda la cosiddetta "sequenza di boot"; si tratta della
sequenza di memorie di massa (floppy disk, hard disk, CD, DVD) analizzate dal
BIOS alla ricerca del codice per il lancio del SO. L'utente ha la
possibilità di indicare al BIOS l'ordine esatto di scansione delle varie memorie
di massa; più avanti vedremo un esempio dettagliato relativo a questo importante
aspetto.
Quando usciamo dal menu di configurazione, il BIOS ci chiede se vogliamo
mantenere o meno le nuove impostazioni che abbiamo eventualmente selezionato; in
caso di risposta affermativa, la nuova configurazione verrà salvata in una apposita
memoria RAM denominata CMOS.
Questa particolare memoria è presente solo a partire dai PC di classe AT;
il suo nome deriva dal fatto che si tratta di una memoria RAM realizzata con i
flip flop in tecnologia CMOS.
Come abbiamo visto nella sezione Assembly Base, tale tecnologia viene impiegata
per la realizzazione di piccole memorie RAM ad alta velocità di accesso; in
particolare, la memoria CMOS occupava appena 64 byte sui primi PC
di classe AT ed è stata portata a 128 byte sui PC successivi.
La Figura 2.7 illustra lo schema a blocchi semplificato del circuito comprendente la
memoria CMOS e l'orologio in tempo reale o Real Time Clock (RTC)
del computer; le informazioni relative alla configurazione hardware vengono memorizzate
nella parte colorata in rosso.
Ad ogni riavvio del computer, il BIOS legge proprio le informazioni contenute
nella CMOS e le utilizza per la configurazione dell'hardware; a maggior ragione
quindi, è necessario evitare l'inserimento di informazioni errate in questa importante
memoria!
Il circuito di Figura 2.7 ha anche l'importante compito di aggiornare continuamente
l'orologio/calendario del computer; questo lavoro è svolto dal dispositivo chiamato
RTC che memorizza le informazioni nella apposita area della CMOS.
Ai pin +3V e Gnd viene collegata una batteria ricaricabile il cui scopo
è quello di permettere la conservazione delle informazioni anche a computer spento; è
molto importante quindi che tale batteria venga mantenuta permanentemente in stato di
carica (ciò si ottiene evitando che il PC rimanga spento per lunghissimi
periodi di tempo).
Il BIOS fornisce numerosi servizi finalizzati a leggere in modo sicuro le
informazioni presenti nella CMOS; a tale proposito, si veda il manuale utente
citato in precedenza.
Nei capitoli successivi, il circuito di Figura 2.7 verrà analizzato in dettaglio.
2.5 La sequenza di boot
L'ultima fase fondamentale svolta dal BIOS durante l'avvio del computer,
consiste in una chiamata alla INT 19h (System - Bootstrap loader) che
esegue la scansione delle varie memorie di massa (floppy disk, hard disk, CD,
DVD, etc) alla ricerca del codice per il lancio del SO; l'ordine seguito
dal BIOS per effettuare tale scansione, prende il nome di sequenza di
boot.
Sui vecchi PC, la sequenza comprende prima di tutto la scansione dei floppy
disk; se non viene trovato il codice relativo al lancio del SO, si passa
alla scansione degli hard disk. Se anche la scansione degli hard disk dà esito
negativo, il BIOS mostra un messaggio di errore sullo schermo!
Sui nuovi PC, alla sequenza appena descritta è stata aggiunta anche la
scansione di eventuali CD/DVD e persino di dispositivi USB, Iomega Zip e schede di
rete; come è stato spiegato in precedenza, la sequenza di boot può essere
alterata dall'utente attraverso il menu di configurazione del BIOS. In questo
modo si può chiedere al BIOS di iniziare la scansione dai dispositivi CD/DVD;
ciò rende possibile il boot direttamente da CD/DVD, come accade, ad esempio,
quando si deve installare Windows o quando si vuole eseguire una distribuzione
"live" di Linux!
Per capire il metodo seguito nella scansione delle varie memorie di massa,
analizziamo il caso dei floppy disk e degli hard disk; per i dettagli relativi ai
CD/DVD avviabili, si può consultare il documento specs-cdrom.pdf (Phoenix
IBM - "El Torito" Bootable CD-ROM Format Specification Version 1.0) scaricabile
da Internet.
In riferimento quindi ai floppy disk e agli hard disk, in seguito alla cosiddetta
formattazione, il BIOS suddivide fisicamente ogni superficie di un
disco in tanti cerchi concentrici denominati tracce; le varie tracce vengono
rappresentate con gli indici 0, 1, 2, etc, a partire da quella
più esterna.
Ogni traccia viene suddivisa in tante parti denominate settori; i vari settori
vengono rappresentati con gli indici 0, 1, 2, etc. Il settore
0, come vedremo più avanti, è riservato; i settori disponibili per la
lettura/scrittura dei file sono quindi quelli con indici 1, 2, 3,
etc.
In base alle considerazioni appena esposte, la superficie di ogni disco risulta
organizzata secondo lo schema mostrato in Figura 2.8.
Le varie coppie (traccia, settore) rappresentano una sorta di coordinate cartesiane
che permettono di accedere in modo casuale a qualsiasi settore del disco; come
sappiamo, il termine "accesso casuale" indica il fatto che il tempo di accesso ad
un qualsiasi settore scelto a caso, è costante e quindi indipendente dalla
posizione in cui si trova il settore stesso.
La lettura/scrittura di un disco avviene attraverso apposite testine magnetiche
denominate heads e rappresentate con gli indici 0, 1, 2,
etc; ogni faccia di un disco viene acceduta attraverso una delle testine magnetiche
presenti.
Nel caso dei floppy disk a doppia faccia (nel senso che entrambe le facce possono
memorizzare informazioni), sono presenti due testine, una per ogni faccia; quella
superiore viene denominata head 0, mentre quella inferiore viene denominata
head 1.
Gli hard disk sono costituiti da uno o più dischi a doppia faccia, sovrapposti tra
loro in modo da formare una pila; le varie testine magnetiche, una per ogni faccia,
vengono quindi denominate head 0, head 1, head 2, etc.
In un hard disk costituito da una pila di dischi a doppia faccia, le tracce aventi
lo stesso indice formano un cosiddetto cilindro; ad esempio, tutte le tracce
0 dei vari dischi che formano un hard disk, rappresentano il cilindro 0
(ciò vale anche per i floppy disk dove, ad esempio, il cilindro 0 è formato
dalla traccia 0 della faccia A e dalla traccia 0 della faccia
B).
I SO suddividono logicamente la struttura di Figura 2.8 in modo da ottenere un
cosiddetto file system; grazie al file system i programmi vedono un
disco, come quello di Figura 2.8, sotto forma di vettore lineare di celle.
Ogni cella può essere costituita da un numero di byte che, in genere, è multiplo di
512; ciò permette di velocizzare notevolmente le operazioni di I/O su
disco.
Il settore 0 relativo alla traccia 0, cilindro 0, head
0, faccia A di un disco (il primo disco, nel caso degli hard disk),
prende il nome di Master Boot Record (settore principale di boot) o
MBR e assume una importanza particolare; infatti, durante la sequenza
di boot, il BIOS controlla il MBR di ogni disco alla ricerca
di un eventuale bootloader.
Con il termine bootloader si indica un piccolo programma da 512 byte
che contiene il codice necessario per il lancio del SO; per identificare un
bootloader, il BIOS utilizza un meccanismo molto semplice che consiste
nel verificare se gli ultimi 2 byte contenuti in un MBR (da 512
byte) assumono il valore AA55h!
In caso affermativo, il BIOS legge questo blocco da 512 byte e lo
carica in memoria all'indirizzo fisico 07C00h a cui possiamo associare, ad
esempio, l'indirizzo logico 0000h:7C00h; a questo punto lo stesso BIOS
carica l'indirizzo logico 0000h:7C00h in CS:IP e cede il controllo
alla CPU.
La CPU esegue quindi le istruzioni contenute nel bootloader; tali
istruzioni, ovviamente, svolgono tutto il lavoro necessario per il caricamento in
memoria del SO!
Dalle considerazioni appena esposte, si intuisce che la scrittura di un piccolo
bootloader "didattico" non presenta alcuna difficoltà; in effetti, si tratta
di una esperienza molto interessante che ogni programmatore Assembly non può
fare a meno di provare.
2.5.1 Esempio di bootloader
Passiamo finalmente alla scrittura di un piccolo bootloader "didattico"; lo
scopo di questo bootloader è semplicemente quello di ricevere il controllo
dal BIOS, mostrare alcune informazioni diagnostiche e chiedere all'utente di
riavviare il computer.
Le considerazioni che seguono sono valide, sia che si stia usando un ambiente
DOS puro su un vecchio PC dotato di lettore per floppy disk, sia che
si stia usando il DOS su una macchina virtuale come VirtualBox; in
questo secondo caso, ovviamente il nostro bootloader verrà scritto su una
immagine virtuale di un floppy disk (più avanti vedremo i dettagli su come creare
un floppy disk virtuale).
Il primo aspetto da affrontare riguarda il fatto che, come è stato spiegato in
precedenza, il BIOS carica i 512 byte del bootloader in memoria,
all'indirizzo logico 0000h:7C00h; a questo punto, lo stesso BIOS cede
il controllo al nostro bootloader e ci lascia "soli con noi stessi"!
Non bisogna dimenticare, infatti, che in questa fase non esiste alcun SO
capace di fornirci i suoi servizi; una prima conseguenza di questo aspetto, è che gli
eventuali strumenti di cui possiamo aver bisogno, devono essere creati attraverso i
servizi del BIOS.
Supponiamo, ad esempio, di voler visualizzare sullo schermo il contenuto esadecimale
di un registro a 16 bit; a tale proposito, dobbiamo prima convertire il valore
esadecimale in una stringa. Infatti, come abbiamo visto in precedenza, la scheda
video viene inizializzata in modalità testo 80x25, per cui sullo schermo
possiamo visualizzare solamente simboli appartenenti al set dei codici
ASCII!
Una stringa formata esclusivamente da codici
ASCII di cifre numeriche
prende il nome di stringa numerica; si tratta in sostanza di una stringa
formata da una sequenza di caratteri del tipo '0', '1', '2',
etc.
Per convertire un numero esadecimale in una stringa numerica, si può utilizzare un
metodo semplicissimo; a tale proposito, creiamoci prima le seguenti stringhe:
Consideriamo ora il valore esadecimale 8CE9h contenuto in AX; copiamo
tale valore in BX e isoliamo il nibble meno significativo con l'istruzione:
and bx, 000Fh
In questo modo si ottiene, ovviamente, BX=0009h; osserviamo ora che:
byte [HexStr + bx] = byte [HexStr + 9] = '9'
Il carattere '9' viene copiato in [RegStr+3].
Facciamo scorrere ora il contenuto di AX di 4 bit verso destra; in
questo modo otteniamo AX=08CEh; copiamo tale valore in BX e isoliamo
il nibble meno significativo con l'istruzione:
and bx, 000Fh
In questo modo si ottiene, ovviamente, BX=000Eh; osserviamo ora che:
byte [HexStr + bx] = byte [HexStr + Eh] = byte [HexStr + 14] = 'E'
Il carattere 'E' viene copiato in [RegStr+2].
A questo punto appare evidente che, con due ulteriori passi, il numero esadecimale
8CE9h viene facilmente convertito nella stringa numerica "8CE9h"; la
procedura che esegue questo lavoro, presuppone che AX contenga il numero da
convertire e assume il seguente aspetto:
Una volta ottenuta la stringa numerica, possiamo visualizzarla attraverso il servizio
BIOS n.13h della INT 10h; in particolare, il programma presentato
più avanti visualizza la coppia CS:IP e il contenuto del registro DL che
rappresenta il codice del disco dal quale è avvenuto il boot.
Un altro importante aspetto da affrontare, riguarda l'indirizzamento dei vari dati
del nostro programma; a tale proposito, partiamo dal fatto che il BIOS
carica il bootloader in memoria a partire dall'indirizzo logico
0000h:7C00h, pone poi CS:IP=0000h:7C00h e infine cede il controllo
alla CPU.
Per l'indirizzamento del codice non c'è quindi alcun problema in quanto il
BIOS ha provveduto ad inizializzare correttamente CS:IP; il problema
si pone, invece, per l'indirizzamento dei dati.
Ciò accade in quanto, in assenza del SO, non viene effettuata alcuna
rilocazione della componente Seg che carichiamo in DS (o in ES)
per accedere ai dati; questo delicato lavoro spetta dunque al programmatore!
Assumiamo allora che il nostro bootloader sia contenuto in un eseguibile
in formato COM; come sappiamo, in tal caso il file eseguibile contiene
esclusivamente il codice macchina del programma (ed è proprio ciò che vogliamo).
L'unico segmento di programma presente, sarà quindi caricato in memoria a partire
dall'indirizzo logico 0000h:7C00h; tale indirizzo logico corrisponde
all'indirizzo fisico, multiplo di 16:
0000h * 10h + 7C00h = 00000h + 7C00h = 07C00h
Come sappiamo, in ambiente DOS i primi 256 byte del segmento unico di
un programma COM devono essere riservati al PSP; nel nostro caso, non
esiste alcun DOS, per cui possiamo posizionare l'entry point dove
vogliamo!
Una prima soluzione consiste allora nell'aprire il segmento unico di programma con
la direttiva:
org 7C00h
In questo caso, sappiamo che l'assembler genera un eseguibile dove tutte le componenti
offset dei dati risulteranno sommate a 7C00h; a questo punto, per accedere
correttamente ai dati stessi, non dobbiamo fare altro che caricare 0000h in
DS.
La seconda soluzione parte dal presupposto che l'indirizzo fisico 07C00h
corrisponde all'indirizzo logico normalizzato 07C0h:0000h; ciò significa
che l'indirizzo logico 07C0h:0000h è perfettamente equivalente all'indirizzo
logico 0000h:7C00h!
Possiamo aprire allora il segmento unico di programma con la direttiva:
org 0000h
In questo caso, sappiamo che l'assembler genera un eseguibile dove tutte le
componenti offset dei dati risulteranno sommate a 0000h; a questo punto,
per accedere correttamente ai dati stessi, non dobbiamo fare altro che caricare
07C0h in DS.
Un ulteriore aspetto interessante riguarda l'eventualità di voler sapere se il
valore assunto da IP all'entry point sia veramente 7C00h;
a tale proposito, possiamo servirci del seguente codice:
Osserviamo che nell'esempio, la CALL (diretta intrasegmento) si trova
all'offset 000Eh; questa istruzione salva nello stack l'indirizzo di ritorno
0011h e salta a CS:0011h. Ma l'indirizzo di ritorno 0011h non è
altro che il valore da caricare in IP per la prossima istruzione da eseguire;
tale valore viene quindi estratto dallo stack e salvato in AX (ovviamente, la
POP ha anche lo scopo di sostituire l'istruzione RETN). Ad AX
dobbiamo ora sottrarre l'offset di get_ip che vale 0011h.
Quando il programma è in fase di esecuzione, la CALL provoca un salto a:
CS:IP = 0000h:(0011h + 7C00h) = 0000h:7C11h
L'indirizzo di ritorno salvato nello stack è quindi 7C11h; di conseguenza,
l'istruzione SUB produce:
AX = 7C11h - 0011h = 7C00h
Analizziamo infine gli aspetti relativi all'uscita dal programma; la soluzione più
semplice consiste nel chiedere all'utente di togliere il floppy disk e riavviare
il computer con la sequenza di tasti:
[Ctrl] + [Alt] + [Canc]
Esiste però una soluzione più elegante che consiste in un riavvio automatico
attraverso un salto FAR a FFFFh:0000h. Come sappiamo, nei vecchi
PC a tale indirizzo è presente un salto FAR alla prima istruzione
eseguibile del POST che provoca il reset della CPU con conseguente
riavvio (reboot) del computer; nei moderni PC, per compatibilità,
in FFFFh:0000h si trovano le istruzioni che provocano il riavvio o lo
spegnimento del computer.
Nel caso generale, prima di effettuare il salto FAR, i SO
inseriscono un apposito codice a 16 bit (POST reset flag) in
un'area della BDA che si trova all'indirizzo logico 0040h:0072h;
sono disponibili i seguenti codici:
Nel caso del nostro bootloader, utilizziamo il codice 1234h per un
warm reboot (che equivale alla pressione dei tasti [Ctrl]+[Alt]+[Canc]);
il codice 0000h (cold boot) permette, invece, di spegnere il computer
(ovviamente, solo per i PC che supportano lo spegnimento via software).
A questo punto abbiamo chiarito tutti i dettagli relativi al nostro bootloader;
il conseguente listato è illustrato in Figura 2.10.
Come si vede in Figura 2.10, per l'accesso ai dati è stato scelto l'indirizzo logico
di riferimento 07C0h:0000h (perfettamente equivalente a 0000h:7C00h);
di conseguenza, la direttiva ORG deve specificare il parametro 0000h
(nessuna traslazione in avanti degli offset).
Il paragrafo 07C0h viene quindi caricato in DS, ES e SS
(NEARSTACK); il registro SP viene inizializzato con il valore massimo
possibile FFFEh (nessuno ce lo impedisce).
Come è stato già spiegato in precedenza, in assenza del DOS non possiamo
contare sui servizi delle varie ISR installate da tale SO; proprio
per questo motivo, dobbiamo rivolgerci ai servizi del BIOS. La procedura
WRITE_STRING usa il servizio BIOS n.13h della INT 10h
per visualizzare una stringa; la procedura WAIT_CHAR attende la pressione di un
tasto grazie al seguente servizio BIOS n.00h della INT 16h
(Keyboard services):
Analizzando il listing file del programma di Figura 2.10, si può notare che
siamo all'interno dei 512 byte; se oltrepassiamo l'offset massimo 510
(dopo il quale c'è il codice AA55h), otteniamo un BootLoader che non funzionerà!
Proprio per questo motivo, i moderni bootloader non fanno altro che caricare
in memoria un ulteriore programma di dimensioni più consistenti; tale programma,
non avendo problemi di spazio, può così effettuare tutte le necessarie inizializzazioni
del SO.
2.5.2 Generazione dell'eseguibile
Per l'eseguibile di un bootloader conviene decisamente orientarsi sul formato
COM; infatti, sappiamo che per tale formato viene generato il solo codice
macchina, senza alcun inutile header che finirebbe anche per alterare le dimensioni
(512 byte) del file eseguibile.
Con il MASM dobbiamo ottenere un file eseguibile in formato COM, i comandi per farlo sono i seguenti:
Il linker mostra un paio di warning per indicare che l'eseguibile è stato
generato in formato COM e l'entry point non si trova all'offset
0100h del segmento unico _TEXT.
Nel caso di TASM siamo obbligati ad usare LINK.EXE
di MASM; perché il linker di TASM non permette di creare un file in formato COM
con un Entry Point in una posizione diversa dall'offset 0100h!
Nel caso di NASM invece possiamo evitare l'uso del linker e ottenere direttamente
l'eseguibile finale (formato binario) attraverso il comando:
nasm -f bin bootload.asm -o bootload.bin
(il file bootload.asm si trova nella sezione Codice sorgente di esempio per la sezione assembly avanzato dell’
Area Downloads di questo sito).
Osservando il file BOOTLOAD.BIN così ottenuto possiamo notare che le sue
dimensioni sono pari esattamente a 512 byte!
2.5.3 Installazione del bootloader
Come è stato ampiamente precisato in precedenza, il bootloader presentato
in Figura 2.10 è destinato ad essere installato nel MBR di un floppy disk;
chi intende servirsi del proprio hard disk, in un vero ambiente DOS, si assume
tutte le responsabilità sulle possibili conseguenze!
Nel nostro caso, la situazione si presenta esente da rischi in quanto stiamo facendo
riferimento al DOS eseguito sotto VirtualBox; il bootloader che
abbiamo realizzato verrà quindi installato in una immagine virtuale di un floppy disk.
Procediamo allora alla creazione del nostro floppy disk virtuale vuoto, che chiameremo
emptyfd.img; in ambiente Linux, assicurandosi di aver installato il
package dosfstools, da root bisogna impartire i comandi:
In ambiente Windows si ottiene lo stesso risultato usando programmi gratuiti
come MagicISO o WinImage; in alternativa, si può scaricare da questo
sito una immagine già pronta di
emptyfd.img.
Per l'installazione del bootloader ci serviremo del comando W (write)
nel programma DEBUG (già illustrato nel Capitolo 14 della sezione Assembly
Base) fornito dal DOS; la sintassi di tale comando è la seguente:
w address drive sector number
- address indica l'indirizzo da cui leggere il blocco sorgente
- drive indica l'unità su cui scrivere i dati
- sector indica il settore iniziale di scrittura
- number indica il numero di blocchi da 512 byte da scrivere
Per quanto riguarda il drive, abbiamo visto in precedenza che il BIOS
assegna il codice 00h al primo lettore floppy disk e 01h al secondo
lettore floppy disk; analogamente, il codice 80h indica il primo hard disk e
81h indica il secondo hard disk.
Il settore iniziale di scrittura è ovviamente il n.0; il numero di blocchi
da 512 byte che dobbiamo scrivere è 1.
In relazione all'indirizzo da cui leggere il blocco sorgente, bisogna ricordare
che DEBUG carica i programmi in formato COM a partire dall'indirizzo
CS:0100h; il valore da caricare in CS viene scelto dallo stesso
DEBUG e non deve essere assolutamente alterato!
Posizionandoci allora nella directory di lavoro dove si trova BOOTLOAD.COM
(ad esempio, C:\MASM\ASMAVAN), impartiamo il comando:
debug bootload.com
Come verifica possiamo impartire il comando u (unassemble) che ci mostrerà
il disassemblato delle prime istruzioni del nostro programma; in questo modo si
può anche constatare che il programma stesso è stato caricato in memoria a partire
dall'indirizzo logico CS:0100h.
A questo punto, in un vero ambiente DOS inseriamo un floppy disk nel lettore,
mentre sotto VirtualBox utilizziamo il menu Dispositivi - Lettori floppy -
Scegli immagine del disco ... per selezionare il nostro floppy disk virtuale
emptyfd.img; da DEBUG impartiamo ora il comando:
w 100 0 0 1
(per il primo floppy disk)
Il nostro bootloader è ora installato nel MBR del primo floppy disk;
infatti, riavviando il computer con il floppy inserito, si può constatare (Figura
2.12) che il BIOS cede il controllo al programma BOOTLOAD.COM e non al
DOS!
Come indica la Figura 2.12, per uscire da BOOTLOAD.COM basta rimuovere il
floppy disk (in VirtualBox bisogna utilizzare il menu Dispositivi -
Lettori floppy - Rimuovi disco dal lettore virtuale) e premere un tasto
qualunque.
Bibliografia
IA-32 Intel Architecture Software Developer's Manual - Volume 3: System Programming Guide
(24547212.pdf)
PhoenixBIOS 4.0 Revision 6 User's Manual
(userman.pdf)
PhoenixBIOS 4.0 Release 6.0 POST Tasks and Beep Codes
(biospostcode.pdf)
Phoenix Technologies, Ltd. AwardBIOS Version 4.51PG - Post Codes & Error Messages
(biosawardpostcode.pdf)
Phoenix IBM - "El Torito" Bootable CD-ROM Format Specification Version 1.0
(specs-cdrom.pdf)
Ralf Brown's Interrupt List