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: 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
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