Assembly Avanzato con MASM
Capitolo 6: La memoria base del PC
Con il termine base memory (memoria base) si indica l'unico MiB di RAM
direttamente indirizzabile attraverso l'address bus a 20 linee delle
vecchie CPU 80x86; tra i vari modelli di tali CPU si possono citare,
in particolare, la 8086, la 80186 e la 8088. Nel seguito del
capitolo si utilizzerà la sigla 8086 per rappresentare una qualunque
CPU con address bus a 20 linee.
Come sappiamo, con un address bus a 20 linee possiamo accedere ad un
massimo di 220=1048576 locazioni di memoria, ciascuna delle quali
occupa 1 byte (sulla piattaforma hardware 80x86); a tali 1048576
byte possiamo assegnare tutti gli indirizzi fisici compresi tra 00000h e
FFFFFh.
Nella sezione Assembly Base abbiamo visto che per rendere possibile l'accesso
a tutti i 1048576 byte della RAM attraverso i registri a 16 bit
di cui erano dotate le vecchie CPU, si è deciso di ricorrere alla cosiddetta
segmentazione della memoria; a tale proposito, i 1048576 byte vengono
suddivisi in blocchi da 16 byte ciascuno, chiamati paragrafi.
Complessivamente, abbiamo quindi un totale di:
1048576 / 16 = 220 / 24 = 220 - 4 = 216 = 65536 paragrafi
L'accesso ai 1048576 indirizzi fisici avviene, via software, attraverso i
cosiddetti indirizzi logici rappresentati da coppie Seg:Offset;
entrambe le componenti, Seg e Offset, hanno un'ampiezza di 16
bit.
La componente Seg rappresenta uno dei 65536 possibili paragrafi (da
0000h a FFFFh); la componente Offset rappresenta uno spiazzamento,
compreso tra 0000h e FFFFh, relativo al paragrafo specificato da
Seg.
La CPU utilizza un metodo semplicissimo per convertire un indirizzo logico in
un indirizzo fisico; infatti, osservando che la componente Seg coincide con il
numero di paragrafo e che la componente Offset è uno spiazzamento relativo a
Seg, si ottiene:
indirizzo_fisico = (Seg * 16) + Offset
Da quanto detto segue immediatamente che ogni paragrafo segna l'inizio di un blocco di
memoria da 65536 byte; tale blocco prende il nome di segmento di memoria.
Gli ultimi 4095 segmenti di memoria vengono definiti incompleti in
quanto hanno una dimensione inferiore a 65536 byte; ciò è dovuto al fatto che
il primo MiB di RAM termina all'indirizzo fisico FFFFFh per cui, ad
esempio, il segmento di memoria n. FFFFh ha una lunghezza che viene troncata
a 16 byte. Infatti:
(FFFFh * 16) + 16 = 1048575 = FFFFFh
Un'altra conseguenza evidente delle cose appena esposte è che i segmenti di memoria,
essendo allineati al paragrafo, hanno indirizzi fisici iniziali del tipo XXXX0h
(multipli interi di 16); normalizzando tali tipi di indirizzo fisico otteniamo
XXXXh:0000h. Possiamo affermare allora che il valore contenuto nella componente
Seg dell'indirizzo logico normalizzato da cui inizia un segmento di memoria,
coincide con il numero di paragrafo che identifica il segmento stesso; ad esempio, il
segmento di memoria n.3FC8h è identificato dal paragrafo n.3FC8h e inizia
dall'indirizzo logico normalizzato 3FC8h:0000h.
Per maggiori dettagli sui concetti appena esposti si consiglia la rilettura del Capitolo
9, sezione Assembly Base.
6.1 Organizzazione della memoria base sotto DOS
La tecnica appena descritta permette di accedere ad uno qualunque dei 1048576
byte presenti nell'unico MiB indirizzabile direttamente dalla 8086; in sostanza,
attraverso gli indirizzi logici possiamo accedere a tutti gli indirizzi fisici
"realmente" presenti nella RAM.
Questa situazione giustifica la definizione di modalità reale per indicare la
modalità operativa con la quale i programmi che girano su una 8086 accedono alla
RAM attraverso le coppie Seg:Offset; come sappiamo, il SO che ha
dominato la modalità reale è stato il DOS.
La Figura 6.1, che già conosciamo, illustra il modo con il quale il DOS organizza
la memoria base.
Ricapitoliamo brevemente il significato dei vari blocchi presenti in Figura 6.1; per la
delimitazione dei blocchi stessi ci serviamo degli indirizzi fisici a 20 bit.
6.1.1 Vettori di interruzione - da 00000h a 003FFh
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 00000h e 003FFh; ciascun vettore di
interruzione viene identificato da un indice compreso tra 0 e 255 (in
esadecimale, tra 00h e FFh).
Bisogna ribadire un aspetto molto importante relativo al fatto che, in modalità
reale, i vettori di interruzione rappresentano il metodo attraverso il quale i
programmi comunicano con il DOS e con il BIOS; un programma che
abbia la necessità di richiedere un servizio del DOS o del BIOS, non
deve fare altro che chiamare l'opportuno vettore di interruzione!
Nel seguito del capitolo esamineremo, in particolare, il vettore di interruzione n.
21h attraverso il quale si possono ottenere numerosi servizi offerti dal
DOS; proprio per questo motivo, tale vettore viene definito vettore dei
servizi DOS.
6.1.2 Area dati ROM-BIOS - da 00400h a 004FFh
Nella sezione Assembly Base è stato spiegato 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 chiamata BDA
(Bios Data Area) e formata da 256 byte (100h byte); per convenzione quest'area
deve essere rigorosamente compresa tra gli indirizzi fisici 00400h e 004FFh.
6.1.3 Area comunicazioni del DOS - da 00500h a 006FFh
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 può essere il DOS, Windows, Linux, BSD, etc.
Nel caso particolare del DOS, una volta che tale SO 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 00500h e 006FFh.
6.1.4 Kernel del DOS, Device Driver, etc - da 00700h 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 driver usati dal DOS e alcuni buffer interni (aree usate dal
DOS per depositare informazioni temporanee).
Come si può notare in Figura 6.1, quest'area parte rigorosamente dall'indirizzo fisico
00700h 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
(o di un emulatore come DOSEmu) è 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.
Ad esempio, 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 driver e i buffer interni occupa alcune decine di KiB.
6.1.5 Disponibile per i programmi DOS - da ?????h a 9FFFFh
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 9FFFFh. L'indirizzo fisico successivo è A0000h 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.
In realtà un programma da 640 KiB è persino 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!
6.1.6 Buffer video per la modalità grafica - da A0000h a AFFFFh
L'indirizzo fisico A0000h segna l'inizio della memoria superiore; teoricamente
la memoria superiore è un'area riservata, non utilizzabile quindi in modo diretto dai
programmi DOS.
Nella sezione Assembly Base 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 buffer nei quali vengono mappate le memorie esterne come
la memoria video, la ROM BIOS, etc; questi buffer sono aree di scambio attraverso
le quali un programma DOS può comunicare con le memorie esterne.
L'area compresa tra gli indirizzi fisici A0000h e AFFFFh viene utilizzata
per mappare la memoria video in modalità grafica; quest'area ha una dimensione pari a:
AFFFFh - A0000h + 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).
6.1.7 Buffer video per la modalità testo in b/n - da B0000h a B7FFFh
L'area della RAM compresa tra gli indirizzi fisici B0000h e B7FFFh
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).
6.1.8 Buffer video per la modalità testo a colori - da B8000h a BFFFFh
L'area della RAM compresa tra gli indirizzi fisici B8000h e BFFFFh
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).
6.1.9 Video ROM BIOS - da C0000h a CFFFFh
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
di tali dispositivi; 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.
6.1.10 Disponibile per altri BIOS e buffer - da D0000h a EFFFFh
Quest'area da 128 KiB è disponibile per ospitare ulteriori buffer 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.
6.1.11 PC ROM BIOS - da F0000h a FFFFFh
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 tali segmenti al BIOS, si evita che i normali programmi
DOS possano incappare nel wrap around.
6.2 Suddivisione della memoria base sotto DOS
Dalle considerazioni appena esposte, risulta che la memoria base del PC viene
suddivisa in due aree delimitate dall'indirizzo fisico A0000h; tali due aree
vengono definite:
- conventional memory (memoria convenzionale) - da 00000h a 9FFFFh
- upper memory (memoria superiore) - da A0000h a FFFFFh
Per una precisa scelta dei progettisti del DOS, i programmi che girano in
modalità reale possono accedere in modo diretto solamente alla memoria convenzionale;
in assenza di diverse indicazioni da parte del programmatore, tutte le operazioni di
allocazione e deallocazione della memoria si riferiscono quindi ai primi 655360
byte di RAM compresi tra gli indirizzi fisici 00000h e 9FFFFh!
6.3 Granularità della memoria base sotto DOS
Per motivi di efficienza, il DOS maneggia la memoria base suddividendola in
paragrafi che, come sappiamo, occupano ciascuno 16 byte; la dimensione
di 16 byte rappresenta quindi la cosiddetta granularità della memoria
DOS e cioè, l'unità di misura che il DOS utilizza per i blocchi di
memoria allocati dai programmi.
In sostanza, un qualsiasi blocco di memoria DOS ha sempre una dimensione multipla
intera di 16 byte; non è possibile quindi avere blocchi di memoria DOS che
occupano, ad esempio, 11 byte, 351 byte, etc.
L'unico svantaggio di questa tecnica è rappresentato da un piccolo spreco di memoria
(ad esempio, se ci servono solamente 18 byte, dobbiamo richiederne per forza
2*16=32); i vantaggi, invece, sono notevoli e possono essere facilmente dedotti
da tutto ciò che abbiamo visto nella sezione Assembly Base.
In particolare, osserviamo che grazie alla granularità pari a 16 byte, un
qualsiasi blocco di memoria DOS parte sempre da un indirizzo fisico del tipo
XXXX0h (multiplo intero di 16) che possiamo associare all'indirizzo
logico normalizzato XXXXh:0000h; la conseguenza è che l'indirizzo logico
normalizzato iniziale di un blocco di memoria DOS coincide con l'indirizzo
logico normalizzato iniziale di un segmento di memoria e quindi ha sempre la componente
Offset che vale 0000h!
Le considerazioni appena esposte permettono al DOS di identificare un qualsiasi
blocco di memoria attraverso la sola componente Seg dell'indirizzo logico
iniziale; infatti, la componente Offset di tale indirizzo vale implicitamente
0000h.
Quando richiediamo allora al DOS un blocco di memoria, ci viene restituito un
valore a 16 bit che rappresenta proprio la componente Seg dell'indirizzo
logico iniziale del blocco stesso; il DOS non ha bisogno di specificare anche
la componente Offset in quanto essa vale, implicitamente, 0000h (e ciò
deve essere ben chiaro al programmatore al quale il DOS delega l'importante
compito di rispettare tale convenzione).
Supponiamo, ad esempio, di richiedere al DOS un blocco da 1500 paragrafi
di memoria, pari a:
1500 x 16 = 24000 byte = 5DC0h byte
Il DOS risponde affermativamente e ci restituisce, ad esempio, il valore
3FC8h; ciò significa che il blocco di memoria da noi richiesto parte
dall'indirizzo logico 3FC8h:0000h ed occupa in totale 5DC0h byte.
Il limite inferiore del nostro blocco è quindi 3FC8h:0000h; il limite superiore
è, invece, 3FC8h:5DBFh (cioè, 3FC8h:(5DC0h-1)). Se non rispettiamo questi
limiti, il nostro programma va a sovrascrivere aree di memoria riservate ad altri
programmi o, peggio ancora, al SO; nella gran parte dei casi la conseguenza di
tutto ciò è un classico crash!
6.4 La catena dei Memory Control Blocks
Per tenere traccia dei vari blocchi di memoria presenti nella RAM, il DOS
assegna a ciascuno di essi una intestazione la quale precede immediatamente il blocco
stesso; ogni intestazione, denominata Memory Control Block o MCB, occupa
16 byte ed assume la struttura mostrata in Figura 6.2.
In sostanza, immediatamente prima di ogni blocco di memoria DOS, è presente un
MCB che contiene la descrizione completa del blocco stesso; ogni MCB è
allineato al paragrafo ed essendo la sua dimensione pari a 16 byte (1
paragrafo), viene anche garantito il corretto allineamento al paragrafo del relativo
blocco di memoria.
Nel loro insieme, i MCB formano una vera e propria catena; il membro
IndicatoreBlocco della struttura di Figura 6.2 indica se ci troviamo in mezzo
alla catena o alla fine (ultimo blocco della catena).
Se tale membro contiene il codice
ASCII della lettera
'M', significa che ci troviamo in uno dei possibili blocchi che precedono
l'ultimo; il codice ASCII
della lettera 'Z' indica, invece, che ci troviamo nell'ultimo blocco della
catena.
Qualsiasi altro valore indica che la catena dei MCB è stata corrotta!
Il membro ProprietarioBlocco è una WORD che contiene l'eventuale
PSP del programma che ha allocato il relativo blocco di memoria; in tal caso,
come sappiamo, il programma stesso ha a disposizione un program segment che
inizia in memoria dall'indirizzo fisico PSP:0000h. Se, invece, il blocco di
memoria è libero, il membro ProprietarioBlocco vale 0000h.
Il membro DimensioneBlocco contiene la dimensione in paragrafi del relativo
blocco di memoria (escluso il paragrafo occupato dal MCB); di conseguenza,
la dimensione in byte del blocco di memoria sarà:
DimensioneBlocco * 16
Il membro DimensioneBlocco è molto importante in quanto ci permette di
percorrere l'intera catena dei MCB; infatti, il MCB immediatamente
successivo a quello corrente si troverà al paragrafo:
Paragrafo_MCB_corrente + DimensioneBlocco + 1
Il +1 è necessario per tenere conto del fatto che DimensioneBlocco
non comprende il paragrafo occupato dall'intestazione.
Il membro NomeBlocco contiene il nome dell'eventuale programma che ha
allocato il relativo blocco di memoria (si ricordi che nel DOS il nome di
un programma non può essere più lungo di 8 caratteri eventualmente seguiti
da una estensione di 3 caratteri); tale membro è disponibile solo con le
versioni più recenti del DOS.
Dalle considerazioni appena esposte risulta chiaramente che la catena dei MCB
forma una classica lista lineare di elementi; infatti, le informazioni presenti
in ogni MCB ci permettono di scandire facilmente tutta la catena.
A tale proposito, ci manca solamente una informazione fondamentale rappresentata
dall'indirizzo del primo MCB; tale informazione può essere ricavata dal
servizio n.52h della INT 21h visibile in Figura 6.3.
Questo servizio restituisce in ES:BX l'indirizzo logico di una tabella, chiamata
List of Lists, contenente una serie di importanti variabili interne del
DOS; per maggiori dettagli sul contenuto di tale tabella si può fare riferimento,
ad esempio, alla Interrupts list di Ralf Brown.
Un aspetto particolare dell'indirizzo restituito in ES:BX è che esso non è quello
iniziale della List of Lists; infatti, la tabella comprende anche ben 48
byte di informazioni disposte agli indirizzi che precedono ES:BX.
In particolare, all'indirizzo ES:(BX-2) è presente una WORD che rappresenta
la componente Seg dell'indirizzo logico iniziale del primo MCB della
catena; ricordando che tutti i MCB sono allineati al paragrafo, la componente
Offset implicita dell'indirizzo logico iniziale del primo MCB (come di
qualsiasi altro MCB) sarà quindi 0000h.
Utilizzando tutte queste informazioni, siamo già in grado di scrivere un semplice
programma che percorre tutti i MCB della catena; ad esempio, servendoci della
libreria COMLIB, possiamo scrivere:
Analizzando l'output prodotto da queste istruzioni si può notare che i vari MCB
occupano paragrafi inferiori a A000h, appartenenti quindi alla conventional
memory; nel seguito del capitolo vedremo che, in realtà, la catena prosegue anche
nella upper memory!
6.5 Servizi DOS per la gestione dei blocchi di memoria
I principali servizi offerti dal DOS per la gestione della memoria base,
permettono di allocare, deallocare o ridimensionare un blocco di memoria; analizziamo
tali servizi in dettaglio.
6.5.1 Allocazione di un blocco di memoria
Se un programma ha bisogno di allocare uno o più blocchi di memoria, può ricorrere al
servizio n.48h della INT 21h; le caratteristiche di tale servizio sono
illustrate in Figura 6.4.
Chiamando quindi la INT 21h con AH=48h, stiamo chiedendo al DOS
di riservarci un blocco di memoria la cui dimensione in paragrafi deve essere
specificata in BX; se la nostra richiesta viene accolta, si ottiene CF=0.
In tal caso, AX contiene la componente Seg dell'indirizzo logico da cui
parte il nostro blocco; come sappiamo, la componente Offset di tale indirizzo
vale, implicitamente, 0000h.
In caso di insuccesso si ottiene CF=1, mentre in AX viene restituito un
codice di errore il cui significato viene illustrato più avanti; il registro BX,
invece, contiene la dimensione in paragrafi del più grande blocco di memoria libero
disponibile.
Osserviamo che, se il blocco di memoria che abbiamo allocato ha una dimensione non
superiore a 64 KiB, la sua gestione è molto semplice; infatti, attraverso la
componente Offset a 16 bit, possiamo spaziare da 0000h a
FFFFh, coprendo l'intera estensione del blocco stesso.
Se, però, il blocco di memoria supera i 64 KiB, la sua gestione diventa più
impegnativa a causa della segmentazione a 64 KiB della memoria; in un caso del
genere, infatti, con la componente Offset non possiamo andare oltre FFFFh
in quanto si verificherebbe il wrap around!
La soluzione consiste allora nel modificare la componente Seg a seconda delle
necessità; a tale proposito, supponiamo di avere ottenuto dal DOS un blocco di
memoria da 128 KiB (2*64 KiB) che parte dall'indirizzo logico normalizzato
3CF2h:0000h (quindi, il DOS ci ha restituito Seg=3CF2h).
In tal caso, ricordando che in 64 KiB ci sono 4096 paragrafi, per gestire
i primi 64 KiB del blocco possiamo usare Seg=3CF2h e un Offset
compreso tra 0000h e FFFFh; invece, per gestire la seconda metà del
blocco (cioè, i successivi 64 KiB), possiamo usare Seg=3CF2h+4096 e un
Offset compreso tra 0000h e FFFFh!
6.5.2 Deallocazione di un blocco di memoria
Se un programma ha bisogno di restituire al DOS un blocco di memoria
precedentemente allocato, può ricorrere al servizio n.49h della INT 21h;
le caratteristiche di tale servizio sono illustrate in Figura 6.5.
Chiamando quindi la INT 21h con AH=49h, stiamo chiedendo al DOS
di liberare un blocco di memoria precedentemente allocato dal nostro programma; il
registro ES deve contenere la stessa componente Seg usata dal DOS
per identificare il blocco da liberare.
In caso di successo si ottiene CF=0, mentre CF=1 indica che si è verificato
un problema; in tal caso, in AX viene restituito un codice di errore il cui
significato viene illustrato più avanti.
6.5.3 Ridimensionamento di un blocco di memoria già allocato
Se un programma ha bisogno di ridimensionare un blocco di memoria già allocato in
precedenza, può ricorrere al servizio n.4Ah della INT 21h; le
caratteristiche di tale servizio sono illustrate in Figura 6.6.
Chiamando quindi la INT 21h con AH=4Ah, stiamo chiedendo al DOS
di ridimensionare un blocco di memoria precedentemente allocato dal nostro programma;
il registro BX deve contenere la nuova dimensione in paragrafi, mentre il
registro ES deve contenere la stessa componente Seg usata dal DOS
per identificare il blocco da ridimensionare.
In caso di successo si ottiene CF=0, mentre CF=1 indica che si è verificato
un problema; in tal caso, in AX viene restituito un codice di errore il cui
significato viene illustrato più avanti. In BX viene restituito il numero
massimo di paragrafi disponibili per l'eventuale ingrandimento di un blocco di memoria.
6.5.4 Codici di errore
La Figura 6.7 illustra il significato dei codici di errore restituiti dai servizi
analizzati in precedenza.
Ad esempio, se abbiamo a disposizione un blocco di memoria da 26 paragrafi e
vogliamo ingrandirlo sino a 40 paragrafi con il servizio n.4Ah della
INT 21h, può capitare che la memoria libera disponibile non sia sufficiente;
in tal caso il DOS ci restituisce CF=1 e AX=08h, mentre in
BX viene restituito il massimo numero di paragrafi disponibile per ingrandire
il nostro blocco di memoria.
6.6 Restituzione della memoria allocata in eccesso per un programma
Analizzando la Figura 6.4 possiamo notare che il servizio n.48h della INT
21h ci mette a disposizione un metodo per conoscere la dimensione in paragrafi
del più grande blocco libero disponibile; il trucco da adottare consiste nel
chiamare tale servizio ponendo in BX un valore "esagerato".
Proviamo, ad esempio, a richiedere un blocco di memoria da BX=FFFFh paragrafi,
pari a:
FFFFh * 16 = 65535 * 16 = 1048560 byte
In un caso del genere siamo praticamente certi del fatto che il DOS rifiuterà
la nostra richiesta restituendoci CF=1 e AX=08h (memoria insufficiente);
ma la cosa più importante è che in BX ci viene restituita la dimensione in
paragrafi del più grande blocco libero disponibile.
Vediamo allora quello che succede eseguendo il seguente codice:
Il risultato può variare da computer a computer ma, nel caso generale, si può
constatare che il più grande blocco libero disponibile è costituito da un numero
veramente irrisorio di paragrafi!
Come è possibile una cosa del genere?
La risposta a questa domanda è molto semplice in quanto basta fare riferimento al
metodo adottato dal DOS per ottimizzare l'uso della memoria; la regola
fondamentale che viene seguita consiste nel fatto che: non possono esistere
due blocchi liberi consecutivi e contigui!
Se si verifica una situazione del genere, il DOS unisce i due blocchi liberi
ottenendo un unico blocco libero; in questo modo viene ridotto al minimo il rischio
rappresentato dalla cosiddetta frammentazione della memoria!
La conseguenza di tutto ciò è che, inizialmente, risultano presenti alcuni blocchi
allocati dal DOS, alcuni piccoli blocchi liberi (non contigui) e un enorme
blocco libero principale da circa 600 KiB; nel momento in cui richiediamo
l'esecuzione di un nostro programma, il DOS va alla ricerca di un blocco per
l'environment segment e un blocco per il program segment.
Per l'environment segment è in genere sufficiente uno dei piccoli blocchi
liberi, mentre per il program segment il DOS va alla ricerca di un
blocco di dimensioni adeguate e trova proprio il suddetto enorme blocco libero
principale; in sostanza, al nostro programma viene assegnata quasi tutta la memoria
convenzionale libera e ciò spiega il perché della strana situazione descritta in
precedenza.
L'unico modo per risolvere questo problema consiste nel ricorrere al servizio
n.4Ah della INT 21h per ridurre al minimo indispensabile le dimensioni
del program segment; a tale proposito, dobbiamo conoscere l'indirizzo logico
iniziale del nostro programma e quello finale, in modo da poter calcolare la dimensione
in paragrafi del programma stesso.
Per l'indirizzo logico iniziale il discorso è semplice in quanto esso è rappresentato,
come sappiamo, da PSP:0000h; per l'indirizzo logico finale, invece, il discorso
è ben più complesso in quanto può capitare che parte del programma si trovi in una o
più librerie di cui non disponiamo del codice sorgente!
Per evitare calcoli contorti, la cosa migliore da fare consiste nel generare il map
file del nostro programma da cui possiamo ricavare la dimensione complessiva in
paragrafi; analizziamo allora come ci si deve comportare nel caso degli eseguibili
COM e EXE.
6.6.1 Riduzione del program segment di un eseguibile COM
In questo caso bisogna ricordare che è presente un unico segmento di programma che
contiene codice, dati e stack; appena il programma viene caricato in memoria, tutti
i registri di segmento, CS, DS, ES e SS, contengono il
PSP, mentre ad SP viene assegnato il valore FFFEh.
Il map file di un eseguibile COM specifica le dimensioni complessive in
byte dell'unico segmento di programma presente; tali dimensioni non comprendono lo
stack (che è a carico del SO).
Supponiamo allora che il map file ci dica che il nostro programma occupa circa
4580 byte; ricaviamo quindi:
4580 / 16 = 286.25 paragrafi
Il risultato che otteniamo deve essere sempre approssimato per eccesso; nel
nostro caso, un valore di sicurezza può essere 300 paragrafi.
A questi 300 paragrafi dobbiamo poi sommare lo stack che si trova sempre agli
indirizzi più alti; supponendo di avere bisogno di circa 800 byte di stack,
pari a 50 paragrafi, otteniamo la dimensione totale del program segment
che è pari a:
300 + 50 = 350 paragrafi = 350 * 16 byte = 5600 byte
Subito dopo il ridimensionamento del program segment, dobbiamo procedere alla
modifica di SP; in base ai calcoli appena effettuati, dobbiamo ovviamente porre
SP=5600.
In definitiva, immediatamente dopo l'entry point (con ES=PSP)
dobbiamo inserire il seguente codice:
Richiamando ora il servizio n.48h della INT 21h con BX=FFFFh, il
DOS ci informa che non può allocare un blocco da quasi 1 MiB in quanto
il più grande blocco libero disponibile ha una dimensione di circa 600 KiB; ciò
dimostra quindi che abbiamo liberato quasi tutta la memoria che era stata assegnata
in eccesso al nostro programma!
6.6.2 Riduzione del program segment di un eseguibile EXE
In questo caso bisogna ricordare che possono essere presenti anche numerosi segmenti
di programma nei quali viene distribuito il codice, i dati e lo stack; appena il
programma viene caricato in memoria, i soli registri di segmento, DS e ES,
contengono il PSP.
Il map file di un eseguibile EXE specifica le dimensioni complessive in
byte dei vari segmenti di programma presenti, compreso il segmento di stack; bisogna
prestare molta attenzione al fatto che tra i vari segmenti possono essere presenti dei
buchi di memoria (necessari per l'allineamento) e quindi non si deve commettere
l'errore di calcolare semplicemente la somma delle dimensioni dei segmenti stessi!
Consideriamo, ad esempio, il map file del programma TIMER2.EXE presentato
nel precedente capitolo; la Figura 6.8 illustra tutti i dettagli.
L'aspetto che ci interessa è che, tenendo conto dei vari buchi di memoria, il primo
segmento in assoluto (DATASEGM) parte dall'indirizzo fisico 00000h,
mentre l'ultimo segmento in assoluto (STACKSEGM) termina all'indirizzo fisico
0155Fh; otteniamo quindi:
155Fh byte = 5471 byte = 5471 / 16 paragrafi = 341.9375 paragrafi
Il risultato che otteniamo deve essere sempre approssimato per eccesso; nel
nostro caso, un valore di sicurezza può essere 350 paragrafi. In definitiva,
immediatamente dopo l'entry point (con ES=PSP) dobbiamo inserire
il seguente codice:
6.7 Strategie di allocazione della memoria
Quando un programma richiede l'allocazione di un blocco di memoria attraverso il
servizio n.48h della INT 21h, il DOS ricorre ad una delle
seguenti tre strategie:
- First Fit (il primo adatto)
- Best Fit (il più adatto)
- Last Fit (l'ultimo adatto)
La prima strategia è quella predefinita e consiste nel fatto che il DOS ci mette
a disposizione il primo blocco di memoria, di adeguate dimensioni, che incontra nella
catena dei MCB; ad esempio, se richiediamo un blocco da 400 paragrafi,
il DOS comincia a percorrere in avanti la catena dei MCB finché non
incontra un blocco da almeno 400 paragrafi.
La seconda strategia consiste nel fatto che il DOS ci mette a disposizione il
primo blocco di memoria le cui dimensioni sono più vicine a quelle richieste; ad esempio,
se richiediamo un blocco da 180 paragrafi, il DOS comincia a percorrere in
avanti la catena dei MCB finché non incontra un blocco le cui dimensioni siano le
più vicine (per eccesso) a 180 paragrafi.
La terza strategia è una sorta di prima strategia al contrario in quanto il DOS
effettua la ricerca partendo dall'ultimo MCB e percorrendo la catena a ritroso;
ad esempio, se richiediamo un blocco da 280 paragrafi, il DOS comincia a
percorrere a ritroso la catena dei MCB finché non incontra un blocco da almeno
280 paragrafi.
Per conoscere la strategia di allocazione corrente, è necessario utilizzare il
servizio n.58h, sottoservizio n.00h, della INT 21h; la Figura 6.9
illustra tutti i dettagli.
Per impostare una nuova strategia di allocazione è necessario utilizzare il servizio
n.58h, sottoservizio n.01h, della INT 21h; la Figura 6.10 illustra
tutti i dettagli.
La strategia di allocazione viene rappresentata attraverso una apposita codifica; con
le versioni più recenti del DOS sono disponibili le codifiche illustrate in
Figura 6.11.
Le vecchie versioni del DOS, che si utilizzavano ai tempi delle CPU con
architettura a 16 bit, rendevano disponibili solo le prime tre strategie
(00h, 01h e 02h); tali strategie operavano esclusivamente sulla
memoria convenzionale del PC.
Bisogna ribadire, infatti, che la memoria superiore era stata concepita come un'area
riservata, invisibile ai normali programmi che giravano in modalità reale (nella
memoria convenzionale); la situazione, però, cambiò radicalmente con l'avvento delle
CPU 80386 e superiori.
Le caratteristiche di tali CPU resero possibile la realizzazione di particolari
software denominati memory manager (gestore della memoria); lo scopo di un
memory manager è quello di rendere visibile ai normali programmi DOS l'intera
memoria fisicamente presente sul computer, compresa la memoria superiore e persino la
memoria estesa disposta oltre il primo MiB!
Nel DOS vero e proprio il memory manager più famoso è stato EMM386.EXE,
fornito insieme allo stesso SO; a loro volta, i moderni emulatori DOS
sono in grado di supportare tutti i servizi offerti dai memory manager più evoluti.
Avendo a disposizione una CPU 80386 o superiore, siamo in grado di sfruttare
al massimo tutte le strategie di allocazione fornite dalle versioni più recenti del
DOS; in questo capitolo analizzeremo l'accesso alla memoria superiore, mentre
nel capitolo successivo parleremo dell'accesso alla memoria estesa.
Il comportamento predefinito del DOS consiste nel fare in modo che i programmi
vedano solo la memoria convenzionale; in sostanza, la catena dei MCB inizia e
termina all'interno dei primi 640 KiB di RAM.
In realtà, come è stato già anticipato, tale catena continua anche nella memoria
superiore; la connessione tra l'ultimo MCB della memoria convenzionale e il
primo MCB della memoria superiore, prende il nome di Upper Memory Link
o UML (collegamento alla memoria superiore).
Nelle condizioni specificate in precedenza (presenza di una CPU 80386 o
superiore, presenza di un memory manager, etc) il programmatore ha la
possibilità di richiedere al DOS l'attivazione o la disattivazione
dell'UML; la situazione predefinita prevede che l'UML sia disattivato.
Per conoscere la condizione corrente dell'UML è necessario utilizzare il
servizio n.58h, sottoservizio n.02h, della INT 21h; la Figura
6.12 illustra tutti i dettagli.
Per impostare lo stato dell'UML è necessario utilizzare il servizio n.58h,
sottoservizio n.03h, della INT 21h; la Figura 6.13 illustra tutti i dettagli.
Lo stato dell'UML viene rappresentato attraverso una apposita codifica illustrata
in Figura 6.14.
Se attiviamo l'UML e proviamo a rieseguire il programma di esempio (presentato in
precedenza) che percorre tutta la catena dei MCB, possiamo notare che verranno
visualizzati anche paragrafi che si trovano oltre i primi 640 KiB; si tenga presente
che lo stato dell'UML deve essere rigorosamente preservato da un programma che lo
intenda modificare!
Tornando ora alla Figura 6.11 e assumendo che l'UML sia attivo, avremo a disposizione
la catena completa dei MCB che si sviluppa per tutta la memoria base (compresa
quindi la memoria superiore); in tal caso, il DOS ricercherà un blocco libero di
memoria secondo i seguenti criteri:
- strategie 00h, 01h, 02h - la ricerca parte dalla memoria convenzionale e prosegue,
se necessario, nella memoria superiore
- strategie 40h, 41h, 42h - la ricerca si svolge esclusivamente nella memoria
superiore
- strategie 80h, 81h, 82h - la ricerca parte dalla memoria superiore e prosegue, se
necessario, nella memoria convenzionale
6.8 Esempi pratici
Come esempio pratico, scriviamo un programma che, dopo aver attivato l'UML,
scandisce tutta la catena dei MCB visualizzando informazioni dettagliate sui
relativi blocchi di memoria; per ogni blocco viene mostrato: il paragrafo iniziale,
la dimensione in byte, la posizione in memoria (convenzionale o superiore), lo stato
(libero o allocato), il proprietario e la lettera ('M' o 'Z') presente
all'offset 0000h del MCB.
L'unico aspetto degno di nota riguarda l'individuazione del proprietario del blocco di
memoria; a tale proposito, facciamo riferimento a quanto esposto in questo capitolo e
nel Capitolo 13 della sezione Assembly Base.
Ricordiamo che il membro ProprietarioBlocco di Figura 6.2 contiene l'eventuale
PSP del proprietario del blocco di memoria; se il blocco è libero, tale membro
vale 0000h.
Quindi, se all'offset 0001h del MCB che stiamo esaminando troviamo il
valore 0000h, siamo sicuri che il relativo blocco è libero; se tale valore è
diverso da 0000h, verifichiamo se il blocco appartiene al DOS.
Ciò può essere accertato grazie al fatto che, per convenzione, i blocchi appartenenti
al DOS sono caratterizzati da ProprietarioBlocco=0008h; se non troviamo
il valore 0008h, verifichiamo se il blocco è un vero program segment di
un programma.
Ciò può essere accertato osservando che, in tal caso, il contenuto del membro
ProprietarioBlocco coincide con il paragrafo da cui inizia il relativo blocco
di memoria; infatti, il program segment di un programma inizia sempre da
PSP:0000h e il contenuto del membro ProprietarioBlocco è proprio il
PSP del programma stesso!
Se la verifica fornisce esito negativo, il blocco potrebbe essere un environment
segment; in tal caso, basta ricordare che all'offset 002Ch del PSP
è presente proprio il paragrafo da cui inizia l'environment segment del
relativo programma.
In sostanza, dobbiamo verificare se il contenuto di PSP:002Ch coincide con il
paragrafo da cui inizia il blocco di memoria che stiamo esaminando; se la verifica
fornisce esito negativo, per esclusione possiamo concludere che abbiamo a che fare
con un normale blocco dati allocato dinamicamente da un programma in esecuzione!
Raccogliendo tutti i concetti appena esposti, otteniamo il listato illustrato in
Figura 6.15.
Prima di tutto, il programma restituisce tutta la memoria allocata in eccesso; dal
map file risulta che il programma richiede circa 276 paragrafi a cui
aggiungiamo 74 paragrafi di stack per un totale di 350 paragrafi
(5600 byte).
Subito dopo, il programma alloca un blocco da 250 paragrafi (4000
byte) e uno da 180 paragrafi (2880 byte); in fase di esecuzione, tali
blocchi appaiono come aree dati allocate dal nostro programma (si noti che, non
essendo ancora attivo l'UML, i due blocchi vengono allocati nella memoria
convenzionale).
Il passo successivo consiste nell'attivare l'UML; è importante ricordare
che ogni programma che intenda modificare lo stato dell'UML, deve preservare
le impostazioni originali.
A questo punto viene eseguito il loop principale del programma che visualizza tutti
i dettagli relativi a ciascuno dei blocchi di memoria incontrati nella catena dei
MCB; il loop viene ripetuto finché il programma non incontra un MCB
identificato dalla lettera 'Z'.
Come si può notare in Figura 6.15, il programma termina senza restituire al
DOS i due blocchi di memoria allocati dinamicamente in fase di esecuzione;
ciò è possibile in quanto, con le versioni più recenti del DOS, tale lavoro
viene svolto in modo automatico!
Si tenga anche presente che il programma di Figura 6.15 è totalmente privo del
controllo sugli errori; tale aspetto è di fondamentale importanza e non può essere
certo trascurato nella progettazione di programmi "seri".
Bibliografia
Cristopher, Feigenbaum, Saliga - MS-DOS MANUALE DI PROGRAMMAZIONE - Mc Graw Hill