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