Assembly Base con NASM
Capitolo 9: La memoria dal punto di vista del programmatore
In un precedente capitolo è stato detto che un insieme di dati e istruzioni, nel loro
insieme formano un programma; nel caso più semplice e intuitivo quindi, un programma
da eseguire con il computer viene suddiviso in due blocchi fondamentali chiamati:
- Blocco Dati
- Blocco Codice
Il blocco dati è un'area riservata ai dati in input del programma, ai dati temporanei
e ai dati che scaturiscono dalle varie elaborazioni (dati in output); il blocco codice
è, invece, un'area riservata alle istruzioni che devono elaborare i dati stessi.
Un programma, per poter essere eseguito, deve essere prima caricato nella memoria
RAM del computer; in questo modo, la CPU può accedere rapidamente al
programma stesso e può quindi gestire al meglio la fase di elaborazione.
Per svolgere il proprio lavoro, la CPU ha bisogno di conoscere una serie di
informazioni sul programma da eseguire; in particolare, la CPU ha bisogno di
sapere: da quale indirizzo parte il blocco codice, da quale indirizzo parte il blocco
dati, qual è l'indirizzo della prossima istruzione da eseguire e qual è l'indirizzo
degli eventuali dati a cui l'istruzione da eseguire fa riferimento. Tutte queste
informazioni vengono gestite dalla CPU attraverso i propri registri interni;
come è stato detto nel precedente capitolo, per poter ottenere le massime prestazioni
possibili, la CPU si serve di registri di tipo parallelo.
Grazie ai propri registri interni la CPU ha, istante per istante, il controllo
totale sul programma in esecuzione; la Figura 9.1 illustra lo schema tipico attraverso
il quale una generica CPU gestisce l'elaborazione di un programma in memoria.
In questa figura, il blocco colorato in giallo rappresenta la RAM del computer;
come si può notare, la memoria è disposta dal basso (inizio RAM) verso l'alto
(fine RAM). All'interno della RAM è presente il programma da eseguire;
la parte colorata in verde rappresenta il blocco codice, mentre la parte colorata in
celeste rappresenta il blocco dati.
Per poter avere il pieno controllo su questo programma, la CPU memorizza tutte
le necessarie informazioni nei suoi registri interni; la Figura 9.1 illustra, in
particolare, la gestione delle informazioni fondamentali riguardanti il blocco codice
e il blocco dati. Il metodo più semplice ed efficiente che permette alla CPU
di gestire questa situazione, consiste nel rappresentare un qualsiasi indirizzo sotto
forma di una coppia Base, Offset, che viene indicata convenzionalmente
con il simbolo Base:Offset; questo concetto viene illustrato dalla Figura 9.2 e
assume una importanza capitale nel campo della programmazione dei computer.
Come si può notare, il termine Base indica l'indirizzo della cella di memoria
da cui inizia un determinato blocco di programma (codice o dati); si tratta chiaramente
di un indirizzo assoluto, calcolato cioè rispetto all'inizio della RAM.
Il termine Offset indica, invece, l'indirizzo di una cella di memoria interna
allo stesso blocco di programma; questo indirizzo viene chiaramente calcolato rispetto
a Base ed è quindi un indirizzo relativo, che in inglese viene appunto
chiamato offset (spiazzamento). La componente Offset ci permette di
muoverci agevolmente all'interno del blocco di programma che vogliamo indirizzare;
possiamo paragonare quindi Offset all'indirizzo di una sorta di cursore che
si sposta all'interno di un blocco di programma.
Dalle considerazioni appena esposte si deduce facilmente che una coppia
Base:Offset rappresenta univocamente la cella di memoria che si trova
all'indirizzo fisico:
Base + Offset
Ogni volta che il programmatore specifica nei propri programmi un indirizzo
Base:Offset, la CPU calcola la somma tra Base e Offset,
ottenendo così un indirizzo fisico da caricare sull'Address Bus; come è stato
detto nel precedente capitolo, questo indirizzo non è altro che l'indice di una cella
appartenente al vettore che forma la RAM.
Sulla base di queste considerazioni, osserviamo in Figura 9.1 che in uno dei registri della
CPU, viene memorizzato l'indirizzo della RAM da cui inizia il blocco codice;
questo indirizzo è stato chiamato simbolicamente BaseCodice (indirizzo di partenza
del blocco codice).
In un altro registro della CPU, viene memorizzato l'indirizzo della RAM in
cui si trova la prossima istruzione da eseguire; questo indirizzo è stato chiamato
simbolicamente OffsetCodice (spiazzamento all'interno del blocco codice).
Mentre BaseCodice è un indirizzo fisso, OffsetCodice viene continuamente
aggiornato dalla logica di controllo della CPU; infatti, nel momento in cui la
CPU sta eseguendo una istruzione, OffsetCodice viene posizionato sulla
prossima istruzione da eseguire.
Supponiamo, ad esempio, di avere una CPU con Address Bus a 16 bit
e supponiamo inoltre che il BaseCodice del programma in esecuzione sia espresso
dal valore esadecimale 0AB2h; tutto ciò significa che il blocco codice del
programma inizia dalla cella n. 0AB2h della RAM. Se l'OffsetCodice
della prossima istruzione da eseguire è 00F4h, allora l'indirizzo effettivo di
questa istruzione è dato da:
0AB2h + 00F4h = 0BA6h
Possiamo dire quindi che il codice macchina della prossima istruzione da eseguire, inizia
dalla cella n. 0BA6h della RAM; per poter accedere a questa cella, la
CPU deve caricare l'indirizzo fisico 0BA6h sull'Address Bus.
In definitiva, la somma:
BaseCodice + OffsetCodice
fornisce alla CPU l'indirizzo effettivo della cella della RAM da cui inizia il
codice macchina della prossima istruzione da eseguire; possiamo paragonare OffsetCodice
all'indirizzo di una sorta di cursore che si muove all'interno del blocco codice del programma
in esecuzione.
Tornando alla Figura 9.1, possiamo notare che in un altro registro della CPU viene
memorizzato l'indirizzo della RAM da cui inizia il blocco dati; questo indirizzo è
stato chiamato simbolicamente BaseDati (indirizzo di partenza del blocco dati) ed ha
lo stesso significato del BaseCodice.
In un ulteriore registro della CPU, viene memorizzato l'indirizzo della RAM
in cui si trova un eventuale dato a cui fa riferimento l'istruzione da eseguire; questo
indirizzo è stato chiamato simbolicamente OffsetDati (spiazzamento all'interno del
blocco dati) ed ha lo stesso significato di OffsetCodice.
Anche in questo caso quindi, mentre BaseDati è un indirizzo fisso, OffsetDati
può essere continuamente aggiornato attraverso le istruzioni del programma in esecuzione; in
questo modo possiamo indicare alla CPU l'indirizzo del dato a cui vogliamo accedere.
Riprendendo il precedente esempio, consideriamo un BaseDati del programma in esecuzione,
espresso in esadecimale dall'indirizzo 0DF0h; tutto ciò significa che il blocco dati
del programma inizia dalla cella n. 0DF0h della RAM. Se la prossima istruzione
da eseguire fa riferimento ad un dato con OffsetDati pari a 00C8h, allora
l'indirizzo effettivo di questo dato è:
0DF0h + 00C8h = 0EB8h
Possiamo dire quindi che il prossimo dato da elaborare inizia dalla cella n. 0EB8h
della RAM; per poter accedere a questa cella, la CPU deve caricare l'indirizzo
fisico 0EB8h sull'Address Bus.
In definitiva, la somma:
BaseDati + OffsetDati
fornisce alla CPU l'indirizzo effettivo della cella della RAM da cui inizia
il contenuto del prossimo dato da elaborare; possiamo paragonare OffsetDati
all'indirizzo di una sorta di cursore che si muove all'interno del blocco dati del programma
in esecuzione.
La tecnica di indirizzamento appena esposta viene definita lineare (in inglese si
usa anche il termine flat); questo termine è riferito al fatto che il programmatore
vede la RAM come un vettore di celle, cioè come una sequenza lineare di celle
consecutive e contigue. Come abbiamo visto nel precedente capitolo, questo è esattamente
lo stesso punto di vista della CPU; si tratta quindi di una situazione ottimale,
che permette al programmatore di accedere alla RAM nel modo più semplice e
intuitivo. Ad ogni coppia Base:Offset specificata dal programmatore, corrisponde
una e una sola cella fisicamente presente nella RAM; analogamente (una volta
fissato l'indirizzo Base), ad ogni cella fisicamente presente nella RAM
corrisponde una e una sola coppia Base:Offset.
Appare evidente il fatto che, per poter gestire nel modo più efficiente possibile un
indirizzamento di tipo lineare, l'ideale sarebbe avere appositi registri interni della
CPU con una ampiezza in bit sufficiente per contenere un qualunque indirizzo di memoria;
in sostanza, la CPU con Address Bus a 16 bit dell'esempio precedente,
dovrebbe essere dotata di registri interni (di indirizzamento) almeno a 16 bit. Più in
generale, come sappiamo, un Address Bus a n bit è in grado di contenere un
indirizzo di memoria compreso tra 0 e 2n-1, per un totale di
2n celle di memoria; per gestire nel modo più efficiente possibile
l'indirizzamento lineare di tutte queste celle fisicamente presenti nella RAM, la
CPU dovrebbe essere dotata di registri interni (di indirizzamento) almeno a n
bit.
In effetti, questo è proprio ciò che accade in molte piattaforme hardware; un esempio pratico
è rappresentato dai vecchi computer Apple Macintosh equipaggiati con le CPU della
Motorola.
9.1 La modalità di indirizzamento reale 8086
La modalità di indirizzamento lineare appena descritta, è un vero e proprio paradiso
per il programmatore; in questa modalità, infatti, il programmatore si trova a gestire
nei propri programmi, indirizzi che fanno riferimento diretto a celle fisicamente
presenti nel vettore della RAM.
Nel caso però della piattaforma hardware basata sulle CPU della famiglia
80x86, la situazione è purtroppo ben diversa da quella appena descritta; per
rendercene conto, partiamo dal caso della CPU 8080 che può essere definita
come "l'origine di tutti i mali".
In un precedente capitolo abbiamo visto che l'8080 è dotata di Address Bus
a 16 linee, Data Bus a 8 linee e quindi anche registri interni a
8 bit; la domanda che sorge spontanea è: come facciamo a gestire indirizzi fisici
a 16 bit attraverso i registri a 8 bit?
Con un Address Bus a 16 linee l'8080 può vedere al massimo
216=65536 byte di RAM; complessivamente, abbiamo quindi
65536 celle di memoria, i cui indirizzi sono compresi (in esadecimale) tra
0000h e FFFFh. Per poter gestire nei nostri programmi questi indirizzi
a 16 bit, abbiamo la necessità di disporre di registri a 16 bit;
l'8080 ci mette però a disposizione registri interni a 8 bit, attraverso
i quali possiamo rappresentare solo gli indirizzi compresi tra 00h e FFh.
Per risolvere questo problema, i progettisti della Intel hanno fatto in modo che
i registri interni a 8 bit dell'8080 possano essere usati anche in coppia;
in questo modo, abbiamo a disposizione registri da 8+8 bit, attraverso i quali
possiamo gestire indirizzi lineari a 16 bit.
Riassumendo quindi, l'8080 ha a disposizione una RAM da 65536 byte
(64 KiB); un programma da eseguire viene caricato in una porzione di questi
64 KiB e grazie alle coppie di registri da 8+8 bit, può essere indirizzato
attraverso la tecnica mostrata in Figura 9.1.
Nel caso dell'8080 quindi, i progettisti della Intel hanno risolto il
problema in modo relativamente semplice; bisogna anche tenere presente che quando
comparve questa CPU (primi anni 70), 64 KiB di RAM erano
una enormità e l'utilizzo di una architettura a 16 bit avrebbe comportato
complicazioni circuitali eccessive e costi di produzione proibitivi.
I problemi di indirizzamento dell'8080 erano però ben poca cosa rispetto a
quello che stava per succedere nel giro di pochi anni; nell'Agosto del
1981, infatti, inizia ufficialmente l'era dei Personal Computer
(PC). In quella data, la IBM mette in commercio un rivoluzionario
computer chiamato PC IBM XT (la sigla XT sta per eXtended
Technology); si tratta del primo computer destinato, non solo a centri di ricerca,
università, enti militari, etc, ma anche al mercato di fascia medio bassa rappresentato
dalle piccole e medie aziende e da semplici utenti.
In poco tempo, l'XT diventa un vero e proprio standard nel mondo del computer,
tanto che negli anni successivi, dopo memorabili battaglie giudiziarie, viene affiancato
da una serie di cloni prodotti da numerose altre aziende; per rappresentare tutta questa
famiglia di cloni dell'XT, viene coniata in quegli anni la famosa definizione di
PC IBM XT compatibili.
La CPU che ha dominato il mondo degli XT compatibili è stata sicuramente
la Intel 8086; la Figura 9.3 mostra la piedinatura del chip che racchiude
questa CPU.
Osservando la Figura 9.3 balza subito agli occhi il fatto che l'8086 è predisposta
per un Address Bus a 20 linee (da AD0 a AD15 e da A16/S3
a A19/S6); con un Address Bus a 20 linee, l'8086 può vedere
sino a 220=1048576 byte (1 MiB) di memoria fisica. Questo aspetto
suscitò all'epoca grande scalpore, tanto da far nascere un acceso dibattito tra gli esperti,
che si interrogarono sulla effettiva utilità di "così tanta" memoria RAM!
L'8086 è una CPU con architettura a 16 bit e dispone quindi di un
Data Bus a 16 linee e di registri interni a 16 bit; osservando la
Figura 9.3 si nota che per la connessione al Data Bus vengono sfruttate le prime
16 linee dell'Address Bus (da AD0 a AD15). L'Address
Bus, una volta che ha trasmesso alla RAM l'indirizzo della cella a cui dobbiamo
accedere, ha praticamente esaurito il proprio lavoro; a questo punto la CPU può
sfruttare i terminali da AD0 a AD15 per connettersi al Data Bus.
Per l'8086 si pone lo stesso interrogativo dell'8080: come facciamo a gestire
indirizzi fisici a 20 bit attraverso i registri a 16 bit?
In base a quanto è stato detto in precedenza per l'8080, si potrebbe pensare che
sull'8086 sia possibile utilizzare in coppia i registri da 16 bit; i
progettisti della Intel hanno escogitato, invece, una soluzione ben più contorta
che ha avuto colossali ripercussioni su tutta la famiglia delle CPU 80x86.
Osserviamo innanzi tutto che, come è stato già detto, con un Address Bus a
20 linee l'8086 può gestire 220=1048576 byte (1
MiB) di memoria fisica; complessivamente, abbiamo quindi 1048576 celle di memoria,
i cui indirizzi sono compresi (in esadecimale) tra 00000h e FFFFFh. Per
poter gestire nei nostri programmi questi indirizzi a 20 bit, abbiamo la necessità
di disporre di registri a 20 bit; l'8086 ci mette però a disposizione
registri interni a 16 bit, attraverso i quali possiamo rappresentare solo gli
indirizzi compresi tra 0000h e FFFFh, per un totale di 65536 byte
(64 KiB) di RAM.
Per risolvere questo problema, i progettisti della Intel hanno deciso di suddividere
"idealmente" la RAM, in tanti blocchi da 64 KiB ciascuno, chiamati
segmenti di memoria; per indirizzare i vari segmenti di memoria, è stato inoltre
introdotto il concetto fondamentale di indirizzo logico. Un indirizzo logico, è
formato da una coppia indicata simbolicamente con Seg:Offset; il significato di questa
coppia è formalmente simile a quello delle coppie Base:Offset. La componente
Seg è un valore a 16 bit che indica il segmento di memoria a cui vogliamo
accedere; con 16 bit possiamo specificare un qualunque segmento di memoria compreso
tra 0000h e FFFFh. La componente Offset è un valore a 16 bit
che indica uno spiazzamento all'interno del segmento di memoria a cui vogliamo accedere;
con 16 bit possiamo specificare un qualunque spiazzamento compreso tra 0000h
e FFFFh.
Seguendo allora un ragionamento logico, possiamo dedurre che i 1048576 byte di
RAM indirizzabili dall'8086 vengono suddivisi in:
1048576 / 65536 = 220 / 216 = 220 - 16 = 24 = 16 segmenti di memoria.
Volendo accedere allora alla trentesima cella del quarto segmento di memoria, dobbiamo
specificare semplicemente l'indirizzo logico 3:29 (in esadecimale,
0003h:001Dh); le deduzioni logiche appena esposte, sono però completamente
sbagliate!
Alla Intel hanno notato che con la componente Seg a 16 bit è possibile
specificare teoricamente uno tra 65536 possibili segmenti di memoria (da 0000h
a FFFFh); analogamente, con la componente Offset a 16 bit è possibile
specificare uno tra 65536 possibili spiazzamenti (da 0000h a FFFFh).
In sostanza, attraverso le coppie Seg:Offset possiamo gestire 65536 segmenti
di memoria, ciascuno dei quali è formato da 65536 byte; complessivamente quindi, con
questo sistema è possibile indirizzare ben:
65536 * 65536 = 216 * 216 = 216 + 16 = 232 = 4294967296 byte = 4 GiB di memoria RAM!
Nei primi anni 80 però i GiB esistevano solo nei film di fantascienza; alla
Intel si misero allora al lavoro per escogitare un sistema che permettesse di
indirizzare con le coppie logiche Seg:Offset, unicamente 1 MiB di RAM.
Alla fine, saltò fuori un'idea a dir poco cervellotica che, come è stato già detto, ha
avuto pesanti ripercussioni sulla architettura di tutte le CPU della famiglia
80x86.
In base all'idea scaturita dalle menti contorte dei progettisti della Intel,
con le coppie Seg:Offset è possibile rappresentare effettivamente 65536
segmenti di memoria differenti, ciascuno dei quali è formato da 65536 byte; la
novità importante è data però dal fatto che ciascun segmento di memoria, viene fatto
partire da un distinto indirizzo fisico che deve essere un multiplo intero di 16
(cioè, 0, 16, 32, 48, 64, etc). In questo modo si
viene a creare la situazione illustrata in Figura 9.4; questa figura si riferisce in
particolare ai primi 12 segmenti di memoria della RAM.
Nella terminologia usata dalla Intel, un blocco di memoria da 16 byte
viene definito paragrafo; è importante anche osservare che il valore
16, in esadecimale si scrive 10h e che il valore 65535, in
esadecimale si scrive FFFFh. Tenendo conto di questi aspetti, possiamo notare
in Figura 9.4 che:
- il segmento di memoria n. 0000h parte dall'indirizzo fisico 00000h e
termina all'indirizzo fisico:
00000h + 0FFFFh = 0FFFFh
il segmento di memoria n. 0001h parte dall'indirizzo fisico 00010h e
termina all'indirizzo fisico:
00010h + 0FFFFh = 1000Fh
il segmento di memoria n. 0002h parte dall'indirizzo fisico 00020h e
termina all'indirizzo fisico:
00020h + 0FFFFh = 1001Fh
il segmento di memoria n. 0003h parte dall'indirizzo fisico 00030h e
termina all'indirizzo fisico:
00030h + 0FFFFh = 1002Fh
e così via.
Ciascun segmento di memoria parte quindi da un distinto indirizzo fisico multiplo
intero di 16; per esprimere questa situazione, si dice in gergo che i vari
segmenti di memoria sono paragraph aligned (allineati al paragrafo).
Osserviamo ora che:
1048576 / 16 = 220 / 24 = 220 - 4 = 216 = 65536
I 1048576 byte della RAM contengono quindi 65536 paragrafi,
ciascuno dei quali segna l'inizio di un nuovo segmento di memoria; ciò conferma
che con questo sistema è possibile avere fisicamente 65536 segmenti di
memoria differenti. Da queste considerazioni si deduce quindi che l'ultimo segmento
di memoria è il n. 65535 (cioè, il n. FFFFh) e che il suo indirizzo
fisico iniziale è FFFF0h.
Lo schema descritto in Figura 9.4 presenta il pregio di permettere alla CPU di
convertire con estrema facilità un indirizzo logico Seg:Offset in un indirizzo
fisico a 20 bit; per capire come avviene questa conversione, è sufficiente osservare
in Figura 9.4 che un generico segmento di memoria n. XXXXh parte dall'indirizzo fisico
XXXX0h. Questo indirizzo fisico si ottiene quindi moltiplicando XXXXh per
10h (cioè per 16); al risultato XXXX0h bisogna poi sommare la componente
Offset e il gioco è fatto. Dato quindi l'indirizzo logico Seg:Offset, il
corrispondente indirizzo fisico a 20 bit si ricava dalla formula:
(Seg * 16) + Offset
Supponiamo, ad esempio, di avere l'indirizzo logico 0C2Fh:AB32h; questo indirizzo logico
rappresenta la cella n. AB32h del segmento di memoria n. 0C2Fh. Il corrispondente
indirizzo fisico a 20 bit è dato allora da:
(0C2Fh * 10h) + AB32h = 0C2F0h + AB32h = 16E22h
Moltiplicare un numero esadecimale per 16, cioè per 161, equivale
come sappiamo a far scorrere di un posto verso sinistra tutte le sue cifre; il posto rimasto
libero a destra viene riempito con uno 0. Analogamente, tenendo presente che la
CPU lavora con il codice binario, moltiplicare un numero binario per 16, cioè
per 24, equivale come sappiamo a far scorrere di quattro posti verso
sinistra tutte le sue cifre; i quattro posti rimasti liberi a destra vengono riempiti con
quattro 0. In definitiva, la conversione di un indirizzo logico Seg:Offset
espresso in binario, in un indirizzo fisico a 20 bit, comporta uno shift di
4 posti verso sinistra delle cifre di Seg e una successiva somma con
Offset; come vedremo nel prossimo capitolo, la CPU dispone di un apposito
circuito, che converte ad altissima velocità un indirizzo logico Seg:Offset in un
indirizzo fisico a 20 bit.
Lo schema di segmentazione della RAM illustrato in Figura 9.4 presenta anche
alcuni inconvenienti; un evidente inconveniente è rappresentato dal fatto che, a
differenza di quanto accade con lo schema di indirizzamento di tipo lineare,
l'indirizzamento di tipo logico non garantisce più la corrispondenza biunivoca
tra indirizzi logici Seg:Offset e indirizzi fisici della RAM. Nel
precedente esempio, abbiamo visto che l'indirizzo logico 0C2Fh:AB32h è
associato univocamente all'indirizzo fisico 16E22h; viceversa, l'indirizzo
fisico 16E22h può essere rappresentato con differenti coppie
Seg:Offset. Per rendercene conto, possiamo osservare in Figura 9.4 che questa
situazione è legata al fatto che i vari segmenti di memoria sono parzialmente
sovrapposti tra loro; ne consegue che, ad esempio, l'indirizzo fisico 00058h
(colorato in rosso in Figura 9.4), è rappresentabile con ben 6 coppie
Seg:Offset differenti e cioè:
0005h:0008h
0004h:0018h
0003h:0028h
0002h:0038h
0001h:0048h
0000h:0058h
Un altro serio inconveniente dello schema di segmentazione della RAM illustrato
in Figura 9.4, è rappresentato dal fatto che, come molti avranno intuito, gli ultimi
segmenti di memoria sono incompleti; questi segmenti hanno cioè una dimensione inferiore
a 65536 byte. Per rendercene conto, osserviamo la Figura 9.5 che mostra gli ultimi
11 segmenti di memoria della RAM.
Per capire bene questo problema, bisogna ricordare che la 8086, a causa
dell'Address Bus a 20 linee, è in grado di vedere solo 100000h
byte di memoria (da 00000h a FFFFFh); come vedremo in seguito, questa
limitazione vale per qualsiasi 80x86 in emulazione 8086 (anche in
presenza di più di 1 MiB di RAM).
Cominciamo dall'ultimo segmento di memoria, cioè dal segmento n. FFFFh;
all'interno di questo segmento possiamo accedere ai vari byte attraverso gli
indirizzi logici FFFFh:0000h, FFFFh:0001h, FFFFh:0002h e
così via, sino ad arrivare a FFFFh:000Fh. Giunti a questo punto, possiamo
constatare che l'indirizzo logico FFFFh:000Fh corrisponde all'indirizzo
fisico:
(FFFFh * 10h) + 000Fh = FFFF0h + 000Fh = FFFFFh
Ma FFFFFh non è altro che l'indirizzo fisico dell'ultima cella della
RAM; tutto ciò significa che l'ultimo segmento di memoria, cioè il n.
FFFFh, è formato da appena 16 byte (10h byte)!
Passiamo ora al penultimo segmento di memoria, cioè al segmento n. FFFEh;
all'interno di questo segmento possiamo accedere ai vari byte attraverso gli
indirizzi logici FFFEh:0000h, FFFEh:0001h, FFFEh:0002h e
così via, sino ad arrivare a FFFEh:001Fh. Giunti a questo punto, possiamo
constatare che l'indirizzo logico FFFEh:001Fh corrisponde all'indirizzo
fisico:
(FFFEh * 10h) + 001Fh = FFFE0h + 001Fh = FFFFFh
Ma FFFFFh non è altro che l'indirizzo fisico dell'ultima cella della
RAM; tutto ciò significa che il penultimo segmento di memoria, cioè il
n. FFFEh, è formato da appena 32 byte (20h byte)!
A questo punto la situazione appare chiara, per cui si può facilmente intuire che, il
terzultimo segmento di memoria (FFFDh) è formato da soli 48 byte (30h
byte), il quartultimo segmento di memoria (FFFCh) è formato da soli 64 byte
(40h byte), il quintultimo segmento di memoria (FFFBh) è formato da soli
80 byte (50h byte) e così via. Quanti sono in totale i segmenti di memoria
incompleti?
Per rispondere a questa domanda basta osservare che:
65536 / 16 = 216 / 24 = 216 - 4 = 212 = 4096
In un segmento di memoria completo ci sono quindi 4096 paragrafi; possiamo dire
allora che degli ultimi 4096 segmenti di memoria, solo il primo di essi (il n.
F000h) è completo. Tutti gli altri 4095 sono incompleti; il n. F001h
è più corto di 16 byte, il n. F002h è più corto di 32 byte, il n.
F003h è più corto di 48 byte e così via, sino al n. FFFFh che
risulta più corto di ben 65520 byte.
Cosa succede se, con una CPU 8086, proviamo ad accedere ad un indirizzo
logico come FFFFh:0010h che corrisponde ad un indirizzo fisico inesistente?
La CPU trova questo indirizzo logico e lo converte nell'indirizzo fisico:
(FFFFh * 10h) + 10h = FFFF0h + 10h = 100000h
Tradotto in binario questo indirizzo fisico è espresso da 100000000000000000000b;
come si può notare, si tratta di un indirizzo formato da 21 bit. L'Address
Bus dell'8086 è formato da sole 20 linee, per cui non può contenere
un indirizzo a 21 bit; quello che succede è che la cifra più significativa
(1) dell'indirizzo a 21 bit viene persa e si ottiene l'indirizzo a 20
bit 00000000000000000000b!
In pratica, se proviamo ad accedere all'indirizzo logico FFFFh:0010h, ci
ritroviamo all'indirizzo fisico 00000h; analogamente, ripetendo il calcolo
precedente si può constatare che, se proviamo ad accedere all'indirizzo logico
FFFFh:0011h, ci ritroviamo all'indirizzo fisico 00001h, se proviamo
ad accedere all'indirizzo logico FFFFh:0012h, ci ritroviamo all'indirizzo
fisico 00002h, se proviamo ad accedere all'indirizzo logico
FFFFh:0013h, ci ritroviamo all'indirizzo fisico 00003h e così via.
Il fenomeno appena descritto è caratteristico della CPU 8086 e prende il
nome di wrap around (attorcigliamento); il programmatore deve evitare accuratamente
di incappare in questa situazione che spesso porta un programma ad invadere aree della
RAM riservate al sistema operativo, con conseguente crash del computer!
Tutti i concetti fondamentali precedentemente esposti, rappresentano la cosiddetta
modalità di indirizzamento reale 8086 (o semplicemente modalità reale);
il termine reale si riferisce al fatto che una qualunque coppia logica valida
Seg:Offset, si riferisce ad un indirizzo realmente esistente all'interno del
MiB di RAM visibile dall'8086. Il programmatore che intende scrivere
programmi per questa modalità operativa delle CPU 80x86, deve avere una
padronanza totale di questi concetti; infatti, nello scrivere un programma destinato alla
modalità reale, il programmatore si trova continuamente alle prese con indirizzamenti che
devono essere espressi sotto forma di coppie logiche Seg:Offset, che verranno poi
tradotte dalla CPU in indirizzi fisici a 20 bit da caricare sull'Address
Bus.
Come vedremo nei capitoli successivi, per permettere al programmatore di gestire al meglio
questa situazione, la CPU 8086 mette a disposizione una serie di registri a
16 bit; in particolare, sono presenti appositi registri destinati a contenere
indirizzi logici del tipo Seg:Offset. Un qualunque registro destinato a contenere
una componente Seg viene chiamato Segment Register (registro di segmento);
un qualunque registro destinato a contenere una componente Offset viene chiamato
Pointer Register (registro puntatore). Utilizzando questi registri è possibile
organizzare un programma nel modo più adatto alle nostre esigenze; nel caso più semplice,
per la corretta gestione di un programma in esecuzione vengono utilizzati: un registro
di segmento per il blocco codice, un registro di segmento per il blocco dati, un registro
puntatore per il blocco codice e uno o più registri puntatori per il blocco dati.
Apparentemente la situazione sembra molto simile al caso dell'indirizzamento lineare
illustrato in Figura 9.1; in realtà, tra una coppia Base:Offset e una coppia
Seg:Offset esistono notevoli differenze sostanziali. La componente Base
di un indirizzo lineare è l'indirizzo fisico iniziale di un blocco di programma; la
componente Seg di un indirizzo logico individua, invece, uno tra i possibili
65536 segmenti di memoria allineati al paragrafo all'interno dell'unico MiB di
RAM gestibile dall'8086. La componente Offset di un indirizzo
lineare è un qualunque spiazzamento calcolato rispetto all'indirizzo fisico Base;
la componente Offset di un indirizzo logico è uno spiazzamento compreso tra
0000h e FFFFh, calcolato rispetto all'indirizzo fisico a 20 bit da
cui inizia il segmento di memoria Seg.
È importante ribadire ancora una volta che un programmatore deve acquisire in modo
completo i concetti appena esposti; in caso contrario non sarà mai in grado di
scrivere un programma serio, né con l'Assembly, né con i linguaggi di alto
livello. L'importanza enorme di questi concetti è legata in particolare al fatto
che, come vedremo tra breve, la modalità reale 8086 continua ad esistere anche
su tutte le CPU della famiglia 80x86; questo discorso vale quindi anche per
le CPU di classe Pentium I, II, III, etc.
9.2 La modalità di indirizzamento protetto
Nel 1982 la Intel mette in commercio una nuova CPU, chiamata
80186; nel mondo dei PC questa CPU passa praticamente inosservata
a causa del fatto che si tratta semplicemente di una versione più evoluta
dell'8086. La vera novità arriva, invece, nello stesso anno con l'inizio della
produzione della CPU 80286; questa CPU è dotata, come l'8086,
di una architettura a 16 bit (Data Bus a 16 linee e registri interni
a 16 bit), ma presenta un Address Bus formato da ben 24 linee!
Con un Address Bus a 24 linee l'80286 può vedere sino a
224=16777216 byte (16 MiB) di RAM fisica; per poter
accedere a questa "gigantesca" quantità di RAM, l'80286 fornisce, per la
prima volta, una nuova modalità operativa chiamata modalità di indirizzamento
protetto. Sostanzialmente, questa modalità operativa permette al programmatore di
accedere alla RAM attraverso lo schema illustrato in Figura 9.1; questo schema
prevede, come sappiamo, l'utilizzo degli indirizzi Base:Offset di tipo lineare. A
tale proposito, i registri di segmento a 16 bit dell'80286, vengono utilizzati
per indicizzare una tabella che si trova in memoria e che contiene la descrizione completa
dei vari blocchi che formano il programma in esecuzione; questa descrizione comprende, in
particolare, la componente Base a 24 bit che esprime, come sappiamo,
l'indirizzo fisico da cui inizia un determinato blocco di programma.
Tra i programmatori però si diffonde subito una certa delusione, dovuta al fatto che
l'architettura a 16 bit dell'80286, permette di esprimere indirizzi
Base:Offset con la componente Offset a soli 16 bit; come accade
quindi per l'8086, anche con l'80286 la componente Offset può
assumere solamente i valori compresi tra 0000h e FFFFh. In base a queste
considerazioni, possiamo dire che con l'80286 il programmatore ha la possibilità
di accedere in modalità protetta a ben 16 MiB di memoria fisica, attraverso
indirizzi Base:Offset dotati di componente Base a 24 bit; a causa
però dell'architettura a 16 bit di questa CPU, la componente Offset
rimane a 16 bit, per cui la memoria risulta ancora suddivisa in blocchi che non
possono superare la dimensione di 64 KiB.
In ogni caso, l'80286 introduce sicuramente numerose e importanti novità che aprono
tra l'altro la strada ai sistemi operativi, come Windows, capaci di eseguire più
programmi contemporaneamente (sistemi operativi multitasking); a tale proposito,
l'80286 fornisce anche il supporto necessario ai sistemi operativi, per evitare
che due o più programmi contemporaneamente in esecuzione, possano danneggiarsi a vicenda.
Questi meccanismi di protezione originano appunto la definizione di modalità protetta;
un programma scritto, invece, per la modalità reale, ha la possibilità, ad esempio, di
invadere liberamente le aree di memoria riservate al sistema operativo, con il rischio
elevato di provocare un crash del computer.
Prima di mettere in commercio l'80286, la Intel si trova ad affrontare una
situazione molto delicata, dovuta al fatto che nel frattempo, il mondo dei PC è
stato letteralmente invaso da una enorme quantità di software scritto esplicitamente per
la modalità reale delle CPU 8086; con l'arrivo dell'80286, c'è quindi il
rischio che tutto questo software diventi inutilizzabile. Per evitare questa eventualità,
la Intel progetta l'80286 rendendo questa CPU compatibile con
l'8086; in particolare, l'80286 conserva la stessa struttura interna dei
registri dell'8086, garantendo così che tutti i programmi scritti per la modalità
reale dell'8086, possano girare senza nessuna modifica sull'80286. In più,
bisogna anche aggiungere che non appena si accende un computer equipaggiato con una
80286, la CPU viene inizializzata in modalità di indirizzamento reale
8086; in questa modalità operativa, l'80286 si comporta come se fosse una
8086.
Ben presto però i progettisti della Intel scoprono un ulteriore problema piuttosto
curioso, che si verifica quando l'80286 si trova in modalità reale; questo problema
è legato al fatto che il wrap around caratteristico dell'8086, non si verifica
più con l'80286. Per capire il perché riprendiamo un precedente esempio nel quale
abbiamo visto che l'indirizzo logico FFFFh:0010h corrisponde all'indirizzo fisico
100000h, il quale necessita di almeno 21 bit; questo indirizzo provoca
naturalmente il wrap around sull'Address Bus a 20 linee dell'8086,
mentre con l'80286, grazie all'Address Bus a 24 linee, è un indirizzo
perfettamente valido!
Non ci vuole molto a capire che, con la CPU 80286, pur lavorando in modalità reale,
è possibile indirizzare anche una certa quantità di memoria che si trova subito dopo
l'unico MiB di RAM visibile dall'8086; con l'8086, il limite massimo è
rappresentato dall'indirizzo fisico FFFFFh, a cui possiamo associare, ad esempio,
l'indirizzo logico FFFFh:000Fh. Con l'80286 possiamo scavalcare questo
limite indirizzando, sempre in modalità reale, tutta la memoria compresa tra gli indirizzi
logici FFFFh:0010h e FFFFh:FFFFh; questi indirizzi logici corrispondono agli
indirizzi fisici 100000h e 10FFEFh, per cui, l'ulteriore memoria indirizzabile
in modalità reale con l'80286 è pari a:
10FFEFh - 100000h + 1h = FFF0h = 65520 byte
Il +1 tiene conto del byte iniziale che si trova all'indirizzo 100000h;
ricordiamoci a tale proposito dell'errore fuori di uno citato nel precedente capitolo.
Questo blocco da 65520 byte è stato chiamato High Memory Area (area di memoria
alta) e può esistere quindi solo in presenza di una CPU 80286 o superiore; come si
può notare, si tratta di un blocco che ha appena 16 byte (un paragrafo) in meno
rispetto ad un segmento di memoria da 65536 byte.
Per evitare qualunque problema di compatibilità con l'8086, la Intel ha fatto
in modo che sull'80286 sia possibile disabilitare la linea A20 dell'Address
Bus; in questo modo l'80286, quando lavora in modalità reale, si ritrova con un
Address Bus a 20 linee, attraverso il quale è possibile indirizzare solo
1 MiB di RAM come accade con l'8086.
Riassumendo, possiamo dire quindi che l'80286 è capace di funzionare, sia in modalità
reale 8086, sia in modalità protetta; in modalità reale gli indirizzamenti si
svolgono con le coppie logiche Seg:Offset da 16+16 bit, mentre in modalità
protetta gli indirizzamenti si svolgono con le coppie Base:Offset da 24+16 bit.
L'80286 è stata subito utilizzata dalla IBM sui propri PC, determinando
in questo modo la nascita di un nuovo standard chiamato PC IBM AT (la sigla AT
sta per Advanced Technology); anche in questo caso il PC IBM AT è stato subito
affiancato da una numerosa serie di cloni, universalmente conosciuti come PC IBM AT
compatibili.
La vera svolta nella storia della Intel arriva nel 1985 con la progettazione
della nuova CPU 80386; la Figura 9.6 mostra la piedinatura (vista dal basso) del chip
che racchiude questa CPU.
Osserviamo subito che l'80386 è predisposta per un Address Bus formato da
ben 32 linee (da A0 a A31); con un Address Bus a 32
linee, l'80386 è in grado di indirizzare sino a 232=4294967296
byte (4 GiB) di memoria RAM fisica!
Come accade per l'80286, anche l'80386 rende disponibile la modalità
operativa protetta, che permette di accedere in modo lineare a tutta questa memoria; la
novità importante però è rappresentata dal fatto che, come si può notare in Figura 9.6,
l'80386 è una CPU con architettura a 32 bit, dotata quindi di
Data Bus a 32 linee (da D0 a D31) e numerosi registri a
32 bit. Per la precisione, i registri di segmento dell'80386 continuano ad
essere tutti a 16 bit; come al solito, questi registri vengono utilizzati in modalità
protetta per indicizzare una tabella che si trova in memoria e che contiene la descrizione
completa dei vari blocchi che formano il programma in esecuzione. Questa descrizione
comprende, in particolare, la componente Base a 32 bit che rappresenta
l'indirizzo fisico da cui parte in memoria un determinato blocco di programma; l'aspetto
fondamentale è rappresentato però dal fatto che, questa volta, nelle coppie
Base:Offset anche la componente Offset è a 32 bit!
Un offset a 32 bit può spaziare da 00000000h a FFFFFFFFh, permettendoci
di accedere linearmente a tutta la RAM disponibile (sino a 4 GiB); tutto ciò
significa in sostanza che con la modalità protetta dell'80386, scompare finalmente la
segmentazione a 64 KiB della memoria!
Nel progettare l'80386 la Intel garantisce la piena compatibilità con il
software scritto per la modalità reale 8086 e per la modalità protetta a 24
bit delle CPU 80286; questa scelta comporta per l'80386 una architettura
interna piuttosto complessa. Come è stato già detto, i registri di segmento dell'80386
continuano ad essere a 16 bit e in modalità reale vengono utilizzati, naturalmente,
per contenere la componente Seg di una coppia logica Seg:Offset; gli altri
registri a 32 bit (compresi i registri puntatori), vengono ottenuti aggiungendo
ulteriori 16 bit ai registri a 16 bit dell'8086.
Come accade per l'80286, non appena si accende un computer equipaggiato con una
80386, la CPU viene inizializzata in modalità reale con conseguente
disabilitazione della linea A20 dell'Address Bus; in queste condizioni,
l'80386 si comporta come se fosse una 8086 ed è in grado di indirizzare
direttamente 1 MiB di RAM.
In definitiva, l'80386 è una CPU capace di funzionare, sia in modalità reale
8086, sia in modalità protetta; in modalità reale gli indirizzamenti si svolgono
con le coppie logiche Seg:Offset da 16+16 bit, mentre in modalità protetta
gli indirizzamenti si svolgono con le coppie Base:Offset da 32+32 bit.
Tutte le CPU successive all'80386, come l'80486, l'80586
(classe Pentium I), l'80686 (classe Pentium II), etc, si possono
considerare a tutti gli effetti come versioni sempre più evolute della stessa 80386;
queste CPU formano una famiglia chiamata 80x86 e garantiscono pienamente
la compatibilità verso il basso. Questo significa che un programma scritto per una
8086, gira senza alcuna modifica su una CPU di classe superiore; viceversa,
un programma scritto esplicitamente per una 80586, non può girare sulle CPU
80486 o inferiori.
È importante ribadire che tutte le CPU della famiglia 80x86, all'accensione
del computer, vengono inizializzate in modalità reale 8086 con conseguente
disabilitazione della linea A20; per queste CPU valgono quindi le stesse
considerazioni già esposte per l'80286 e per l'80386.
Un aspetto piuttosto interessante è rappresentato dal fatto che una CPU 80386
o superiore che lavora in modalità reale, mette a disposizione molte sue caratteristiche
architetturali senza la necessità di passare in modalità protetta; avendo a disposizione, ad
esempio, una CPU 80386 con architettura a 32 bit, è possibile sfruttare
interamente i registri a 32 bit senza uscire dalla modalità reale. Tutto ciò si
rivela particolarmente utile nel caso di operazioni che coinvolgono numeri a 32 bit;
sfruttando i registri a 32 bit dell'80386, possiamo effettuare queste operazioni
in modo rapidissimo.
Le considerazioni appena esposte creano un dubbio relativo all'uso dei registri puntatori
a 32 bit negli indirizzamenti in modalità reale; abbiamo visto che in questa
modalità operativa, vengono utilizzati indirizzi logici del tipo Seg:Offset
con le due componenti Seg e Offset che hanno entrambe una ampiezza di
16 bit. Nel caso della componente Seg non ci sono problemi in quanto le
CPU 80386 e superiori continuano ad avere registri di segmento a 16
bit; è possibile utilizzare, invece, un registro puntatore a 32 bit per contenere
una componente Offset a 32 bit?
La risposta è affermativa in quanto le CPU come l'80386 permettono al
programmatore di selezionare, anche in modalità reale, l'ampiezza in bit della componente
Offset di un indirizzo logico Seg:Offset; la tecnica da utilizzare coinvolge
aspetti che richiedono la conoscenza della programmazione in modalità protetta delle
CPU 80x86. A sua volta, la programmazione in modalità protetta richiede una
solidissima conoscenza del linguaggio Assembly, per cui questo argomento verrà
trattato in dettaglio nella apposita sezione Modalità Protetta di questo sito;
tutte le considerazioni svolte nella sezione Assembly Base si riferiscono, invece,
alla modalità operativa reale delle CPU 80x86 con indirizzamenti logici di tipo
Seg:Offset da 16+16 bit.
9.3 Sistemi operativi per la modalità reale: il DOS
Come è stato detto in un precedente capitolo, il sistema operativo (SO) di un
computer può essere paragonato al cruscotto di una automobile; sarebbe piuttosto difficile
guidare una automobile senza avere a disposizione il volante, il freno, l'acceleratore, la
spia dell'olio, etc. Attraverso questi strumenti, l'automobilista può gestire facilmente
una automobile senza la necessità di conoscere il funzionamento interno del motore e dei
vari meccanismi; un discorso analogo può essere fatto anche per il computer. Sarebbe
praticamente impossibile, infatti, usare un computer senza avere a disposizione un
SO; attraverso il SO anche un utente privo di qualsiasi conoscenza di
informatica può utilizzare il computer per usufruire di tutta una serie di servizi che
permettono, ad esempio, di ascoltare musica, di giocare, di navigare in Internet, etc.
Nell'ambiente operativo rappresentato dalla modalità reale delle CPU 80x86, il
SO dominante (nel bene e nel male) è stato ed è sicuramente il DOS
(Disk Operating System); questo SO venne scelto per la prima volta dalla
IBM per equipaggiare il PC IBM XT. L'antenato del DOS è il CP/M
della Digital Research, destinato alla gestione dei computer con architettura a
8 bit; il DOS può essere definito come una evoluzione del CP/M, ed è
rivolto esplicitamente alla modalità reale delle CPU 80x86.
Il DOS è un SO con interfaccia testuale (interfaccia a caratteri o
alfanumerica); ciò significa che quando l'utente accende un computer gestito dal DOS,
dopo le varie inizializzazioni si trova davanti ad uno schermo in modalità testo, che mostra
un cursore lampeggiante chiamato Prompt del DOS. A questo punto l'utente, attraverso la
tastiera, può impartire al SO i comandi desiderati; ciascun comando viene letto da un
apposito programma chiamato interprete dei comandi, che dopo averne verificato la
validità provvede ad eseguirlo.
Ridotto all'essenziale il DOS è formato da tre programmi contenuti nei tre file
IO.SYS, MSDOS.SYS e COMMAND.COM; nella versione IBM del
DOS, il file IO.SYS viene chiamato IBMBIO.SYS, mentre il file
MSDOS.SYS viene chiamato IBMDOS.SYS.
Il programma IO.SYS si occupa innanzi tutto di un compito fondamentale che consiste
nel caricamento in memoria e nella conseguente inizializzazione del DOS; si tratta
quindi di un modulo che sfrutta ampiamente gli strumenti messi a disposizione dal
BIOS.
Proprio attraverso il BIOS, il modulo IO.SYS fornisce anche una serie di
procedure che si occupano della gestione a basso livello dell'hardware del computer;
attraverso questo programma l'utente può richiedere i servizi a basso livello offerti
da svariate periferiche. La gestione a basso livello delle periferiche viene resa possibile
dai cosiddetti device drivers (piloti di dispositivo); si tratta di appositi
programmi sviluppati dai produttori delle periferiche, che permettono al DOS di
dialogare con tastiere, stampanti, mouse, etc, senza conoscerne il funzionamento interno.
Il programma MSDOS.SYS fornisce, invece, una numerosa serie di servizi che permettono
ai programmi DOS di interfacciarsi ad alto livello con le periferiche del computer;
grazie a questi servizi, un programma DOS può, ad esempio, inviare dati ad una generica
stampante senza preoccuparsi degli aspetti legati al funzionamento a basso livello di questa
periferica. Un altro importantissimo compito svolto da MSDOS.SYS consiste nella
gestione del cosiddetto file system del computer; si tratta di un insieme di servizi
di alto livello che permettono ai programmi DOS di accedere in lettura e in scrittura
alle memorie di massa (hard disk, floppy disk, etc), senza preoccuparsi dei dettagli relativi
al metodo utilizzato per memorizzare fisicamente i dati su questi supporti.
Nel loro insieme, IO.SYS e MSDOS.SYS formano il cosiddetto kernel del
DOS; il termine kernel deriva dal tedesco e significa nucleo centrale.
Il programma COMMAND.COM rappresenta l'interprete dei comandi del DOS; questo
programma attiva un ciclo infinito in attesa che l'utente impartisca un comando. Quando ciò
accade, COMMAND.COM verifica il comando e, se lo ritiene valido, lo esegue tramite gli
opportuni servizi offerti da MSDOS.SYS e IO.SYS. Se il comando non è valido,
COMMAND.COM produce un messaggio di errore del tipo:
Comando o nome file non valido
Se il DOS è stato installato nella partizione C: del disco rigido, allora i
tre file IO.SYS, MSDOS.SYS e COMMAND.COM si trovano nella cartella
C:\; trattandosi di file nascosti, per visualizzarli bisogna posizionarsi in
C:\ e impartire il comando:
dir /ah
Per prendere confidenza con l'ambiente operativo offerto dal DOS, esaminiamo ora
l'organizzazione della RAM del computer sotto questo SO; la Figura 9.7
illustra la mappa della RAM, relativa ad un generico PC IBM compatibile.
Per trattare il caso più generale possibile supponiamo di avere a che fare con un PC
dotato di CPU 80386 o superiore e di più di 1 MiB di RAM; in questo modo
possiamo anche osservare il punto di vista del DOS in relazione alla RAM che
si trova oltre il primo MiB.
In Figura 9.7, sulla parte sinistra della RAM possiamo notare i vari indirizzi fisici
a 32 bit; ovviamente, questi indirizzi possono essere visti dall'80386 solo
in modalità protetta. Sulla parte destra della RAM notiamo, invece, i vari indirizzi
logici di tipo Seg:Offset a 16+16 bit così come vengono visti dal programmatore
attraverso la modalità reale del DOS; come già sappiamo, gli indirizzi logici
compresi tra FFFFh:0010h e FFFFh:FFFFh esistono solo in presenza di un PC
dotato di CPU 80286 o superiore e di oltre 1 MiB di RAM.
Nella terminologia del DOS, tutta la memoria rappresentata dal primo MiB di RAM
viene chiamata Base Memory (memoria base); si tratta come sappiamo della sola
memoria direttamente indirizzabile in modalità reale 8086. La memoria base
viene suddivisa in due parti chiamate Conventional Memory (memoria convenzionale)
e Upper Memory (memoria superiore); il confine tra queste due aree di memoria viene
delimitato dall'indirizzo fisico 000A0000h che può essere associato all'indirizzo
logico A000h:0000h. Come già sappiamo, il blocco di memoria da 65520 byte
immediatamente successivo al primo MiB viene chiamato High Memory (memoria alta);
questo blocco è indirizzabile in modalità reale solo con una CPU 80286 o superiore
(con la linea A20 abilitata). Tutta la memoria che sta al di sopra della High
Memory viene chiamata Extended Memory (memoria estesa); questa memoria è
direttamente indirizzabile solo in modalità protetta dalle CPU 80286 e superiori.
Analizziamo ora le varie aree che nel loro insieme formano la mappa della RAM
relativa all'ambiente operativo supportato dal DOS.
9.3.1 Vettori di interruzione - da 00000000h a 000003FFh
Come è stato detto in un precedente capitolo, i vettori di interruzione sono degli
indirizzi che individuano la posizione in memoria di una serie di procedure (spesso
scritte in Assembly); queste procedure, rendono disponibili ai programmi in
esecuzione, una numerosa serie di servizi di basso livello (livello macchina)
offerti dall'hardware del computer.
Sfruttando le nuove conoscenze che abbiamo acquisito in questo capitolo, possiamo
aggiungere che ciascun vettore di interruzione è un indirizzo logico del tipo
Seg:Offset da 16+16 bit; risulta evidente quindi che questi vettori
di interruzione sono disponibili solo quando le CPU della famiglia 80x86,
lavorano in modalità reale. Proprio per questo motivo, ciascun vettore di interruzione
"punta" ad una procedura che deve necessariamente trovarsi nel primo MiB della
RAM; in questo modo, la procedura stessa può essere chiamabile da un programma
che opera in modalità reale 8086 e che può vedere quindi solo 1 MiB di
RAM.
Generalmente, i vettori di interruzione vengono installati in fase di avvio del
computer; una serie piuttosto numerosa di vettori di interruzione, viene installata
principalmente dal DOS e dal BIOS del PC. L'installazione di
un vettore di interruzione consiste innanzi tutto nel caricamento in memoria
della relativa procedura, chiamata ISR o Interrupt Service Routine
(procedura di servizio dell'interruzione); l'indirizzo di questa procedura, viene
poi sistemato nell'apposita area della RAM, riservata ai vettori di
interruzione.
In seguito ad una serie di convenzioni stabilite dai produttori di hardware e di
SO, è stato deciso che i PC debbano riservare un'area della
RAM sufficiente a contenere 256 vettori di interruzione; ciascun
vettore è un indirizzo logico Seg:Offset che richiede 16+16=32 bit
(4 byte) e quindi, l'area complessiva della RAM riservata ai
vettori di interruzione è pari a:
256 * 4 = 1024 byte = 400h byte
Quest'area deve essere rigorosamente posizionata nella parte iniziale della
RAM compresa tra gli indirizzi fisici 00000000h e 000003FFh;
ciascun vettore di interruzione viene identificato da un indice compreso tra
0 e 255 (in esadecimale, tra 00h e FFh). Come si può
facilmente intuire, molti di questi indici sono riservati rigorosamente al
DOS e al BIOS del PC; il vettore n. 10h, ad esempio,
è riservato ai servizi offerti dal BIOS della scheda video. Se vogliamo
usufruire di questi servizi, non dobbiamo fare altro che chiamare la procedura
che si trova all'indirizzo associato al vettore di interruzione n. 10h;
la fase di chiamata di un vettore di interruzione, effettuata da un programma
in esecuzione, prende il nome di interruzione software.
I servizi offerti dai vettori di interruzione, vengono richiesti anche dalle
periferiche che hanno la necessità di comunicare con la CPU; in questo caso
si parla di interruzione hardware (questo argomento è stato già trattato nel
Capitolo 7).
9.3.2 Area dati ROM-BIOS - da 00000400h a 000004FFh
Nel Capitolo 7 è stato detto che quando si accende il computer, il controllo passa ad
una serie di programmi memorizzati nella ROM del PC; questi programmi
comprendono, in particolare, il POST che esegue l'autodiagnosi relativa a tutto
l'hardware del computer.
Uno dei compiti fondamentali svolti dai programmi della ROM consiste nella raccolta
di una serie di importanti informazioni relative alle caratteristiche generali dell'hardware
del PC; queste informazioni vengono messe a disposizione dei programmi e comprendono,
ad esempio, gli indirizzi delle porte seriali e parallele, lo stato corrente dell'hard disk e
del floppy disk, la modalità video corrente, etc.
Tutte queste informazioni vengono inserite in un'area della RAM formata da 256
byte (100h byte); per convenzione quest'area deve essere rigorosamente compresa tra
gli indirizzi fisici 00000400h e 000004FFh.
9.3.3 Area comunicazioni del DOS - da 00000500h a 000006FFh
Come già sappiamo, l'ultimo compito svolto dai programmi della ROM in fase di avvio
del computer, consiste nel cercare ed eventualmente eseguire un programma chiamato
boot loader; il boot loader ha l'importante compito di caricare in memoria
il SO che nel nostro caso è il DOS. Una volta che il DOS ha ricevuto
il controllo, esegue a sua volta una serie di inizializzazioni; in questa fase il DOS
raccoglie anche una serie di importanti informazioni globali relative al SO.
Tutte queste informazioni vengono messe a disposizione dei programmi in un'area della
RAM formata da 512 byte (200h byte); per convenzione quest'area deve
essere rigorosamente compresa tra gli indirizzi fisici 00000500h e 000006FFh.
9.3.4 Kernel del DOS, Device Drivers, etc - da 00000700h a ????????h
In quest'area viene caricato, in particolare, il kernel del DOS formato dai due
file IO.SYS e MSDOS.SYS; sempre in quest'area trovano posto svariati
device drivers usati dal DOS e alcuni buffers interni (aree usate dal
DOS per depositare informazioni temporanee).
Come si può notare in Figura 9.7, quest'area parte rigorosamente dall'indirizzo fisico
00000700h e assume una dimensione variabile; ciò è dovuto al fatto che, a seconda
della configurazione del proprio computer, è possibile ridurre le dimensioni di quest'area
spostando porzioni del DOS in memoria alta o nella memoria superiore e liberando
così spazio nella memoria convenzionale. Sui computer dotati di un vero DOS (quindi,
non un emulatore), è presente in C:\ un file chiamato CONFIG.SYS; all'interno
di questo file si possono trovare i comandi che permettono appunto di liberare spazio nella
memoria convenzionale. Il comando:
DOS=UMB
se è presente, permette di spostare parti del DOS in memoria superiore; la sigla
UMB sta per Upper Memory Blocks (blocchi di memoria superiore).
Il comando:
DOS=HIGH
se è presente, permette di spostare parti del DOS in memoria alta.
In generale, l'area della RAM destinata ad ospitare il kernel del DOS, i
device drivers e i buffers interni occupa alcune decine di KiB.
9.3.5 Disponibile per i programmi DOS - da ????????h a 000A0000h
Tutta l'area rimanente nella memoria convenzionale è disponibile per i programmi DOS;
quest'area inizia quindi dalla fine del blocco precedentemente descritto e termina
all'indirizzo fisico 0009FFFFh. L'indirizzo fisico successivo è 000A0000h che
tradotto in base 10 corrisponde a 655360 (640*1024) e rappresenta la
tristemente famosa barriera dei 640 KiB; un qualunque programma DOS per poter
essere caricato in memoria ed eseguito, deve avere dimensioni non superiori a 640 KiB.
Commentando questo aspetto, l'allora semisconosciuto Bill Gates pronunciò una famosa
frase (poi smentita) dicendo che: "640 KiB rappresentano una quantità enorme di memoria,
più che sufficiente per far girare qualunque applicazione presente e futura"!
In realtà un programma da 640 KiB è troppo grande in quanto abbiamo visto che
i primi KiB della RAM sono riservati ai vettori di interruzione, all'area dati
della ROM BIOS, etc; tolte queste aree riservate restano a disposizione per i
programmi circa 600 KiB effettivi!
9.3.6 Buffer video - modo grafico - da 000A0000h a 000AFFFFh
L'indirizzo fisico 000A0000h segna l'inizio della memoria superiore; teoricamente
la memoria superiore è un'area riservata, non utilizzabile quindi in modo diretto dai
programmi DOS.
In questo capitolo abbiamo visto che una CPU che opera in modalità reale 8086
è in grado di indirizzare in modo diretto solo 1 MiB di RAM (memoria base);
questo significa che qualsiasi altra memoria esterna, per poter essere accessibile deve
essere mappata in qualche zona della memoria base (I/O Memory Mapped). Lo scopo
principale della memoria superiore è proprio quello di contenere svariati buffers nei
quali vengono mappate le memorie esterne come la memoria video, la ROM BIOS, etc;
questi buffers sono aree di scambio attraverso le quali un programma DOS può
comunicare con le memorie esterne.
L'area compresa tra gli indirizzi fisici 000A0000h e 000AFFFFh viene utilizzata
per mappare la memoria video in modalità grafica; quest'area ha una dimensione pari a:
000AFFFFh - 000A0000h + 1h = 10000h byte = 65536 byte = 64 KiB
Attraverso quest'area un programma DOS può quindi leggere o scrivere in un blocco da
64 KiB della memoria video grafica; tutto ciò ci fa intuire che, com'era prevedibile,
la segmentazione a 64 KiB della modalità reale si ripercuote anche sulle memorie
esterne.
Un buffer per una memoria esterna può essere paragonato ad un fotogramma di una pellicola
cinematografica; il proiettore fa scorrere la pellicola mostrando agli spettatori la
sequenza dei vari fotogrammi. Allo stesso modo, il programmatore può richiedere la mappatura
nel buffer video della porzione desiderata della memoria video; questa tecnica permette di
accedere dal buffer video a tutta la memoria video disponibile. Proprio per questo motivo,
un buffer come quello descritto prende anche il nome di frame window (finestra
fotogramma) o frame buffer (buffer fotogramma).
9.3.7 Buffer video - modo testo in b/n - da 000B0000h a 000B7FFFh
L'area della RAM compresa tra gli indirizzi fisici 000B0000h e 000B7FFFh
contiene il buffer per l'I/O con la memoria video in modalità testo per i vecchi
monitor in bianco e nero; si tratta quindi di un buffer da 8000h byte, cioè
32768 byte (32 KiB).
9.3.8 Buffer video - modo testo a colori - da 000B8000h a 000BFFFFh
L'area della RAM compresa tra gli indirizzi fisici 000B8000h e 000BFFFFh
contiene il buffer per l'I/O con la memoria video in modalità testo a colori; si tratta
anche in questo caso di un buffer da 8000h byte, cioè 32768 byte (32 KiB).
9.3.9 Video ROM BIOS - da 000C0000h a 000CFFFFh
In quest'area da 64 KiB viene mappata la ROM BIOS delle schede video che
contiene una numerosa serie di procedure per l'accesso a basso livello all'hardware
della scheda video; le schede video sono ben presto diventate talmente potenti ed evolute
da richiedere un loro specifico BIOS che permette ai SO di gestire al meglio
queste periferiche.
9.3.10 Disponibile per altri BIOS e buffers - da 000D0000h a 000EFFFFh
Quest'area da 128 KiB è disponibile per ospitare ulteriori buffers per il collegamento
con altre memorie esterne; in particolare, in quest'area viene creata la frame window
per l'accesso alle espansioni di memoria che si utilizzavano ai tempi delle CPU
8086.
Può capitare frequentemente che in quest'area rimangano dei blocchi liberi di memoria;
questi blocchi liberi possono essere utilizzati dai programmi DOS e in certi casi
è persino possibile far girare programmi DOS in memoria superiore. A tale proposito
è necessario disporre di una CPU 80386 o superiore e di un cosiddetto Memory
Manager (gestore della memoria) che permetta di indirizzare in modalità reale queste
aree riservate; nel mondo del DOS il memory manager più famoso è senz'altro
EMM386.EXE che viene fornito insieme allo stesso SO.
9.3.11 PC ROM BIOS - da 000F0000h a 000FFFFFh
In quest'area da 64 KiB viene mappata la ROM BIOS principale del PC;
in questo modo i programmi DOS possono usufruire di numerosi servizi offerti dal
BIOS per accedere a basso livello all'hardware del computer.
Come si può notare, quest'area comprende anche i famosi 4095 segmenti di memoria
incompleti; riservando questi segmenti al BIOS, si evita che i normali programmi
DOS possano incappare nel wrap around.
9.4 Organizzazione della memoria sotto DOS
Per motivi di efficienza il DOS suddivide idealmente la RAM in paragrafi; in
questo modo, la memoria base del computer (cioè il primo MiB) risulta suddivisa come già
sappiamo in 65536 blocchi da 16 byte ciascuno. Il paragrafo rappresenta anche
la granularità della memoria DOS, cioè la quantità minima di memoria che
il DOS può gestire; ciò significa che un qualunque blocco di memoria DOS ha
una dimensione che è sempre un multiplo intero di 16 byte (16, 32,
48, 64, etc).
Tutti i blocchi di memoria DOS sono sempre allineati al paragrafo; di conseguenza,
l'indirizzo logico iniziale di un blocco di memoria DOS è sempre del tipo
XXXXh:0000h. Ciò permette al DOS di riferirsi a questi blocchi attraverso
la sola componente Seg dell'indirizzo iniziale; la componente Offset iniziale,
infatti, è ovviamente 0000h.
Supponiamo, ad esempio, di richiedere al DOS un blocco da 420 paragrafi di
memoria (6720 byte); se il DOS accetta la richiesta, ci restituisce un
valore a 16 bit che rappresenta appunto la componente Seg dell'indirizzo
iniziale del blocco di memoria richiesto. Supponiamo a tale proposito che il DOS
ci restituisca il valore 0BF2h; tenendo presente che 6720 in esadecimale si
scrive 1A40h, possiamo dire che il blocco di memoria messoci a disposizione dal
DOS è compreso tra gli indirizzi logici 0BF2h:0000h e 0BF2h:1A3Fh
per un totale appunto di 1A40h byte (6720 byte).
Da queste considerazioni risulta evidente che la dimensione minima di un blocco di
memoria DOS è pari a 1 paragrafo (16 byte), mentre la dimensione
massima è pari a 4096 paragrafi (65536 byte = 64 KiB); questo limite
massimo è ovviamente una diretta conseguenza della modalità reale 8086 che non
permette di esprimere un offset maggiore di FFFFh.
Se abbiamo bisogno di un blocco di memoria più grande di 64 KiB, siamo costretti
a richiedere al DOS due o più blocchi di memoria, ciascuno dei quali non può
superare i 64 KiB; in certi casi questa situazione può creare parecchi fastidi al
programmatore che si trova costretto a dover saltare continuamente da un blocco di memoria
ad un altro.
9.5 Indirizzamento di un programma DOS
Un programma DOS viene suddiviso in blocchi chiamati segmenti di programma
(da non confondere con i segmenti di memoria); nel caso più semplice e intuitivo,
un programma DOS è formato da due segmenti chiamati Data Segment (segmento
dati) e Code Segment (segmento di codice). Come al solito, il segmento dati
contiene i dati temporanei e permanenti del programma; il segmento di codice contiene,
invece, le istruzioni che devono elaborare i dati stessi.
Caricare un programma in memoria significa sistemare ciascun segmento di programma
all'interno di un segmento di memoria; nel caso più semplice ed intuitivo, ogni
segmento di programma viene posizionato a partire dall'offset 0000h di un
segmento di memoria. Questa situazione però può anche essere alterata grazie al
fatto che al programmatore, viene data la possibilità di stabilire a piacere il
posizionamento di un segmento di programma all'interno di un segmento di memoria;
questa particolare caratteristica prende il nome di allineamento in memoria di un
segmento di programma.
Come vedremo nei capitoli successivi, esiste la possibilità di richiedere un allineamento
ad indirizzi multipli interi di 2, di 4, di 16, etc, oppure nessun
allineamento (cioè, un allineamento ad un indirizzo qualunque); può capitare allora che
un determinato segmento di programma, per poter soddisfare i requisiti di allineamento,
venga posizionato all'interno di un segmento di memoria a partire da un offset maggiore
di 0000h. La Figura 9.8, ad esempio, mostra il caso di un segmento di programma da
44 byte (2Ch byte), che è stato posizionato a partire dall'offset
0006h del segmento di memoria n. 0BA9h; notiamo che sul lato sinistro
della RAM vengono indicati i vari segmenti di memoria, mentre sul lato destro
vengono indicati i vari indirizzi logici.
L'indirizzo logico 0BA9h:0006h corrisponde, come sappiamo, all'indirizzo
fisico:
(0BA9h * 10h) + 0006h = 0BA90h + 0006h = 0BA96h
Si tratta chiaramente di un allineamento ad un indirizzo fisico multiplo intero
di 2.
Possiamo dire allora che, nel rispetto della modalità di indirizzamento reale, la
CPU accede al segmento di programma di Figura 9.8, attraverso coppie logiche
Seg:Offset con la componente Seg che vale sempre 0BA9h; la
componente Offset parte dal valore minimo 0006h e può spaziare sino
al valore massimo possibile FFFFh.
Attraverso la componente Offset possiamo muoverci all'interno del segmento
di programma accedendo a tutti i suoi 44 byte; il primo byte del segmento
di programma si trova all'offset 6, mentre l'ultimo byte, cioè il numero
44, si viene a trovare all'offset:
6 + (44 - 1) = 6 + 43 = 49 = 31h
Riassumendo, possiamo dire che il primo byte del segmento di programma si trova
all'indirizzo logico 0BA9h:0006h, mentre l'ultimo byte si trova all'indirizzo
logico 0BA9h:0031h.
Questi concetti possono dare l'impressione di essere un po' ostici da capire; in realtà,
con un po' di esercizio ci si rende conto che si tratta di aspetti abbastanza elementari
(che verranno comunque abbondantemente approfonditi nei capitoli successivi).
A titolo di curiosità, in Figura 9.8 possiamo notare che il nostro segmento di programma
termina poco oltre l'inizio del segmento di memoria n. 0BACh; osservando,
infatti, che l'offset 31h è formato da 3 paragrafi (30h) più
1 byte, ricaviamo subito:
0BA9h:0031h = (0BA9h + 3h):0001h = 0BACh:0001h
Tutte le considerazioni precedentemente esposte, hanno una importante implicazione che,
come al solito, è una diretta conseguenza della modalità reale 8086; osserviamo
innanzi tutto che una componente Offset di un indirizzo logico può assumere tutti
i valori compresi tra 0000h e FFFFh. Il segmento di programma di Figura 9.8
inizia dall'offset 0006h e termina all'offset 0031h; se volessimo aggiungere
informazioni a questo segmento, le sue dimensioni potrebbero crescere liberamente sino
ad arrivare all'offset FFFFh. La dimensione massima di questo segmento di programma
sarebbe quindi:
FFFFh - 0006h + 1h = FFFAh byte = 65530 byte
Nella migliore delle ipotesi, possiamo allineare un segmento di programma al paragrafo,
in modo che la sua componente Offset parta sicuramente da 0000h; in questo
caso, l'offset può variare da 0000h a FFFFh e il segmento di programma
raggiungerebbe la dimensione massima possibile, pari a:
FFFFh - 0000h + 1h = 10000h byte = 65536 byte = 64 KiB
Tutto ciò significa che un qualunque segmento di programma, non può superare la
dimensione massima di 64 KiB e cioè, la dimensione di un segmento di memoria!
Se il nostro programma ha bisogno di meno di 64 KiB di dati e meno di 64 KiB
di codice, allora possiamo ritenerci fortunati; in questo caso, infatti, dobbiamo comunicare
alla CPU la componente Seg relativa all'unico blocco codice e la componente
Seg relativa all'unico blocco dati. Supponiamo, ad esempio, di chiamare queste due
componenti SegCodice e SegDati; esistendo un solo blocco codice e un solo blocco
dati, queste due componenti restano fisse per tutta la fase di esecuzione del programma. Se
vogliamo allora accedere a un dato, abbiamo la possibilità di comunicare alla CPU la
sola componente Offset dell'indirizzo del dato stesso; la CPU associa
automaticamente Offset a SegDati ottenendo così l'indirizzo logico
SegDati:Offset.
Un indirizzo formato dalla sola componente Offset a 16 bit viene chiamato
indirizzo NEAR (indirizzo vicino); gli indirizzi NEAR ci permettono di
risparmiare memoria e vengono gestiti molto più velocemente dalla CPU.
Se però il nostro programma ha bisogno di più di 64 KiB di dati e/o più di 64
KiB di codice, allora entriamo nell'inferno della segmentazione a 64 KiB; in questo caso,
infatti, il nostro programma deve essere suddiviso in due o più segmenti di codice e/o due o
più segmenti di dati. Supponiamo, ad esempio, che il nostro programma abbia due segmenti di
dati associati alle due componenti SegDati1 e SegDati2; in questo caso, se
vogliamo accedere a un dato, dobbiamo comunicare alla CPU, non solo la componente
Offset, ma anche la componente SegDati1 o SegDati2 che identifica il
segmento di appartenenza del dato stesso.
Un indirizzo formato da una coppia completa Seg:Offset a 16+16 bit viene
chiamato indirizzo FAR (indirizzo lontano); gli indirizzi FAR occupano il
doppio della memoria necessaria per un indirizzo NEAR e quindi risultano più
lenti da gestire per la CPU.
I linguaggi di programmazione di alto livello rivolti ai programmatori esperti, mettono a
disposizione le cosiddette variabili puntatore che sono destinate a contenere proprio
indirizzi di tipo NEAR o FAR; nel linguaggio C, ad esempio, la
definizione:
char near *ptcn;
crea un dato intero senza segno a 16 bit chiamato ptcn e destinato a contenere
l'indirizzo NEAR di un dato di tipo char (intero con segno a 8 bit).
La variabile ptcn viene chiamata puntatore NEAR ad un dato di tipo char.
Analogamente, la definizione:
char far *ptcf;
crea un dato intero senza segno a 32 bit chiamato ptcf e destinato a contenere
l'indirizzo FAR di un dato di tipo char (intero con segno a 8 bit).
La variabile ptcf viene chiamata puntatore FAR ad un dato di tipo char.
9.6 Supporto del DOS sui sistemi operativi per la modalità protetta
Esistono numerosi SO destinati a supportare la modalità protetta delle CPU
80286 e superiori; tra i più famosi si possono citare Windows, OS/2,
MacOSX e Linux. Le versioni meno vecchie di questi SO, richiedono
almeno una CPU 80386, in modo da poter usufruire di un supporto più avanzato
della modalità protetta; tutti questi SO, sono di tipo multitasking
e quindi sono in grado di eseguire più programmi contemporaneamente. Naturalmente, con
un'unica CPU a disposizione, è possibile eseguire un solo programma alla volta;
per ottenere allora il multitasking, si utilizza un noto espediente chiamato
time sharing (ripartizione del tempo). In sostanza, il SO fa girare a turno
tutti i programmi, assegnando a ciascuno di essi un piccolissimo intervallo di tempo di
esecuzione (una frazione di secondo); in questo modo, l'utente ha l'impressione che i
vari programmi stiano girando contemporaneamente.
I SO della famiglia Windows offrono il pieno supporto dei programmi scritti
per la modalità reale del DOS; questo supporto varia da versione a versione di
Windows. Nel caso di Windows 95 e Windows 98 si ha a disposizione
un vero DOS; come è stato ampiamente dimostrato da alcuni hackers, in realtà
queste due versioni di Windows sono delle sofisticate interfacce grafiche che si
appoggiano sul DOS!
All'interno di Windows 95 e Windows 98 si può attivare il tipico ambiente
DOS selezionando il menu Start - Programmi - Prompt di MS-DOS; in questo
modo viene aperta una finestra (Dos Box) che simula la caratteristica schermata
testuale del DOS con tanto di cursore lampeggiante.
Nella cartella C:\ si può constatare la presenza dei file nascosti IO.SYS,
MSDOS.SYS e COMMAND.COM; la cartella riservata ai vari programmi di
utilità del DOS è C:\WINDOWS\COMMAND. La presenza di un vero DOS
permette anche di richiedere il riavvio del computer in modalità MS-DOS; in questo
caso ci ritroviamo ad operare in un ambiente DOS vero e proprio.
Nel caso, invece, di Windows XP e superiori, viene fornito un emulatore DOS;
tale emulatore svolge egregiamente il proprio dovere, ma risulta essere particolarmente lento.
Con queste versioni di Windows ovviamente non è possibile riavviare il computer
in modalità DOS vera e propria; infatti, la funzione riavvia in modalità
MS-DOS di Windows XP, ad esempio, ci fornisce ugualmente un emulatore DOS!
Linux è nato per sfruttare la modalità protetta delle CPU 80386 e superiori;
questo SO non fornisce quindi alcun supporto per la modalità reale. In un caso del
genere bisogna ricorrere necessariamente ad un emulatore DOS; come è stato già
anticipato in un precedente capitolo, per seguire adeguatamente questo tutorial in
ambiente Linux si può installare un adeguato emulatore.
Il DOS è un SO monotasking, ed è in grado quindi di eseguire un
solo programma per volta; l'unico programma in esecuzione ha tutto il computer a sua
completa disposizione e può accedere liberamente e direttamente a tutto l'hardware
disponibile. Ci si può chiedere allora come faccia un SO come Windows
ad eseguire addirittura due o più programmi DOS contemporaneamente ad altri
programmi Windows (che, tra l'altro, girano anche in modalità protetta).
Per poter fare una cosa del genere questi SO sfruttano una caratteristica veramente
rivoluzionaria delle CPU 80386 e superiori, rappresentata dalla cosiddetta modalità
virtuale 8086 (V86); si tratta di una potente caratteristica che permette alla
CPU di eseguire programmi DOS senza uscire dalla modalità protetta!
Nel caso dell'80286, se ci troviamo in modalità protetta e vogliamo eseguire un
programma DOS, dobbiamo prima tornare in modalità reale; queste continue commutazioni
tra modalità protetta e modalità reale provocano tra l'altro un sensibile rallentamento
nel funzionamento del computer. La modalità V86 delle CPU 80386 o
superiori crea una macchina virtuale 8086 costituita da un'area di memoria virtuale da
1 MiB; un programma DOS gira in questo MiB virtuale credendo di trovarsi in un
tipico ambiente DOS. Il MiB virtuale creato dalla modalità V86 può trovarsi in
qualunque area della memoria; l'aspetto straordinario della modalità V86 è che
grazie al time sharing, un SO multitasking può creare più macchine virtuali
facendo girare più programmi DOS contemporaneamente!
Come si può facilmente intuire, in un SO di tipo multitasking tutti i
tentativi di accesso all'hardware da parte di un programma in esecuzione vengono filtrati
dal SO stesso; in questo modo è possibile fare in modo che più programmi in
esecuzione possano accedere alla stessa periferica senza interferenze reciproche.