Assembly Avanzato con NASM

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

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