Assembly Avanzato con MASM
Capitolo 7: Lo standard XMS - eXtended Memory Specifications
Intorno al 1982, la Intel mette in commercio una nuova CPU della
famiglia 80x86, denominata 80286; tale CPU presenta, proprio
come la 8086, una normalissima architettura a 16 bit, ma la vera novità
è rappresentata dalla presenza di un address bus a 24 linee!
Ciò significa che mentre la 8086, con il suo address bus a 20
linee, può indirizzare 1 MiB di RAM, la 80286 è in grado di
accedere a ben:
224 = 16777216 byte = 16 MiB di memoria fisica!
Per rendere possibile l'indirizzamento di tutti i 16 MiB di memoria fisica, la
80286 introduce una nuova modalità operativa denominata modalità protetta;
anche in tale modalità continuano ad esistere gli indirizzi logici Seg:Offset
da 16+16 bit, ma il loro significato è completamente differente rispetto al caso
della modalità reale.
Nella modalità protetta della 80286, la componente Seg a 16
bit di un indirizzo logico, contiene un indice relativo ad una tabella di cosiddetti
descrittori di segmento; come si intuisce dal nome, ciascun descrittore contiene,
appunto, la descrizione completa di un segmento di programma. In particolare, ogni
descrittore contiene l'indirizzo a 24 bit da cui inizia in memoria il relativo
segmento di programma; tale indirizzo prende il nome di base address (indirizzo
base).
La componente Offset a 16 bit di un indirizzo logico permette allora di
specificare uno spiazzamento, compreso tra 0000h e FFFFh, relativo al
corrispondente base address; con questo meccanismo, è possibile indirizzare
tutti i 16 MiB gestibili da una CPU 80286!
Nel progettare la 80286, la Intel deve necessariamente tenere conto del
fatto che il mondo dei PC è ormai invaso da una enorme quantità di software
destinato alla modalità reale della 8086; a tale proposito, la
80286 mantiene una struttura interna totalmente compatibile con l'architettura
della 8086 e può quindi eseguire i programmi scritti originariamente per la
modalità reale.
Nello sforzo teso a garantire la massima compatibilità verso il basso della
80286, la Intel si trova ad affrontare un curioso aspetto legato proprio
all'address bus a 24 linee della nuova CPU; per capire tale
aspetto, è necessario ricordare il fenomeno del wrap around tipico della
modalità reale delle CPU con address bus a 20 linee.
Consideriamo l'indirizzo logico normalizzato FFFFh:000Fh; in sostanza, abbiamo
a che fare con uno spiazzamento 000Fh relativo al segmento di memoria
n.FFFFh (ultimo segmento disponibile). Tale indirizzo logico corrisponde
all'indirizzo fisico a 20 bit:
(FFFFh * 16) + Fh = (65535 * 16) + 15 = 1048575 = FFFFFh = 11111111111111111111b
Si tratta chiaramente dell'indirizzo fisico relativo all'ultimo byte dell'unico MiB di
RAM gestibile da una CPU con address bus a 20 linee!
Incrementando ora di 1 la componente Offset del precedente indirizzo
logico normalizzato, otteniamo FFFFh:0010h; tale indirizzo logico corrisponde
all'indirizzo fisico a 21 bit:
(FFFFh * 16) + 10h = (65535 * 16) + 16 = 1048576 = 100000h = 100000000000000000000b
Una CPU 8086, avendo a disposizione solo 20 linee di indirizzamento, tronca
il bit più significativo (il ventunesimo bit) del precedente indirizzo fisico e ottiene
00000000000000000000b; in sostanza, se tentiamo di accedere all'indirizzo logico
FFFFh:0010h, ci ritroviamo all'indirizzo fisico 00000h!
Come è facile constatare, la situazione è del tutto analoga anche per gli offset
successivi a 0010h, relativi al segmento di memoria n.FFFFh; quindi,
riassumendo:
- se tentiamo di accedere a FFFFh:0010h, ci ritroviamo all'indirizzo
fisico 00000h
- se tentiamo di accedere a FFFFh:0011h, ci ritroviamo all'indirizzo
fisico 00001h
- se tentiamo di accedere a FFFFh:0012h, ci ritroviamo all'indirizzo
fisico 00002h
- se tentiamo di accedere a FFFFh:0013h, ci ritroviamo all'indirizzo
fisico 00003h
e così via.
Il discorso cambia radicalmente nel caso di un programma, scritto per la modalità
reale, che viene fatto girare su una 80286; infatti, grazie all'address
bus a 24 linee, tutti i precedenti indirizzi logici sono perfettamente
validi!
Non essendoci alcun troncamento del bit più significativo, risulta che:
- se tentiamo di accedere a FFFFh:0010h, ci ritroviamo all'indirizzo
fisico 100000h
- se tentiamo di accedere a FFFFh:0011h, ci ritroviamo all'indirizzo
fisico 100001h
- se tentiamo di accedere a FFFFh:0012h, ci ritroviamo all'indirizzo
fisico 100002h
- se tentiamo di accedere a FFFFh:0013h, ci ritroviamo all'indirizzo
fisico 100003h
e così via.
In pratica, un programma in modalità reale che gira su una 80286 è in
grado di scavalcare il primo MiB di RAM attraverso tutti gli indirizzi logici
compresi tra FFFFh:0010h e FFFFh:FFFFh; complessivamente si tratta di
un numero di byte pari a:
FFFFh - 0010h + 1 = 65535 - 16 + 1 = 65535 - 15 = 65520
Stiamo parlando quindi di un blocco di memoria la cui dimensione in byte è inferiore
di 1 paragrafo a quella di un segmento di memoria; questo blocco da 65520
byte, che si trova immediatamente dopo il primo MiB, è stato chiamato High Memory
Area o HMA (area di memoria alta).
Per garantire l'assoluta compatibilità verso la modalità reale 8086, la
Intel ha allora fatto in modo che sulla 80286 sia possibile disabilitare
via software tutte le linee di indirizzo dalla A20 alla A23; in questo
modo, la 80286 si trova ad operare con un address bus a 20 linee
e può quindi eseguire senza alcun problema, programmi destinati alla modalità reale
8086.
Tutte le considerazioni appena esposte si applicano anche alle CPU successive
alla 80286; in generale quindi, una qualsiasi CPU 80286 o superiore, può
operare in modalità reale attraverso la disabilitazione di tutte le linee di indirizzo
dalla A20 in su.
Nel gergo tecnico si semplifica tutto il discorso attraverso il riferimento alla sola
linea A20. Quando si parla quindi di "linea A20 disabilitata", si intende
dire che la CPU opera con le sole prime 20 linee dell'address bus
(da A0 ad A19); l'espressione "linea A20 abilitata" indica, invece,
che la CPU opera con tutte le linee che compongono il suo address bus (ad
esempio, su una 80486 con linea A20 abilitata risultano disponibili tutte
le 32 linee di indirizzo, da A0 ad A31).
7.1 La HMA - High Memory Area
Nel precedente capitolo è stato spiegato che, avendo a disposizione una CPU 80386
o superiore e un memory manager, è possibile accedere alla Upper Memory
da un normale programma che gira in modalità reale; un discorso analogo può essere fatto
anche per la HMA!
Tutto è iniziato quando alcuni hackers osservarono che il DOS 5.x era in grado di
spostare parte del kernel proprio nella HMA attraverso il comando del file
CONFIG.SYS:
DOS=HIGH
Dopo un intenso lavoro di reverse engineering, quegli stessi hackers si accorsero che il
DOS 5.x non faceva altro che sfruttare la possibilità di scavalcare, in modalità
reale, la barriera del primo MiB di RAM sulle CPU 80286 o superiori; in
sostanza, il DOS 5.x era stato dotato di funzioni nascoste capaci di abilitare la
linea A20 in modo da poter accedere a tutti i 65520 byte della HMA
attraverso gli indirizzi logici, da FFFFh:0010h a FFFFh:FFFFh, tipici della
modalità reale!
Il reverse engineering ha portato a scoprire diversi servizi non documentati, usati dal
DOS per la determinazione della quantità di HMA libera, per l'allocazione
e la deallocazione di blocchi in HMA, etc; un aspetto importante è che i vari
servizi (tutti basati sulla INT 2Fh, denominata multiplex interrupt)
presuppongono che il DOS abbia caricato parte del kernel in HMA.
Se un programma ha bisogno di conoscere la quantità di HMA libera, può ricorrere al
servizio n.4Ah, sottoservizio n.01h, della INT 2Fh; le caratteristiche
di tale servizio sono illustrate in Figura 7.1.
Come è stato già spiegato, i vari servizi presuppongono che il DOS abbia caricato
parte del kernel in HMA; in caso contrario, il servizio 4A01h della INT
2Fh restituisce BX=0000h e ES:DI=FFFFh:FFFFh!
In analogia a quanto abbiamo visto per la upper memory, anche per l'accesso alla
HMA è richiesta una CPU 80286 o superiore, un memory manager e un
apposito comando nel file CONFIG.SYS rappresentato dalla linea:
DOS=HIGH
Come vedremo in seguito, le versioni più recenti del DOS hanno reso disponibile
un memory manager denominato HIMEM.SYS; il suo scopo è quello di fornire
una serie completa di servizi destinati alla gestione della upper memory, della
high memory e della extended memory.
Nell'ipotesi quindi che siano verificati tutti i requisiti appena esposti, possiamo
conoscere la quantità di HMA libera sul nostro computer con il seguente codice:
Generalmente, il risultato prodotto da questo codice mostra la presenza di uno o due
KiB di HMA liberi; un esempio di output può essere il seguente (da notare come
l'indirizzo del primo byte libero faccia riferimento ad un'area della RAM oltre
il primo MiB):
Gli altri servizi della INT 2Fh, relativi alla HMA, permettono di
allocare/deallocare blocchi di memoria e di ottenere l'indirizzo iniziale della catena
dei MCB in HMA; a tale proposito si può consultare, ad esempio, la
interrupts list di Ralf Brown.
7.2 La memoria estesa e lo standard XMS
Con il passare degli anni, si è venuta a creare una notevole confusione in relazione
alla possibilità, offerta ai programmi in modalità reale, di accedere ai vari blocchi
della upper memory, della high memory e della extended memory;
in particolare, i programmatori si sono trovati davanti ad un un proliferare di
tecniche di programmazione che molto spesso risultavano incompatibili tra loro.
Una delle prime tecniche venne introdotta dalla Microsoft e si basava sull'uso
di un driver denominato VDISK.SYS; il driver creava un disco virtuale in
memoria, attraverso il quale era possibile accedere alla RAM oltre i 640
KiB convenzionali.
Un'altra tecnica consisteva nel ricorrere ai servizi della INT 15h (Big
Memory Services) del BIOS; attraverso tale interruzione, il BIOS
permette ai programmi di accedere anche alla memoria oltre il primo MiB.
Proprio per porre rimedio a questa situazione confusionaria, sul finire degli anni
80 un gruppo di aziende del settore hardware e software si sono riunite per
definire uno standard denominato XMS o eXtended Memory Specifications
(specifiche per la memoria estesa); nel seguito del capitolo faremo riferimento allo
standard XMS 3.0 del 1991, definito da un consorzio di cui facevano
parte la Intel Corporation, la Lotus Development Corporation, la AST
Research Inc. e la Microsoft Corporation.
Lo standard XMS definisce tutte le specifiche necessarie per la progettazione
dei driver (memory manager) attraverso i quali i programmi che girano in
modalità reale possono accedere alla upper memory, alla high memory e
alla extended memory; in questo modo, l'accesso alle varie aree di memoria
avviene attraverso una interfaccia standard che garantisce l'assoluta compatibilità
tra i programmi.
Come vedremo nel seguito, ogni driver deve fornire obbligatoriamente una determinata
serie di servizi; è anche previsto un certo numero di ulteriori servizi opzionali.
7.2.1 Definizioni convenzionali
La Figura 7.2 illustra le definizioni convenzionali previste dallo standard XMS
per le varie aree di memoria; per delimitare le aree stesse, vengono usati gli indirizzi
fisici a 32 bit (ovviamente, nel caso della 80286 si deve fare riferimento
ai corrispondenti indirizzi a 24 bit).
Quindi, il termine extended memory rappresenta tutta la memoria (estesa) che
si trova oltre il primo MiB; lo standard XMS suddivide tale area in HMA
(primi 65520 byte di memoria estesa) e EMB (memoria estesa successiva
alla HMA)!
Proseguendo con le definizioni convenzionali, il termine A20 indica la ventunesima
linea dell'address bus di una CPU 80286 o superiore; abilitando la linea
A20 è possibile accedere alla memoria oltre il primo MiB.
Il termine XMM sta per eXtended Memory Manager e indica un qualsiasi
driver necessario per l'accesso alla memoria superiore, alta e estesa in modalità reale;
in effetti, come è stato già spiegato, lo scopo fondamentale dello standard XMS è
quello di fornire una interfaccia standard unificata per tali tre aree di memoria.
Il memory manager più famoso è sicuramente HIMEM.SYS in quanto viene fornito
insieme al DOS; esistono anche altri XMM abbastanza conosciuti come, ad
esempio, 386MAX, QEMM, NETROOM, etc.
7.2.2 Installazione dell'eXtended Memory Manager
Come è stato appena spiegato, il compito di un XMM è quello di far "apparire" la
memoria superiore, alta e estesa ai programmi che girano in modalità reale; se si ha la
necessità di accedere a tali aree di memoria, è necessario quindi installare un XMM.
In un ambiente DOS puro, aprendo con un editor il file C:\CONFIG.SYS, si
può notare la presenza di una linea del tipo:
DEVICE=C:\DOS\HIMEM.SYS
Tale linea provvede, appunto, ad installare il driver HIMEM.SYS in fase di
avvio del DOS; lo stesso driver accetta anche dei parametri da linea di comando,
alcuni dei quali verranno illustrati nel seguito (per un elenco dettagliato di tali
parametri e del loro significato, si consiglia di consultare un manuale del DOS).
7.3 Interfaccia di programmazione XMS
Tutti i servizi forniti da un XMM risultano accessibili attraverso una chiamata
alla cosiddetta XMS driver's control function (funzione di controllo del driver
XMS); ciascuno di tali servizi viene identificato attraverso un codice a 8
bit da caricare in AH.
Nel caso generale, quindi, l'accesso ad un servizio XMS, fornito da un XMM,
avviene nel seguente modo:
Un programma che intenda accedere ai servizi offerti dallo standard XMS deve
innanzi tutto verificare che sia installato un XMM in memoria; a tale proposito,
è necessario porre AL=00h, AH=43h e chiamare la INT 2Fh.
Se un XMM è installato in memoria, la precedente chiamata alla INT 2Fh
restituisce AL=80h; qualunque altro valore restituito in AL indica che
non è presente alcun XMM.
Il codice da eseguire assume quindi il seguente aspetto:
Una volta accertata la presenza di un XMM installato in memoria, dobbiamo
procedere con la determinazione dell'indirizzo della funzione di controllo; a tale
proposito, dobbiamo porre AL=10h, AH=43h e chiamare la INT 2Fh.
La funzione di controllo è di tipo FAR per cui il suo indirizzo è una coppia
Seg:Offset da 16+16 bit che viene restituita in ES:BX; la
cosa migliore da fare consiste nel memorizzare tale indirizzo in una DWORD.
Nell'ipotesi allora di aver definito una variabile a 32 bit denominata
XMSControlFunc, possiamo scrivere il seguente codice:
Ricordiamoci di rispettare sempre la convenzione Intel per la memorizzazione
degli indirizzi logici Seg:Offset; in base a tale convenzione, la componente
Offset deve sempre precedere la componente Seg!
A questo punto, avendo a disposizione l'indirizzo della funzione di controllo,
possiamo richiedere i vari servizi XMS con le seguenti istruzioni:
Nel caso di MASM, la chiamata della funzione di controllo diventa:
call dword ptr XMSControlFunc
o
call dword ptr [XMSControlFunc]
Se la richiesta di un servizio XMS ha successo, la funzione di controllo
restituisce AX=0001h più eventuali informazioni addizionali in BX e
DX; se, invece, la richiesta fallisce, la funzione di controllo restituisce
AX=0000h più un codice di errore in BL.
I codici di errore validi hanno sempre il bit più significativo che vale 1;
la Figura 7.3 illustra l'elenco completo dei codici di errore.
Il significato dei vari termini presenti in Figura 7.3 sarà chiarito nel seguito del
capitolo.
7.4 Servizi XMS
Lo standard XMS comprende una serie di servizi che possono essere suddivisi
nei gruppi illustrati in Figura 7.4.
Un XMM viene considerato pienamente aderente allo standard XMS 3.0
quando implementa tutti i servizi dei primi quattro gruppi; quindi, i servizi
relativi al quinto gruppo (gestione della upper memory) sono considerati
facoltativi.
In generale, un determinato driver potrebbe anche non implementare tutti i servizi
obbligatori, previsti dallo standard XMS 3.0; proprio per questo motivo, è
importante che il programmatore verifichi sempre l'eventuale codice di errore
restituito in BL dalla funzione di controllo, con particolare riferimento
al codice 80h (servizio non implementato)!
Analizziamo ora l'elenco completo dei vari servizi XMS e le caratteristiche
di ciascuno di essi; per maggiori dettagli si consiglia di scaricare il file
xms30.zip presente nella sezione
Documentazione tecnica di supporto al corso assembly dell’
Area Downloads di questo sito.
7.4.1 Servizio n. 00h: Get XMS Version Number
Questo servizio restituisce il numero di versione dello standard XMS supportato
dal driver XMM.
Il valore restituito in AX è in formato BCD; ad esempio, AX=0250h
significa che il driver supporta lo standard XMS versione 2.50.
Il valore restituito in BX si riferisce al numero di versione del driver
XMM e non al numero di versione dello standard XMS.
Si presti attenzione al fatto che DX=0001h indica che la HMA esiste ma
non è detto che sia disponibile; in sostanza, può capitare che la HMA, pur
essendo presente, sia già in uso da parte di un altro programma!
7.4.2 Servizio n. 01h: Request High Memory Area
Questo servizio tenta di allocare per un programma i 65520 byte della HMA.
Un TSR o Terminate and Stay Resident è un programma che, una volta lanciato,
si installa in memoria e resta attivo, in genere, sino allo spegnimento del computer; il
suo scopo è quello di fornire dei servizi richiesti da altri programmi.
Lo standard XMS tratta la HMA come un blocco unico di memoria, non
condivisibile tra due o più programmi; tale blocco viene quindi assegnato in esclusiva al
primo programma che ne faccia richiesta, purché tale richiesta sia compatibile con il
valore specificato attraverso il parametro HMAMIN del driver HIMEM.SYS.
Il parametro HMAMIN specifica la quantità minima (in KiB) di HMA che un
programma deve richiedere per avere in esclusiva l'accesso a tale area della RAM;
ad esempio, il comando:
DEVICE=C:\DOS\HIMEM.SYS /HMAMIN=32
installa in memoria il driver HIMEM.SYS e stabilisce che la HMA venga
assegnata al primo programma che ne richieda un blocco da almeno 32 KiB.
Il valore specificato da HMAMIN (che deve essere compreso tra 0 e
63) è significativo solo per i TSR e per i device driver; i normali
programmi dovrebbero specificare sempre DX=FFFFh quando richiedono il servizio
n.01h.
Si presti anche attenzione al fatto che HMAMIN specifica un valore in KiB, mentre
in DX bisogna inserire un valore in byte!
L'allocazione della HMA da parte di un programma ha senso solo quando tale
area di memoria non è utilizzata dal DOS; se si ha intenzione di sfruttare la
HMA dai propri programmi, è necessario quindi togliere il comando
DOS=HIGH dal CONFIG.SYS (o sostituirlo con il comando DOS=LOW).
Per sicurezza, conviene comunque verificare sempre se il DOS sta usando o meno
la HMA; a tale proposito, si può ricorrere al servizio 33h, sottoservizio
06h della INT 21h, che restituisce nel bit 4 di DH, il
valore 0 se il DOS non occupa la HMA, il valore 1 se il
DOS occupa la HMA.
7.4.3 Servizio n. 02h: Release High Memory Area
Questo servizio restituisce la HMA precedentemente allocata da un programma.
A differenza di quanto accade per i normali blocchi di memoria DOS, la
HMA non viene automaticamente deallocata quando il programma che l'aveva in
uso termina; tale compito deve essere svolto rigorosamente dal programma stesso!
7.4.4 Servizio n. 03h: Global Enable A20
Questo servizio tenta di abilitare la linea A20 in modo da rendere possibile
l'accesso alla memoria oltre il primo MiB.
Questo servizio deve essere usato esclusivamente da un programma intenzionato a
richiedere il controllo diretto della HMA; in ogni caso (cioè, anche se
l'allocazione della HMA fallisce), il programma stesso deve provvedere a
disabilitare la linea A20 prima di terminare!
7.4.5 Servizio n. 04h: Global Disable A20
Questo servizio tenta di disabilitare la linea A20.
Questo servizio deve essere usato esclusivamente da un programma che ha ottenuto il
controllo diretto della HMA; in sostanza, il programma che ha l'accesso
esclusivo alla HMA deve provvedere rigorosamente a restituire tale area di
memoria e a disabilitare la linea A20 prima di terminare!
7.4.6 Servizio n. 05h: Local Enable A20
Questo servizio tenta di abilitare la linea A20 in modo da rendere possibile
l'accesso alla memoria oltre il primo MiB.
Questo servizio deve essere usato esclusivamente da un programma intenzionato ad
accedere in I/O alla HMA già allocata da un altro programma che ne ha
il controllo diretto; lo stesso programma che richiede il servizio n.05h deve
poi provvedere a disabilitare la linea A20 con il servizio n. 06h!
7.4.7 Servizio n. 06h: Local Disable A20
Questo servizio tenta di disabilitare la linea A20.
Questo servizio deve essere usato esclusivamente da un programma intenzionato ad
accedere in I/O alla HMA già allocata da un altro programma che ne ha
il controllo diretto; prima di terminare, il programma che ha eseguito l'accesso in
I/O alla HMA deve provvedere rigorosamente a disabilitare localmente
la linea A20 con il servizio n.06h!
7.4.8 Servizio n. 07h: Query A20
Questo servizio verifica lo stato della linea A20.
Questo servizio verifica se la linea A20 è abilitata o meno; a tale proposito,
viene tentato l'accesso all'indirizzo logico FFFFh:0010h per verificare se si
ottiene (linea A20 disabilitata) o meno (linea A20 abilitata) un wrap
around!
In generale, si tenga presente che su molti computer, tutte le operazioni che hanno
a che fare con la linea A20 possono risultare relativamente lente; proprio per
questo motivo, i programmi DOS che fanno un uso intensivo dei servizi XMS
soffrono di prestazioni non particolarmente brillanti!
7.4.9 Servizio n. 08h: Query Free Extended Memory
Questo servizio restituisce la dimensione del più grande EMB disponibile.
Questo servizio restituisce informazioni relative alla dimensione totale in KiB della
memoria estesa presente sul computer (DX) e alla dimensione in KiB del più
grande EMB libero; tali informazioni non comprendono i 65520 byte
della HMA.
7.4.10 Servizio n. 09h: Allocate Extended Memory Block
Questo servizio tenta di allocare un EMB per un programma.
Questo servizio tenta di allocare un blocco di memoria estesa (EMB) per un
programma; la dimensione richiesta deve essere espressa in KiB.
In caso di successo, viene restituito in DX un cosiddetto handle
(maniglia) e cioè, un valore a 16 bit che identifica univocamente l'EMB
appena allocato; l'handle viene utilizzato in tutte le operazioni che
coinvolgono l'EMB ad esso associato.
Gli handle sono disponibili in quantità relativamente limitata; in assenza di
altre indicazioni, la quantità predefinita è 32. Se necessario, si può
incrementare tale valore attraverso il parametro NUMHANDLES che permette di
specificare un numero compreso tra 1 e 128; possiamo scrivere, ad
esempio:
DEVICE=C:\DOS\HIMEM.SYS /NUMHANDLES=64
7.4.11 Servizio n. 0Ah: Free Extended Memory Block
Questo servizio tenta di deallocare un EMB precedentemente allocato da un
programma.
A differenza di quanto accade per i normali blocchi di memoria DOS, un EMB
non viene automaticamente deallocato quando il programma che lo aveva in uso termina;
tale compito deve essere svolto rigorosamente dal programma stesso!
L'handle dell'EMB appena deallocato non è più valido e quindi non deve
essere utilizzato per ulteriori operazioni di I/O con l'EMB stesso!
7.4.12 Servizio n. 0Bh: Move Extended Memory Block
Questo servizio tenta di effettuare un trasferimento dati tra una sorgente e una
destinazione.
Questo servizio tenta di effettuare un trasferimento dati tra un blocco sorgente e
un blocco destinazione; il servizio opera principalmente sugli EMB, ma è
permesso anche l'utilizzo di normali blocchi DOS che si trovano nella memoria
base (convenzionale o superiore). In sostanza, possiamo effettuare trasferimenti di
dati tra memoria base e memoria base, tra memoria base e memoria estesa, tra memoria
estesa e memoria base, tra memoria estesa e memoria estesa!
Tutte le informazioni relative al trasferimento dati, devono trovarsi in una apposita
struttura denominata Extended Memory Move Structure (EMMS); tale
struttura assume l'aspetto mostrato in Figura 7.5.
Il valore contenuto in Lenght deve essere un numero pari; anche se non è
obbligatorio, si raccomanda di allineare i blocchi da trasferire, alla WORD
sulle CPU a 16 bit (80286) e alla DWORD sulle
CPU a 32 bit.
Se il blocco sorgente si trova nella memoria base, allora SourceHandle deve
valere 0000h, mentre SourceOffset viene interpretato come un normale
indirizzo logico Seg:Offset a 16+16 bit; come al solito, si faccia
attenzione a rispettare la convenzione Intel per la disposizione in memoria
degli indirizzi logici!
Se il blocco sorgente è un EMB, allora SourceOffset indica un Offset
a 32 bit relativo all'indirizzo iniziale dello stesso blocco sorgente; ad esempio,
00000000h indica uno spiazzamento nullo relativo all'indirizzo iniziale del blocco
sorgente, mentre 0008FB42h indica uno spiazzamento pari a 588610 byte
relativo all'indirizzo iniziale del blocco sorgente.
Considerazioni del tutto identiche valgono anche per DestHandle e DestOffset.
Se il blocco sorgente e il blocco destinazione sono parzialmente o totalmente sovrapposti,
è obbligatorio che l'indirizzo iniziale del blocco sorgente sia minore dell'indirizzo
iniziale del blocco destinazione; in caso contrario, si ottiene il codice di errore
BL=A8h!
Come si nota in Figura 7.5, il membro Lenght ha un'ampiezza di 32 bit per cui,
teoricamente, potremmo effettuare trasferimenti di dati tra blocchi grandi sino a 4
GiB; inoltre, grazie a questa caratteristica, il servizio 0Bh può essere sfruttato
per aggirare la segmentazione a 64 KiB relativa ai trasferimenti di dati che
coinvolgono la memoria base!
Nel caso di trasferimenti di dati tra blocchi particolarmente grandi, il servizio
0Bh provvede a garantire un adeguato numero di "finestre temporali" durante le
quali la CPU può elaborare eventuali interruzioni hardware in attesa; in questo
modo si evitano gravi rallentamenti generali del sistema.
I programmi che richiedono servizi XMS relativi agli EMB, non devono
assolutamente modificare lo stato della linea A20 (ad esempio, non è necessario
abilitare la linea A20 per accedere ad un EMB); infatti, a differenza di
quanto accade per la HMA, quando si opera sugli EMB tale lavoro viene
svolto in automatico dagli stessi servizi XMS!
7.4.13 Servizio n. 0Ch: Lock Extended Memory Block
Questo servizio tenta di bloccare la posizione di un blocco di memoria già allocato
da un programma.
Le varie operazioni di allocazione e deallocazione degli EMB compiute dai
programmi, possono portare al classico fenomeno della frammentazione; per
ovviare a questo problema, un XMM potrebbe decidere ad un certo punto di
riorganizzare la disposizione in memoria degli stessi EMB.
Una tale situazione può creare seri problemi a quei programmi che, in quel momento,
stanno operando su EMB presupponendo che essi si trovino in posizione fissa;
per evitare sorprese, un programma può allora utilizzare il servizio 0Ch
attraverso il quale è possibile richiedere il bloccaggio temporaneo della posizione
di un EMB già allocato dal programma stesso.
In caso di successo, il servizio 0Ch restituisce in DX:BX l'indirizzo
fisico a 32 bit dell'EMB appena bloccato; il programma che ha
richiesto il servizio può quindi utilizzare tale indirizzo per accedere in modo
diretto all'EMB stesso. Come è facile intuire, si tratta di un servizio
destinato principalmente ai SO che operano in modalità protetta.
Un EMB deve essere tenuto bloccato da un programma per il minor tempo
possibile.
Il servizio 0Ch non deve essere usato per gestire il trasferimento dati tra
blocchi di memoria (servizio 0Bh).
L'XMM è dotato di un lock counter (contatore dei bloccaggi) per tenere
traccia dello stato (bloccato/sbloccato) dei vari EMB.
7.4.14 Servizio n. 0Dh: Unlock Extended Memory Block
Questo servizio tenta di sbloccare la posizione di un blocco di memoria precedentemente
bloccato da un programma.
Un programma che ha precedentemente bloccato un EMB, deve provvedere a sbloccarlo
al più presto con il servizio 0Dh; subito dopo lo sbloccaggio, l'indirizzo a
32 bit dell'EMB (ottenuto dal servizio 0Ch) potrebbe non essere più
valido e quindi non deve essere utilizzato per altre operazioni di I/O!
7.4.15 Servizio n. 0Eh: Get EMB Handle Information
Questo servizio tenta di ottenere informazioni addizionali relative ad un EMB
allocato da un programma.
Il servizio 0Eh restituisce una serie di informazioni addizionali relative ad
un EMB allocato da un programma; se si desidera conoscere anche l'indirizzo
fisico iniziale a 32 bit dell'EMB stesso, bisogna ricorrere al servizio
0Ch.
7.4.16 Servizio n. 0Fh: Reallocate Extended Memory Block
Questo servizio tenta di ridimensionare un EMB allocato da un programma.
Il servizio 0Fh tenta di ridimensionare un EMB allocato da un programma;
l'EMB può essere ridimensionato solo se è stato precedentemente sbloccato.
Se la nuova dimensione è minore di quella vecchia, tutti i dati che si trovavano nella
parte finale (eccedente) del vecchio EMB vengono persi!
7.4.17 Servizio n. 10h: Request Upper Memory Block
Questo servizio tenta di allocare un UMB per un programma.
Il servizio 10h tenta di allocare un UMB per un programma; ovviamente,
trovandosi la upper memory al di sotto del primo MiB, non è richiesta alcuna
abilitazione della linea A20!
Come già sappiamo, gli UMB sono paragraph aligned per cui, in caso di
successo, il servizio 10h restituisce in BX la sola componente
Seg dell'indirizzo logico iniziale dell'UMB stesso; la componente
Offset è implicitamente 0000h.
Analogamente a quanto abbiamo visto nel precedente capitolo, se vogliamo conoscere
la dimensione in paragrafi del più grande UMB disponibile dobbiamo chiamare
il servizio 10h ponendo DX=FFFFh.
7.4.18 Servizio n. 11h: Release Upper Memory Block
Questo servizio tenta di deallocare un UMB precedentemente allocato da un
programma.
Il servizio 11h tenta di deallocare un UMB precedentemente allocato da
un programma; dopo la deallocazione, tutte le informazioni contenute nel blocco non
sono più valide!
7.4.19 Servizio n. 12h: Reallocate Upper Memory Block
Questo servizio tenta di ridimensionare un UMB allocato da un programma.
Il servizio 12h tenta di ridimensionare un UMB precedentemente allocato
da un programma; se la nuova dimensione è minore di quella vecchia, tutti i dati che
si trovavano nella parte finale (eccedente) del vecchio UMB vengono persi!
7.5 Libreria XMSLIB
Visto e considerato che i servizi XMS sono standard, la cosa migliore da fare
consiste nello scriversi una apposita libreria linkabile a tutti i programmi che ne
hanno bisogno; nella sezione
Librerie di supporto per il corso assembly dell’
Area Downloads di questo sito,
è presente una libreria, denominata XMSLIB, che può essere
linkata ai programmi destinati alla generazione di eseguibili in formato EXE.
All'interno del pacchetto xmslibexe.zip è presente la documentazione, la
libreria vera e propria XMSLIB.ASM, l'include file XMSLIBN.INC per il
NASM, l'include file XMSLIBM.INC per il MASM e l'header file
XMSLIB.H per eventuali programmi scritti in linguaggio C (purché
destinati sempre alla modalità reale).
Per la creazione dell'object file di XMSLIB.ASM è richiesta la presenza
della libreria di macro LIBPROC.MAC (Capitolo 29 della sezione Assembly Base
con NASM); l'assemblaggio consiste nel semplice comando:
nasm -f obj xmslib.asm
L'object file così ottenuto è perfettamente linkabile anche ai programmi
scritti con MASM; a tale proposito, è necessario ricordarsi di chiamare le varie
procedure della libreria secondo le convenzioni C (passaggio dei parametri da
destra verso sinistra e pulizia dello stack a carico del caller).
Per esercizio si può anche provare a convertire XMSLIB.ASM in formato COM;
a tale proposito, nella eventualità di volersi interfacciare anche all'altra libreria
COMLIB.OBJ, bisogna ricordarsi di utilizzare un segmento unico avente le seguenti
caratteristiche:
COMSEGM SEGMENT PARA PUBLIC USE16 'CODE'
La possibilità di linkare la libreria ai programmi scritti in C è legata al fatto
che le varie procedure definite in XMSLIB seguono le convenzioni C per il
passaggio degli argomenti e per il valore di ritorno; da notare, inoltre, la presenza
degli underscore all'inizio di ogni identificatore globale definito nella libreria
stessa. Più avanti vedremo un esempio pratico.
Analizzando il codice sorgente della libreria XMSLIB.ASM, notiamo subito il metodo
di chiamata alla funzione di controllo:
Nel caso di MASM dobbiamo usare la sintassi:
call dword ptr cs:XMSControlFunc ; chiamata indiretta intersegmento
o
call dword ptr [cs:XMSControlFunc] ; chiamata indiretta intersegmento
La variabile XMSControlFunc a 32 bit è definita all'interno del blocco
codice XMSLIBCODE ed è destinata a contenere l'indirizzo Seg:Offset della
control function; quando l'esecuzione del programma salta al codice definito in
tale blocco, abbiamo sicuramente CS=XMSLIBCODE per cui, in quel momento:
cs:XMSControlFunc
rappresenta l'indirizzo logico Seg:Offset della variabile XMSControlFunc.
Analogamente:
[cs:XMSControlFunc]
rappresenta il contenuto della variabile XMSControlFunc!
Senza il segment override, la variabile XMSControlFunc verrebbe associata a
DS il quale, in quel momento, sta puntando a DATASEGM!
Diverse procedure della libreria richiedono argomenti passati per indirizzo; tale
indirizzo deve essere sempre di tipo FAR!
Ricordando le convenzioni C per il passaggio degli argomenti (da destra verso
sinistra), dobbiamo stare attenti quindi a mettere nella lista di chiamata, prima la
componente Offset e poi la componente Seg della variabile da passare
per indirizzo; in questo modo, la componente Offset si troverà a precedere la
componente Seg in memoria, nel rispetto della convenzione Intel (con
il Pascal avremmo dovuto disporre le due componenti in ordine inverso).
All'interno delle procedure, le variabili passate per indirizzo vengono gestite con
la coppia DS:SI inizializzata con l'istruzione LDS; come sappiamo, tale
istruzione lavora in modo corretto solo quando la coppia Seg:Offset, da
caricare in DS:SI, si trova disposta in memoria (in questo caso, nello stack)
secondo la convenzione Intel!
7.6 Esempi pratici
Prima di procedere con alcuni esempi pratici, è necessario predisporre l'ambiente
operativo in modo che la memoria estesa e la HMA vengano attivate e rese
utilizzabili; a tale proposito, si deve procedere diversamente a seconda del
SO che si sta usando.
Nel caso del DOS vero e proprio (anche eseguito sotto VirtualBox), è
necessario editare il file C:\CONFIG.SYS; al suo interno dovrebbe essere
presente una linea del tipo:
DEVICE=C:\DOS\HIMEM.SYS
Se questa linea non è presente deve essere aggiunta dall'utente; in tal caso, è
anche necessario riavviare il computer (o riavviare la macchina virtuale sotto
VirtualBox).
Come sappiamo, HIMEM.SYS è il driver più diffuso per il supporto dell'XMS;
bisogna ricordare comunque che esistono anche altri XMM altrettanto validi come,
ad esempio, 386MAX, QEMM e NETROOM.
Su alcune versioni del DOS il driver potrebbe anche avere nomi del tipo
HIMEM.COM o HIMEM.EXE; inoltre, abbiamo anche visto che il driver stesso
accetta diversi parametri come, ad esempio, /HMAMIN=n e /NUMHANDLES=m.
Se abbiamo intenzione di accedere alla HMA via XMS, dobbiamo ricordarci
di commentare, sempre in C:\CONFIG.SYS, la linea DOS=HIGH; nel caso in
cui siano presenti altri parametri (ad esempio, DOS=HIGH,UMB,AUTO) basta
togliere solamente HIGH o sostituirlo con LOW. Analoghe considerazioni
per l'impostazione DOS=UMB, necessaria nel caso in cui si voglia accedere
alla memoria superiore; anche tali modifiche diventano attive solo dopo il riavvio
del computer.
Nel caso di FreeDOS (anche eseguito sotto VirtualBox), in fase di
avvio bisogna selezionare il Menu 1:
Load FreeDOS with JEMMEX, no EMS (most UMBs), max RAM free
Inoltre, nel file C:\FDCONFIG.SYS bisogna modificare la riga:
123?DOS=HIGH
in modo da avere
123?DOS=LOW
Nel caso di un emulatore come DOSEmu per Linux, i file di avvio della
finestra DOS sono i soliti AUTOEXEC.BAT e CONFIG.SYS (che si
trovano nella directory scelta come radice C:\ dell'emulatore stesso); per
quanto riguarda le impostazioni della memoria, basta editare il file
$HOME/.dosemurc, apportare le modifiche desiderate nella sezione Memory
settings e riavviare l'emulatore.
7.6.1 Accesso alla HMA via XMS
Partiamo con un esempio il cui scopo è quello di permettere ad un programma DOS
di richiedere la disponibilità della HMA; la Figura 7.6 illustra il relativo
listato.
Prima di tutto, il programma effettua una serie di test diagnostici per verificare la
presenza del driver XMM, la disponibilità della HMA, etc; per ogni test
viene visualizzato un apposito messaggio informativo.
Successivamente, il programma tenta di allocare la HMA e di abilitare la linea
A20; se l'operazione riesce, viene effettuato un trasferimento dati che consiste
nel sistemare in HMA una stringa formata da 80x24=1920 elementi di tipo
BYTE, ciascuno dei quali contiene il codice
ASCII della lettera 'A'.
Per il trasferimento dati viene usata l'istruzione REP STOSD con
CX=480; infatti, un vettore di 1920 elementi di tipo BYTE può
essere trasferito molto più velocemente quanto viene gestito come un vettore di
480 elementi di tipo DWORD.
Come si può notare, l'operando destinazione di STOSD è ES:DI che,
inizialmente, punta a FFFFh:0010h (indirizzo logico iniziale della HMA);
ciò dimostra che il programma di Figura 7.6, pur operando in modalità reale, riesce a
scrivere nel blocco da 65520 byte che si trova disposto in memoria subito dopo
il primo MiB di RAM!
Come sappiamo, l'istruzione CLD pone a zero il flag DF facendo in modo
che REP STOSD si occupi anche dell'incremento automatico del puntatore DI;
nel nostro caso, terminato il trasferimento dati, DI punta alla fine della
stringa per cui l'istruzione successiva aggiunge alla stringa stessa un carattere
'$' richiesto dal servizio Display String della INT 21h.
Subito dopo, la stringa viene fatta puntare da DS:DX=FFFFh:0010h per essere
visualizzata, appunto, con il servizio 09h della INT 21h; infine, il
programma termina dopo aver rigorosamente disabilitato la linea A20 e
restituito la HMA!
7.6.2 Conversione in linguaggio C del programma HMATEST.ASM
Come è stato già anticipato, la libreria XMSLIB può essere linkata anche ai
programmi scritti in linguaggio C (purché destinati alla modalità reale); la
Figura 7.7 mostra la traduzione in linguaggio C (Borland C) del precedente
esempio HMATEST.ASM.
Per la generazione dell'eseguibile, è necessaria la presenza dell'header file
XMSLIB.H (disponibile nel pacchetto xmslibexe.zip) e dell'object
file XMSLIB.OBJ; a tale proposito, dobbiamo impartire il comando:
nasm -f obj xmslib.asm
A questo punto possiamo chiamare il compilatore Borland C con il comando:
bcc -ml -3 hmatest2.c xmslib.obj
Osserviamo, in particolare, il parametro -ml passato al compilatore; come
sappiamo, tale parametro dice al Borland C che vogliamo usare il modello di
memoria large.
Questo aspetto è fondamentale in quanto tutte le procedure definite in XMSLIB
e tutti gli argomenti che esse richiedono per indirizzo, sono di tipo FAR; in
presenza del parametro -ml, anche il compilatore tratta, implicitamente, tutti
i puntatori e tutte le funzioni esterne come se fossero di tipo FAR (a meno
che il programmatore non specifichi indicazioni differenti attraverso gli operatori
di distanza NEAR o FAR).
Analizzando il codice sorgente di Figura 7.7 notiamo, all'inizio della funzione
main, la definizione:
Uchar *HMA_pointer = (Uchar *)MK_FP(0xFFFF, 0x0010);
MK_FP (Make Far Pointer) è una semplicissima macro del Borland C che
crea un puntatore FAR generico (void *) a partire da una coppia
Seg:Offset; è necessario quindi usare un cast per indicare a
MK_FP che la variabile HMA_Pointer è un puntatore a Uchar
(intero senza segno a 8 bit).
Nel nostro caso, HMA_pointer viene inizializzato con l'indirizzo logico
FFFFh:0010h; come sappiamo, si tratta dell'indirizzo logico da cui inizia la
HMA.
Osserviamo ora il codice che trasferisce 1920 byte (ASCII('A')=41h) in
HMA; siccome HMA_pointer è un puntatore a Uchar, avremmo dovuto
scrivere:
Se, però, vogliamo trasferire 4 byte alla volta, dobbiamo usare il cast
(Ulong *) per indicare al compilatore che, in questo caso particolare, vogliamo
che HMA_pointer venga trattato come puntatore a Ulong (intero senza segno
a 32 bit); possiamo quindi scrivere:
Infine, consideriamo la seguente istruzione che visualizza la stringa memorizzata in
HMA:
printf("%s", HMA_pointer);
In questo caso, printf ha bisogno di un puntatore FAR alla stringa da
visualizzare; e infatti, HMA_pointer è proprio un puntatore FAR alla
nostra stringa che si trova all'indirizzo logico FFFFh:0010h.
Utilizzando una sintassi ridondante, avrebbe quindi senso scrivere anche:
printf("%s", *(&HMA_pointer));
Il simbolo *(&HMA_pointer) rappresenta, infatti, il contenuto
(FFFFh:0010h) dell'indirizzo in cui si trova memorizzata la variabile
HMA_pointer!
Le considerazioni appena esposte dimostrano ulteriormente per quale motivo il
linguaggio C venga definito un Assembly di alto livello; inoltre,
appare anche evidente il fatto che la conoscenza dell'Assembly permette di
sfruttare al massimo i potenti strumenti offerti dai linguaggi di alto livello!
7.6.3 Accesso alla memoria estesa via XMS
La Figura 7.8 illustra un altro esempio del tutto analogo a quello di Figura 7.6;
l'unica differenza è data dal fatto che al posto della HMA utilizziamo un
EMB.
Inizialmente il programma mostra i soliti messaggi diagnostici; in questo caso, viene
mostrata anche la quantità totale di memoria estesa e la dimensione del più grande
EMB libero.
In seguito, il programma alloca un UMB da 121 paragrafi (1936
byte) e un EMB da 2 KiB (2048 byte); questi due blocchi vengono
utilizzati per lo scambio di dati tra memoria base e memoria estesa. Si noti che per
poter allocare blocchi di memoria DOS, il programma deve prima liberare la
memoria che occupa in eccesso; in base alle informazioni ricavate dal map file,
la dimensione del program segment viene ridotta a 480 paragrafi.
Come sappiamo, prima di richiedere un UMB dobbiamo attivare l'UML e
modificare opportunamente la strategia di allocazione del DOS; questi due
passaggi devono essere svolti in ordine rigoroso (prima l'attivazione dell'UML
e poi la modifica della strategia di allocazione).
A questo punto, l'UMB viene riempito con una stringa di 1920 lettere
'X' più un '$' finale, per un totale di 1921 byte; tale stringa
viene poi trasferita nell'EMB come se fosse un blocco unico da 1936
byte o 121 paragrafi (si ricordi che i trasferimenti di dati che coinvolgono
la memoria estesa devono avere una lunghezza in byte multipla di 2).
Successivamente, l'UMB viene ripulito attraverso 1920 spazi (' ')
e la stringa precedente viene ritrasferita dall'EMB all'UMB stesso;
l'ultimo passo consiste nel visualizzare la stringa appena trasferita.
Per l'allocazione e la deallocazione dell'UMB il programma di Figura 7.8 si
serve dei normali servizi DOS; questa è una pratica molto comune anche perché,
come sappiamo, lo standard XMS 3.0 considera facoltativo il supporto dei servizi
per la gestione della memoria superiore!
Bibliografia
eXtended Memory Specifications (XMS), ver. 3.0
(disponibile nella sezione Downloads - Documentazione - xms30.zip)