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