Assembly Base con MASM

Capitolo 27: I modelli di memoria


In base a quanto abbiamo visto nei precedenti capitoli, un generico programma scritto con un qualunque linguaggio di programmazione può essere suddiviso in tre blocchi fondamentali (o program segments) destinati a contenere, il codice, i dati e lo stack; lo stack segment contiene i dati temporanei (dinamici) del programma, il data segment contiene i dati permanenti (statici) del programma, mentre il code segment contiene le istruzioni che elaborano i dati.

Sappiamo, inoltre, che il DOS inizializza il proprio ambiente operativo in modo che ciascun program segment sia accessibile, contemporaneamente, in lettura, in scrittura e in esecuzione; ciò significa che è possibile creare anche dei program segments al cui interno sono presenti, contemporaneamente, codice, dati e stack.
In generale, è possibile creare programmi dotati delle strutture più varie, dalla più semplice alla più complessa; l'importante è che siano rispettate tutte le regole relative all'indirizzamento del codice, dei dati e dello stack.

In base alla struttura che lo caratterizza, un programma Assembly può assumere tutta una serie di possibili configurazioni distinte, corrispondenti a diverse esigenze di memoria; nella terminologia dei linguaggi di alto livello, queste configurazioni vengono anche chiamate modelli di memoria.
In questo capitolo vengono illustrate in dettaglio le principali configurazioni che può assumere un programma Assembly; tutte le considerazioni che seguono sono riferite, come al solito, alla modalità di indirizzamento reale 8086.
Per il momento, continuiamo a fare riferimento a programmi Assembly interamente contenuti in un unico file; nel prossimo capitolo vedremo come distribuire un programma Assembly su due o più file.

27.1 Convenzioni DOS per i programmi eseguibili

Prima di proseguire, è necessario riassumere tutte le convenzioni seguite dal DOS per l'inizializzazione di un programma eseguibile; analizziamo quindi i due casi che si possono presentare e che sono relativi, come sappiamo, al formato EXE e al formato COM.

27.1.1 Convenzioni DOS per il formato EXE

Non appena un eseguibile EXE viene caricato in memoria per la fase di esecuzione, il SO inizializza la coppia CS:IP facendola puntare all'indirizzo logico dell'entry point del programma; tale entry point deve essere specificato, obbligatoriamente, dal programmatore.
Un programma eseguibile, indipendentemente dal formato, deve essere dotato di un unico entry point; l'indirizzo logico dell'entry point coincide con quello della prima istruzione del programma che verrà eseguita dalla CPU.
La CPU tratta CS come registro di segmento naturale per il codice (code segment register); qualsiasi componente Offset presente in IP, viene automaticamente associata dalla CPU a CS.
Durante la fase di esecuzione del programma, il compito di gestire la coppia CS:IP spetta rigorosamente alla CPU; istante per istante, i registri CS e IP vengono continuamente aggiornati dalla logica di controllo in modo da puntare alla prossima istruzione eseguibile.

Un eventuale segmento di programma dotato di attributo di combinazione STACK, viene utilizzato dal SO per inizializzare automaticamente la coppia SS:SP; tale inizializzazione consiste nel far puntare SS:SP all'indirizzo più alto (con offset pari) del segmento stesso.
Nel caso in cui non sia presente alcun segmento di programma con attributo di combinazione STACK, l'inizializzazione della coppia SS:SP spetta al programmatore; come vedremo più avanti, ciò che dobbiamo fare consiste nel simulare lo stesso comportamento del SO.
La CPU tratta SS come registro di segmento naturale per lo stack (stack segment register); qualsiasi componente Offset specificata attraverso i registri puntatori SP e BP, viene automaticamente associata dalla CPU al registro SS.
Durante la fase di esecuzione del programma, il compito di gestire la coppia SS:SP spetta (salvo rare eccezioni) alla CPU; il programmatore può, invece, utilizzare liberamente BP per accedere allo stack.

Il SO provvede anche ad inizializzare i registri DS e ES assegnando a ciascuno di essi il paragrafo di memoria da cui inizia il PSP; quando parte la fase di esecuzione del programma abbiamo quindi, DS=PSP e ES=PSP.
L'indirizzo fisico del PSP precede di 256 byte l'indirizzo fisico da cui inizia il programma vero e proprio; di conseguenza, al programmatore viene delegato il compito di modificare il contenuto di DS e/o ES per referenziare eventuali dati statici del programma. Sulle CPU 80386 e superiori sono disponibili anche i registri FS e GS.
La CPU tratta DS come registro di segmento naturale per i dati (data segment register); qualsiasi componente Offset specificata attraverso i registri puntatori BX, SI e DI, viene automaticamente associata dalla CPU al registro DS.

27.1.2 Convenzioni DOS per il formato COM

Non appena un eseguibile COM viene caricato in memoria per la fase di esecuzione, il SO inizializza la coppia CS:IP facendola puntare all'indirizzo logico dell'entry point del programma; tale entry point deve essere specificato, obbligatoriamente, dal programmatore.
L'entry point deve essere unico e, necessariamente, viene a trovarsi nell'unico program segment presente; l'indirizzo logico dell'entry point deve avere, tassativamente, una componente Offset pari a 0100h (256d).
È proibito inserire qualsiasi istruzione o dato inizializzato prima dell'offset 0100h; infatti, nei 256 byte iniziali dell'unico program segment presente viene inserito il PSP. In base a queste considerazioni, possiamo affermare che la coppia CS:IP verrà inizializzata con l'indirizzo logico PSP:0100h.
La CPU tratta CS come registro di segmento naturale per il codice (code segment register); qualsiasi componente Offset presente in IP, viene automaticamente associata dalla CPU a CS.
Durante la fase di esecuzione del programma, il compito di gestire la coppia CS:IP spetta rigorosamente alla CPU; istante per istante, i registri CS e IP vengono continuamente aggiornati dalla logica di controllo in modo da puntare alla prossima istruzione eseguibile.

Il compito di inizializzare la coppia SS:SP spetta rigorosamente al SO; tale inizializzazione consiste nel far puntare SS:SP all'offset FFFEh dell'unico program segment presente. In virtù del fatto che esiste un unico program segment, possiamo affermare che la coppia SS:SP verrà inizializzata con l'indirizzo logico PSP:FFFEh.
La CPU tratta SS come registro di segmento naturale per lo stack (stack segment register); qualsiasi componente Offset specificata attraverso i registri puntatori SP e BP, viene automaticamente associata dalla CPU al registro SS.
Durante la fase di esecuzione del programma, il compito di gestire la coppia SS:SP spetta (salvo rare eccezioni) alla CPU; il programmatore può, invece, utilizzare liberamente BP per accedere allo stack.

Il SO provvede anche ad inizializzare i registri DS e ES assegnando a ciascuno di essi il paragrafo di memoria da cui inizia il PSP; quando parte la fase di esecuzione del programma abbiamo quindi, DS=PSP, ES=PSP, SS=PSP e CS=PSP. Se vogliamo accedere ad eventuali dati statici definiti nell'unico program segment presente, non dobbiamo quindi apportare alcuna modifica al contenuto di DS e/o ES.
La CPU tratta DS come registro di segmento naturale per i dati (data segment register); qualsiasi componente Offset specificata attraverso i registri puntatori BX, SI e DI, viene automaticamente associata dalla CPU al registro DS.

27.2 Il modello di memoria TINY

Supponiamo voler scrivere un programma per il quale la somma complessiva delle dimensioni del blocco codice, del blocco dati e del blocco stack non superi i 65536 byte; in un caso del genere esiste la possibilità di inserire codice, dati e stack in un unico segmento di programma!
Naturalmente, sarebbe meglio evitare il più possibile un tale "miscuglio" di informazioni; in ogni caso, con un minimo di buon senso e di razionalità è possibile scrivere ugualmente programmi "monosegmento" semplici e comprensibili.
Nella terminologia dei linguaggi di alto livello, un programma formato da un unico segmento contenente codice, dati e stack, viene definito programma con modello di memoria TINY; il termine TINY, in inglese, significa minuscolo.

In base a quanto è stato spiegato in precedenza, un programma con modello di memoria TINY può dare vita ad un eseguibile in formato EXE o COM; analizziamo in dettaglio queste due possibilità.

27.2.1 Programma TINY in formato EXE

Creiamo un programma dotato di un unico program segment a cui assegnamo il nome simbolico TINYSEGM; all'interno di TINYSEGM definiamo l'entry point attraverso una etichetta a cui assegnamo il nome simbolico start.
Assegnamo al programma il nome TINY1.ASM e convertiamolo in formato EXE ottenendo così l'eseguibile TINY1.EXE; sulla base delle considerazioni svolte in precedenza, subito dopo il caricamento in memoria, il programma TINY1.EXE assumerà la struttura mostrata in Figura 27.1. Come si può notare, il SO inizializza automaticamente la coppia CS:IP con l'indirizzo logico TINYSEGM:Offset(start); a CS viene assegnato il paragrafo di memoria da cui parte TINYSEGM, mentre a IP viene assegnato l'offset di start calcolato, ovviamente, rispetto allo stesso TINYSEGM.
L'entry point può trovarsi in un punto qualunque di TINYSEGM; nell'area compresa tra l'inizio di TINYSEGM e start possiamo inserire qualunque tipo di informazione. Spesso quest'area viene sfruttata per le definizioni dei dati statici e delle procedure; in alternativa, possiamo utilizzare la solita area che segue l'exit point.

Nel caso generale, un segmento unico come TINYSEGM ha un attributo di combinazione diverso da STACK (ad esempio, PUBLIC); di conseguenza, il SO non effettua alcuna inizializzazione automatica della coppia SS:SP. Tale compito spetta quindi al programmatore; vediamo allora quali possono essere i valori da caricare in SS:SP.
L'unico program segment presente è destinato a contenere codice, dati e stack; di conseguenza, appare ovvio che si debba porre, necessariamente, SS=CS. Per quanto riguarda il valore da caricare in SP dobbiamo ricordare che lo stack è una struttura di tipo LIFO che, nei programmi, viene disposta in modo che inizi a riempirsi a partire dagli indirizzi più alti; ciò significa che conviene assegnare a SP l'offset (di indice pari) più alto di TINYSEGM.
La soluzione più semplice consiste nel creare alla fine di TINYSEGM un vettore di BYTE di dimensioni opportune (ad esempio, 1024 byte) e rigorosamente allineato alla WORD; subito dopo tale vettore possiamo creare una etichetta il cui offset verrà appunto assegnato a SP.

Il SO inizializza DS e ES ponendo DS=PSP e ES=PSP; se vogliamo indirizzare i dati statici presenti in TINYSEGM ci conviene quindi porre almeno DS=CS; in questo modo abbiamo la possibilità di evitare numerosi segment override per l'indirizzamento degli stessi dati statici.

È anche opportuno l'utilizzo di una direttiva ASSUME che associa TINYSEGM a DS, SS e CS; se necessario, possiamo servirci anche di ES, FS e GS.

Indicando allora con stack_label l'etichetta che delimita l'indirizzo più alto dello stack, possiamo affermare che le inizializzazioni a carico del programmatore assumeranno il seguente aspetto: A questo punto abbiamo quindi, ES=PSP, CS=TINYSEGM, DS=TINYSEGM e SS=TINYSEGM; inoltre, IP contiene l'offset di start, mentre SP contiene l'offset di stack_label.

In Figura 27.2 vediamo un programma di esempio che illustra in pratica i concetti appena esposti; vengono utilizzate le convenzioni C per il passaggio degli argomenti e per la pulizia dello stack. Osserviamo subito che il programma di Figura 27.2 è dotato di un unico segmento chiamato TINYSEGM; per enfatizzare il fatto che si tratta dell'unico segmento del programma, a TINYSEGM è stato assegnato l'attributo di classe 'SEG_UNICO'.

Le definizioni dei dati statici e delle procedure sono state inserite nell'area compresa tra l'inizio di TINYSEGM e l'etichetta start. Il listato di Figura 27.2 mostra anche le altre aree adatte a contenere i dati statici e le procedure; naturalmente, il programmatore è libero di complicarsi la vita trovando soluzioni più contorte di quelle illustrate nell'esempio.
La procedura printStr1 è di tipo NEAR e richiede come parametro l'indirizzo NEAR di una stringa da visualizzare; a tale proposito, viene utilizzato il servizio 09h (Display String) della INT 21h. Questo servizio presuppone che la stringa da visualizzare sia puntata da DS:DX; ovviamente, non abbiamo bisogno di modificare DS in quanto tale registro contiene già la componente Seg (TINYSEGM) dell'indirizzo logico della stringa.
La procedura printStr2 è di tipo FAR e richiede come parametro l'indirizzo FAR di una stringa da visualizzare; questa procedura ha lo scopo di dimostrare che anche in presenza di un unico program segment possiamo trovarci a gestire indirizzi FAR per i dati e per il trasferimento del controllo (ciò accade, ad esempio, con le ISR). Osserviamo che questa volta viene modificato anche DS per cui è fondamentale preservare il contenuto originale di tale registro; ovviamente, in questo caso la modifica di DS è del tutto superflua ed ha un puro scopo didattico.

Per la corretta gestione dello stack, nella parte finale di TINYSEGM inseriamo una direttiva ALIGN 2; in questo modo, tutto ciò che segue verrà allineato alla WORD.
Definiamo poi un vettore vstack formato da un numero rigorosamente pari (1024) di byte non inizializzati; subito dopo viene creata l'etichetta stack_label il cui offset viene assegnato a SP. Il nostro stack inizia quindi a riempirsi a partire da stack_label ed ha una capienza di 1024 byte; se, in fase di esecuzione, superiamo tale capienza, andremo a sovrascrivere le istruzioni del programma con la conseguenza di un sicuro crash!
Per inizializzare SP potevamo anche scrivere:
mov sp, offset vstack + STACK_SIZE
Se ora proviamo a convertire il programma di Figura 27.2 in un eseguibile di tipo EXE, otteniamo un messaggio di avvertimento (warning) del linker che ci informa della mancanza dello stack; questo messaggio indica che la coppia SS:SP non è stata inizializzata a causa dell'assenza di un program segment con attributo di combinazione STACK. Ovviamente, tale messaggio può essere tranquillamente ignorato purché il programmatore abbia provveduto ad inizializzare manualmente SS:SP!

27.2.2 Programma TINY in formato COM

Nel caso particolare dei programmi monosegmento, possiamo ottenere notevoli semplificazioni strutturali richiedendo al linker la generazione di un eseguibile in formato COM (COre iMage); in questo modo, infatti, buona parte delle inizializzazioni verranno effettuate dal SO!

Creiamo un programma dotato di un unico program segment a cui assegnamo il nome simbolico TINYSEGM; all'interno di TINYSEGM definiamo l'entry point attraverso una etichetta a cui assegnamo il nome simbolico start. Questa volta è fondamentale che start si trovi tassativamente all'offset 0100h di TINYSEGM; inoltre, nell'area compresa tra l'inizio di TINYSEGM e l'etichetta start non possiamo inserire alcuna istruzione o dato inizializzato (è possibile inserire, invece, dati non inizializzati che verranno però sovrascritti dal PSP).
Assegnamo al programma il nome TINY2.ASM e convertiamolo in formato COM ottenendo così l'eseguibile TINY2.COM; sulla base delle considerazioni svolte in precedenza, subito dopo il caricamento in memoria, il programma TINY2.COM assumerà la struttura mostrata in Figura 27.3. Il SO inserisce TINYSEGM in un segmento di memoria completo (pari a 65536 byte); il PSP viene inserito nei primi 256 byte di TINYSEGM, per cui TINYSEGM=PSP.
In seguito, il SO inizializza CS, DS, ES e SS con PSP; di conseguenza, avremo anche, CS=TINYSEGM, DS=TINYSEGM, ES=TINYSEGM e SS=TINYSEGM.
Al registro IP viene assegnato l'offset 0100h; al registro SP viene assegnato l'offset FFFEh (ultimo offset di indice pari all'interno di TINYSEGM).

In Figura 27.4 vediamo un programma di esempio che illustra in pratica i concetti appena esposti; in sostanza, si tratta del programma di Figura 27.2 convertito in formato COM. Osserviamo che le definizioni dei dati statici e delle procedure sono state inserite subito dopo l'entry point; per evitare che la CPU possa finire per sbaglio in quest'area, viene utilizzata una istruzione JMP che provoca un salto incondizionato all'etichetta start_code.
Il listato di Figura 27.4 mostra anche le altre aree adatte a contenere i dati statici e le procedure; l'importante è che non venga utilizzata l'area riservata al PSP!
Le procedure printStr1 e printStr2 sono identiche a quelle dell'esempio di Figura 27.2; come si può notare, anche nei programmi in formato COM possiamo trovarci a gestire indirizzi FAR per i dati e per il trasferimento del controllo.
Si tenga presente che il MASM, in relazione ai programmi in formato COM, non permette l'uso dell'operatore FAR PTR per la chiamata di una procedura FAR; per aggirare questo problema è sufficiente definire le procedure prima della loro chiamata e cioè, prima del blocco di codice principale.

Un ultimo aspetto importante da ricordare riguarda il fatto che nel caso dei programmi in formato COM è proibito qualsiasi riferimento a componenti Seg rilocabili; ciò accade in quanto gli eseguibili in formato COM non sono dotati di header e non permettono quindi al SO di gestire eventuali componenti Seg rilocabili.
Non è possibile quindi scrivere istruzioni del tipo:
mov ax, seg stringa2
Osservando che esiste un unico program segment referenziato da tutti i registri di segmento, possiamo riscrivere la precedente istruzione in questo modo:
mov ax, ds

27.3 Il modello di memoria SMALL

Il modello TINY appena illustrato permette di compattare al massimo il codice, i dati e lo stack di un programma, ottenendo così un notevole risparmio di memoria; gli svantaggi evidenti di tale modello di memoria sono rappresentati dalla scarsa flessibilità e dalla confusione che regna nell'unico program segment presente!

In certi casi, si presenta la necessità di scrivere programmi che richiedono più di 64 KiB per i dati e/o più di 64 KiB per il codice; in una situazione del genere, dobbiamo ricordare che la modalità reale prevede che un qualsiasi program segment non possa superare la dimensione massima di 64 KiB.
La soluzione a questo problema consiste allora nel suddividere i dati in due o più data segments e il codice in due o più code segments; in tal caso:

"benvenuti nell'inferno della segmentazione a 64 KiB!"

Si tenga presente che per lo stack di un programma, nella quasi totalità dei casi, sono più che sufficienti poche migliaia di byte; non avrebbe molto senso quindi il ricorso a programmi dotati di due o più stack segments!

Partiamo allora dal caso più semplice rappresentato da un programma le cui esigenze non superano 64 KiB per i dati e 64 KiB per il codice; in tal caso, possiamo adottare una soluzione molto elegante ed efficiente che consiste nel creare un programma formato da: un unico data segment, un unico code segment e un unico stack segment.
Nella terminologia dei linguaggi di alto livello, un programma dotato della struttura appena descritta viene definito programma con modello di memoria SMALL; il termine SMALL, in inglese, significa piccolo.

Creiamo allora un programma dotato di un blocco dati denominato DATASEGM, un blocco codice denominato CODESEGM e un blocco stack denominato STACKSEGM (con attributo di combinazione STACK); l'entry point dovrà trovarsi, necessariamente, nell'unico blocco codice (CODESEGM) presente e sarà rappresentato da una etichetta denominata start.
Assegnamo al programma il nome SMALL.ASM e convertiamolo in formato EXE ottenendo così l'eseguibile SMALL.EXE; sulla base delle considerazioni svolte in precedenza, subito dopo il caricamento in memoria, il programma SMALL.EXE assumerà la struttura mostrata in Figura 27.5. Il SO inizializza automaticamente la coppia CS:IP; a CS viene assegnato il paragrafo da cui parte CODESEGM, mentre a IP viene assegnato l'offset dell'etichetta start.
In presenza del segmento STACKSEGM con attributo di combinazione STACK, il SO inizializza automaticamente anche la coppia SS:SP; a SS viene assegnato il paragrafo da cui parte STACKSEGM, mentre a SP viene assegnato l'offset più alto all'interno dello stesso STACKSEGM.
I due registri di segmento DS e ES vengono entrambi inizializzati con il paragrafo da cui parte il PSP; se il programmatore vuole utilizzare DS e/o ES per gestire il blocco DATASEGM, deve provvedere alla inizializzazione manuale di tali registri.

In Figura 27.6 vediamo un programma di esempio che illustra in pratica i concetti appena esposti. Naturalmente, in un programma come SMALL.ASM sarebbe opportuno disporre in modo ordinato il codice in CODESEGM, i dati in DATASEGM e lo stack in STACKSEGM; un programmatore Assembly deve essere, però, in grado di padroneggiare qualsiasi situazione. Proprio per questo motivo, nell'esempio di Figura 27.6 notiamo la presenza di dati statici le cui definizioni si trovano in tutti i tre program segments; in questo modo possiamo analizzare le tecniche da impiegare per il corretto indirizzamento dei dati stessi.

La procedura printStr1 è di tipo NEAR e richiede come parametro l'indirizzo NEAR di una stringa da visualizzare attraverso il servizio Display String della INT 21h; questa procedura si aspetta che la stringa stessa sia stata definita in un program segment referenziato da DS.
Possiamo servirci allora di printStr1 per visualizzare facilmente una stringa come stringa1; infatti, tale stringa è stata definita nel blocco DATASEGM che stiamo referenziando con DS. La procedura printStr1 riceve l'offset di stringa1 e lo associa a DS ottenendo così l'indirizzo logico completo Seg:Offset richiesto dal servizio Display String.
Come dobbiamo comportarci, però, con stringa2 e stringa3?

Osserviamo che stringa2 è stata definita nel blocco CODESEGM referenziato da CS; invece, stringa3 è stata definita nel blocco STACKSEGM referenziato da SS.
Per affrontare facilmente questa situazione, possiamo servirci di una procedura come printStr2; tale procedura richiede come parametro l'indirizzo FAR di una stringa da visualizzare attraverso il servizio Display String della INT 21h.
Bisogna anche osservare che printStr2 modifica DS preservandone rigorosamente il contenuto originale; in caso contrario, le modifiche apportate a DS da printStr2 diventerebbero permanenti con conseguente malfunzionamento del programma.

In relazione alla stringa stringa3, tenendo presente che lo stack inizia a riempirsi a partire dagli indirizzi più alti, è importante che le definizioni di eventuali dati statici nel blocco STACKSEGM vengano sistemate all'inizio del blocco stesso; in questo modo si riduce il rischio che SP, decrementandosi, possa sovrascrivere i dati stessi.

Ciascuno dei tre segmenti di cui è dotato il programma di Figura 27.6 può crescere sino a 65536 byte; complessivamente, il programma può quindi raggiungere una dimensione massima pari a:
65536 * 3 = 196608 byte = 192 KiB
Si tratta chiaramente di una dimensione più che sufficiente per soddisfare le esigenze della stragrande maggioranza dei programmi Assembly; proprio per questo motivo, il modello di memoria SMALL è quello più utilizzato nello sviluppo dei programmi.

27.4 Il modello di memoria MEDIUM

Supponiamo di dover scrivere un programma le cui esigenze richiedono meno di 64 KiB di dati e oltre 64 KiB di codice; in un caso del genere ci vediamo costretti, necessariamente, a distribuire il codice su due o più program segments!
Possiamo creare allora un programma dotato di un solo data segment, un solo stack segment e due o più code segments; nella terminologia dei linguaggi di alto livello, un programma strutturato in questo modo viene definito programma con modello di memoria MEDIUM (medio).

Creiamo allora un programma dotato di un blocco dati denominato DATASEGM, un blocco stack denominato STACKSEGM (con attributo di combinazione STACK) e tre blocchi di codice; ai tre blocchi di codice assegnamo i nomi CODESEGM, CODESEGM2 e CODESEGM3.
È importante ricordare che un programma Assembly deve specificare un unico entry point; nel nostro caso, l'entry point viene definito nel blocco CODESEGM attraverso una etichetta denominata start.
Assegnamo al programma il nome MEDIUM.ASM e convertiamolo in formato EXE ottenendo così l'eseguibile MEDIUM.EXE; sulla base delle considerazioni svolte in precedenza, subito dopo il caricamento in memoria, il programma MEDIUM.EXE assumerà la struttura mostrata in Figura 27.7. Il SO inizializza automaticamente la coppia CS:IP; a CS viene assegnato il paragrafo da cui parte CODESEGM, mentre a IP viene assegnato l'offset dell'etichetta start.
In presenza del segmento STACKSEGM con attributo di combinazione STACK, il SO inizializza automaticamente anche la coppia SS:SP; a SS viene assegnato il paragrafo da cui parte STACKSEGM, mentre a SP viene assegnato l'offset più alto all'interno dello stesso STACKSEGM.
I due registri di segmento DS e ES vengono entrambi inizializzati con il paragrafo da cui parte il PSP; se il programmatore vuole utilizzare DS e/o ES per gestire il blocco DATASEGM, deve provvedere alla inizializzazione manuale di tali registri.

In Figura 27.8 vediamo un programma di esempio che illustra in pratica i concetti appena esposti. In un programma con modello di memoria MEDIUM, l'aspetto più interessante da analizzare riguarda il fatto che per passare da un segmento di codice all'altro sono necessari dei salti FAR; infatti, trattandosi di salti intersegmento, la CPU ha bisogno di modificare, sia IP, sia CS.
Per verificare questo aspetto, nel programma di Figura 27.8 vengono definite tre apposite procedure; la procedura printStr1 è di tipo NEAR e quindi può essere chiamata solo dal segmento in cui è stata definita (CODESEGM). Le procedure printStr2 e printStr3 sono, invece, di tipo FAR; di conseguenza, tali procedure possono essere chiamate dall'interno di CODESEGM attraverso FAR call.

Tutte queste tre procedure richiedono come parametro l'indirizzo NEAR di una stringa da visualizzare attraverso il servizio Display String della INT 21h; si assume quindi che tale stringa si trovi ad un indirizzo logico la cui componente Seg è contenuta in DS.

Nel caso in cui si vogliano utilizzare dati definiti in segmenti "non naturali", bisogna ricorrere ad indirizzi di tipo FAR; a tale proposito, osserviamo che nell'esempio di Figura 27.8 sono presenti anche le tre stringhe: strCode1, strCode2 e strCode3 definite, rispettivamente, in CODESEGM, CODESEGM2 e CODESEGM3.
Per poter visualizzare tali stringhe, le tre procedure printStr1, printStr2 e printStr3 ne devono conoscere l'indirizzo completo Seg:Offset; osserviamo che, ad esempio, printStr3 per poter visualizzare strCode3 con il servizio Display String, deve porre DS=CODESEGM3 e deve poi caricare in DX l'offset di strCode3.
Come al solito, le procedure che modificano DS ne devono preservare il contenuto originale; nel caso delle tre procedure di Figura 27.8, se non viene ripristinato DS, il tentativo di visualizzare la stringa ricevuta come argomento (strOffs) fallisce!

Il programma di Figura 27.8 dispone di un segmento dati, un segmento di stack e tre segmenti di codice; ciascuno di questi cinque segmenti può crescere sino a 65536 byte. Complessivamente, il programma può raggiungere la dimensione massima di:
65536 * 5 = 327680 byte = 320 KiB
Al momento di eseguire questo programma, il DOS verifica se in memoria c'è spazio a sufficienza; in caso negativo, viene mostrato un messaggio di errore che, in genere, informa l'utente sul fatto che il programma è troppo grande!
Come è stato spiegato in un precedente capitolo, i programmi DOS hanno a disposizione circa 600 KiB di memoria RAM (convenzionale); per verificare la quantità di memoria convenzionale libera si può utilizzare il comando mem /c dal prompt del DOS.

27.5 Il modello di memoria COMPACT

La situazione opposta rispetto al caso appena esaminato è costituita da un programma le cui esigenze richiedono meno di 64 KiB di codice e oltre 64 KiB di dati; in un caso del genere ci vediamo costretti, necessariamente, a distribuire i dati su due o più program segments!
Possiamo creare allora un programma dotato di un solo code segment, un solo stack segment e due o più data segments; nella terminologia dei linguaggi di alto livello, un programma strutturato in questo modo viene definito programma con modello di memoria COMPACT (compatto).
L'unico code segment presente dovrà contenere, necessariamente, l'entry point del programma; tale entry point può essere rappresentato da una etichetta denominata start.

Creiamo allora un programma dotato di un blocco codice denominato CODESEGM, un blocco stack denominato STACKSEGM (con attributo di combinazione STACK) e tre blocchi di dati; ai tre blocchi di dati assegnamo i nomi DATASEGM, DATASEGM2 e DATASEGM3.
Assegnamo al programma il nome COMPACT.ASM e convertiamolo in formato EXE ottenendo così l'eseguibile COMPACT.EXE; sulla base delle considerazioni svolte in precedenza, subito dopo il caricamento in memoria, il programma COMPACT.EXE assumerà la struttura mostrata in Figura 27.9. Il SO inizializza automaticamente la coppia CS:IP; a CS viene assegnato il paragrafo da cui parte CODESEGM, mentre a IP viene assegnato l'offset dell'etichetta start.
In presenza del segmento STACKSEGM con attributo di combinazione STACK, il SO inizializza automaticamente anche la coppia SS:SP; a SS viene assegnato il paragrafo da cui parte STACKSEGM, mentre a SP viene assegnato l'offset più alto all'interno dello stesso STACKSEGM.
I due registri di segmento DS e ES vengono entrambi inizializzati con il paragrafo da cui parte il PSP; se il programmatore vuole utilizzare DS e/o ES per gestire i blocchi DATASEGM, DATASEGM2 e DATASEGM3, deve provvedere alla inizializzazione manuale di tali registri.

In Figura 27.10 vediamo un programma di esempio che illustra in pratica i concetti appena esposti. In un programma con modello di memoria COMPACT, l'aspetto più interessante da analizzare riguarda la presenza di due o più data segments; questa situazione può essere affrontata in diversi modi.
Con le CPU 8086 abbiamo a disposizione i due registri di segmento DS e ES; possiamo gestire quindi sino a due data segments contemporaneamente.
Con le CPU 80386 e superiori abbiamo a disposizione i due ulteriori registri di segmento FS e GS; in questo caso possiamo gestire quindi sino a quattro data segments contemporaneamente.
Se il nostro programma è dotato di cinque o più data segments, i quattro registri DS, ES, FS e GS non sono più sufficienti; in questo caso, possiamo usare uno o più di tali registri per referenziare a turno i vari segmenti di dati ai quali vogliamo accedere.
In ogni caso, appare evidente che in presenza di due o più data segments, la gestione dei dati comporta un uso massiccio di indirizzi FAR; di conseguenza, ci troveremo alle prese con numerosi segment override che, eventualmente, possiamo delegare all'assembler attraverso le opportune direttive ASSUME.

Nel programma di Figura 27.10, ad esempio, sono presenti tre data segments, per cui possiamo fare ricorso ad altrettante direttive ASSUME che associano DATASEGM a DS, DATASEGM2 a ES e DATASEGM3 a FS; oltre alle direttive ASSUME dobbiamo ricordarci di inizializzare i tre registri di segmento, per cui alla fine otteniamo le seguenti istruzioni: Le direttive ASSUME sono importanti solo se vogliamo accedere per nome alle variabili definite nei vari segmenti di dati delegando all'assembler il compito di inserire i vari segment override; se non usiamo le direttive ASSUME, tutti i segment override sono a nostro carico.

Nel programma di Figura 27.10 si nota la presenza delle tre stringhe stringa1, stringa2 e stringa3 definite, rispettivamente, nei tre segmenti di dati DATASEGM, DATASEGM2 e DATASEGM3; per visualizzare queste tre stringhe ci serviamo di una apposita procedura printStr1, la quale svolge il proprio lavoro con l'ausilio del servizio Display String della INT 21h.
La procedura printStr1 è di tipo NEAR e può essere quindi chiamata solo dal segmento CODESEGM in cui è stata definita; osserviamo, inoltre, che per gestire stringhe definite in diversi data segments, la procedura printStr1 ha bisogno di conoscere l'indirizzo FAR delle stringhe stesse.
Il programma di Figura 27.10 può fare a meno delle direttive ASSUME per i dati in quanto stiamo lavorando esclusivamente con gli indirizzi dei dati stessi; ricordiamo, infatti, che gli operatori dell'Assembly come SEG e OFFSET, applicati ad una determinata variabile, agiscono direttamente sul segmento di appartenenza della variabile stessa. Nel caso, ad esempio, di stringa3, l'istruzione:
push seg stringa3
inserisce nello stack il valore DATASEGM3; analogamente, l'istruzione:
push offset stringa3
inserisce nello stack il valore 0000h che è l'offset di stringa3 calcolato rispetto a DATASEGM3.

Per quanto riguarda, infine, gli aspetti legati alla dimensione massima raggiungibile da un programma con modello di memoria COMPACT, valgono tutte le considerazioni già svolte per il modello di memoria MEDIUM.

27.6 Il modello di memoria LARGE

In relazione alle esigenze di memoria di un programma, il caso più gravoso si presenta quando abbiamo bisogno di oltre 64 KiB per il codice e di oltre 64 KiB per i dati; in un caso del genere, sia il codice, sia i dati, devono essere distribuiti su due o più program segments.
Possiamo creare allora un programma dotato di due o più data segments, due o più code segments e un solo stack segment; nella terminologia dei linguaggi di alto livello, un programma strutturato in questo modo viene definito programma con modello di memoria LARGE (largo/grosso).
Solo uno dei code segments presenti dovrà contenere l'entry point del programma; tale entry point può essere rappresentato da una etichetta denominata start.

Creiamo allora un programma dotato di un blocco stack denominato STACKSEGM (con attributo di combinazione STACK), tre blocchi di dati e tre blocchi di codice; ai tre blocchi di dati assegnamo i nomi DATASEGM, DATASEGM2 e DATASEGM3, mentre ai tre blocchi di codice assegnamo i nomi CODESEGM, CODESEGM2 e CODESEGM3.
Assegnamo al programma il nome LARGE.ASM e convertiamolo in formato EXE ottenendo così l'eseguibile LARGE.EXE; sulla base delle considerazioni svolte in precedenza, subito dopo il caricamento in memoria, il programma LARGE.EXE assumerà la struttura mostrata in Figura 27.11. Il SO inizializza automaticamente la coppia CS:IP; a CS viene assegnato il paragrafo da cui parte CODESEGM, mentre a IP viene assegnato l'offset dell'etichetta start.
In presenza del segmento STACKSEGM con attributo di combinazione STACK, il SO inizializza automaticamente anche la coppia SS:SP; a SS viene assegnato il paragrafo da cui parte STACKSEGM, mentre a SP viene assegnato l'offset più alto all'interno dello stesso STACKSEGM.
I due registri di segmento DS e ES vengono entrambi inizializzati con il paragrafo da cui parte il PSP; se il programmatore vuole utilizzare DS e/o ES per gestire i blocchi DATASEGM, DATASEGM2 e DATASEGM3, deve provvedere alla inizializzazione manuale di tali registri.

In Figura 27.12 vediamo un programma di esempio che illustra in pratica i concetti appena esposti. Appare evidente che il modello LARGE rappresenta una combinazione tra il modello MEDIUM e il modello COMPACT; in una tale situazione ci troveremo quindi a gestire numerosi indirizzamenti FAR, sia per i dati, sia per il codice.

Il programma di esempio di Figura 27.12 ha il compito di illustrare in pratica questi aspetti; in tale esempio, si nota la presenza di tre stringhe stringa1, stringa2 e stringa3, definite, rispettivamente, nei tre segmenti di dati DATASEGM, DATASEGM2 e DATASEGM3. Ciascuna di queste tre stringhe viene passata per indirizzo ad una apposita procedura che provvede a visualizzarla con il servizio Display String; osserviamo che printStr1 è di tipo NEAR e può essere chiamata quindi solo dall'interno del blocco CODESEGM nel quale è stata definita.
Ciascuna delle tre procedure ha bisogno di conoscere l'indirizzo completo Seg:Offset della stringa da visualizzare; questo perché le varie stringhe vengono definite in diversi segmenti di dati.

27.7 Il modello di memoria HUGE

Alcuni compilatori di linguaggi, come il C/C++, mettono a disposizione anche un particolare modello di memoria denominato HUGE (enorme); un programma con modello di memoria HUGE ha una struttura del tutto simile a quella offerta dal modello LARGE.
La particolarità del modello HUGE consiste nel permettere la definizione di dati (generalmente, grossi vettori) che superano la dimensione di 64 KiB; in sostanza, attraverso il modello HUGE possiamo superare il limite dei 64 KiB imposto ai program segments dalla modalità reale 8086!

Nei precedenti capitoli abbiamo visto che il limite dei 64 KiB ci impedisce di scrivere definizioni del tipo:
bigvect dw 40000 dup (0)
Osserviamo, infatti, che il vettore bigvect è formato da 40000 elementi, ciascuno dei quali ha una dimensione pari a 1 word (2 byte); in totale, un simile vettore occupa quindi in memoria uno spazio pari a:
40000 * 2 = 80000 byte
Ciò significa che un singolo program segment da 64 KiB (65536 byte) non è in grado di contenere bigvect; se proviamo, infatti, ad assemblare un programma contenente la precedente definizione, otteniamo un messaggio di errore del tipo:
Location counter overflow
Come sappiamo, questo messaggio indica che la componente Offset del location counter ($) ha superato il valore massimo FFFFh durante la fase di assemblaggio di un program segment!

Per aggirare questo problema, si può ricorrere ad un semplice trucco che consiste nel disporre un vettore come bigvect in due o più data segments consecutivi e contigui; nel nostro caso, possiamo notare che sono più che sufficienti due soli data segments.
Disponiamo, ad esempio, i primi 20000 elementi di bigvect in un blocco DATASEGM allineato al paragrafo; osserviamo che DATASEGM avrà quindi una dimensione pari a:
20000 * 2 = 40000 byte
Inoltre, tenendo presente che 40000 in esadecimale si scrive 9C40h, possiamo affermare che questa "prima metà" di bigvect parte dall'indirizzo logico DATASEGM:0000h e termina all'indirizzo logico DATASEGM:9C3Fh.
Disponiamo ora i successivi 20000 elementi di bigvect in un blocco DATASEGM2 definito subito dopo DATASEGM; siccome bigvect è un vettore di WORD, assegnamo a DATASEGM2 un attributo di allineamento alla WORD, ottenendo così la seguente situazione: L'indirizzo immediatamente successivo a DATASEGM:9C3Fh e multiplo intero di 2 è DATASEGM:9C40h; ciò significa che il linker farà partire DATASEGM2 subito dopo la fine di DATASEGM. I due blocchi DATASEGM e DATASEGM2 sono quindi consecutivi e contigui e possono essere gestiti come un unico blocco di dimensioni maggiori di 64 KiB!

Il problema che si presenta è dato dal fatto che in modalità reale, una qualsiasi componente Offset deve essere compresa tra 0000h e FFFFh (cioè, tra 0 e 65535); come facciamo allora a gestire un program segment di dimensioni maggiori di 64 KiB (65536 byte)?

Per superare questo problema, i compilatori gestiscono i segmenti come DATASEGM e DATASEGM2 attraverso i cosiddetti indirizzi logici normalizzati che abbiamo già conosciuto nei precedenti capitoli; nel seguito del capitolo, per indicare tali indirizzi utilizzeremo la sigla ILN.
Come già sappiamo, i generici indirizzi logici non permettono di avere una corrispondenza biunivoca con gli indirizzi fisici; infatti, a causa della parziale sovrapposizione dei segmenti di memoria (vedere la Figura 9.4 del Capitolo 9), un determinato indirizzo fisico può essere rappresentato con uno o più indirizzi logici. Consideriamo, ad esempio, l'indirizzo fisico 00037h; tale indirizzo fisico può essere ottenuto dai seguenti quattro indirizzi logici: Infatti, convertendo questi indirizzi logici in indirizzi fisici abbiamo: L'ambiguità appena descritta crea gravi problemi nella gestione degli indirizzamenti; ad esempio, confrontando due dei precedenti puntatori FAR attraverso una pseudo-istruzione del tipo:
if (0001h:0027h == 0003h:0007h)
si otterrebbe FALSE in quanto, apparentemente, 0001h:0027h è diverso da 0003h:0007h. In realtà, sappiamo benissimo che i due puntatori FAR sono uguali; infatti, entrambi rappresentano l'indirizzo fisico 00037h!

Un altro grave problema dei generici indirizzi logici è rappresentato dal cosiddetto wrap around (attorcigliamento); come già sappiamo, partendo, ad esempio, dall'indirizzo logico 3FABh:FFFFh e incrementando di 1 la componente Offset, otteniamo 3FABh:0000h!
Sommando 1 a FFFFh dovremmo ottenere 10000h; una componente Offset della modalità reale ha, però, una ampiezza di 16 bit, per cui il bit più significativo di 10000h (10000000000000000b) viene perso e si ottiene 0000h!

Tutti questi problemi scompaiono grazie agli ILN; infatti, gli ILN si trovano in corrispondenza biunivoca con gli indirizzi fisici.
Per convertire un indirizzo fisico in un ILN basta partire dalla sua rappresentazione esadecimale a 5 cifre; le 4 cifre più a sinistra rappresentano la componente Seg, mentre la cifra più a destra rappresenta la componente Offset. Ad esempio, l'indirizzo fisico 00037h può essere convertito nel seguente ILN:
00037h -> 0003h:7h = 0003h:0007h
Infatti, osserviamo che:
(0003h * 10h) + 0007h = 00030h + 0007h = 00037h
Quindi, l'indirizzo fisico 00037h corrisponde a ben 4 indirizzi logici generici e ad un unico ILN rappresentato da 0003h:0007h; viceversa, 0003h:0007h corrisponde ad un unico indirizzo fisico rappresentato da 00037h.

Il perché della corrispondenza biunivoca appena descritta è abbastanza evidente. I generici indirizzi logici sono legati alla suddivisione del primo MiB di RAM in tanti segmenti di memoria (cioè, in tanti blocchi da 65536 byte ciascuno); a causa della parziale sovrapposizione dei segmenti di memoria, uno stesso indirizzo fisico può corrispondere a uno o più indirizzi logici generici.
Gli ILN, invece, sono legati alla suddivisione del primo MiB di RAM in tanti paragrafi (cioè, in tanti blocchi da 16 byte ciascuno); infatti, osserviamo che un ILN è formato da una componente Seg a 16 bit che, come al solito, rappresenta un indice di paragrafo, e da una componente Offset a 4 bit che rappresenta uno spiazzamento (compreso tra 0h e Fh) all'interno del paragrafo stesso. A differenza di quanto accade con i segmenti di memoria, i paragrafi di memoria sono disposti in modo consecutivo e contiguo, senza alcuna sovrapposizione; è impossibile quindi che due o più ILN possano rappresentare lo stesso indirizzo fisico!

Una prima conseguenza delle considerazioni appena esposte è data dal fatto che una comparazione tra puntatori FAR in formato ILN fornisce sempre il risultato corretto. Una conseguenza ancora più importante è data dal fatto che con gli ILN scompare il wrap around; per dimostrarlo, facciamo riferimento all'indirizzo logico generico 3FABh:FFFFh utilizzato in un precedente esempio. Convertiamo 3FABh:FFFFh nel corrispondente indirizzo fisico, ottenendo così:
(3FABh * 10h) + FFFFh = 3FAB0h + FFFFh = 4FAAFh
Convertendo ora 4FAAFh in un ILN otteniamo:
4FAAFh -> 4FAAh:Fh = 4FAAh:000Fh
Sommando 1 alla componente Offset di 4FAAh:000Fh otteniamo il generico indirizzo logico 4FAAh:0010h, che corrisponde all'indirizzo fisico:
(4FAAh * 10h) + 0010h = 4FAA0h + 0010h = 4FAB0h
Convertendo ora 4FAB0h in un ILN otteniamo:
4FAB0h -> 4FABh:0h = 4FABh:0000h
Come possiamo notare, siamo passati dall'offset 000Fh del paragrafo 4FAAh, all'offset 0000h del paragrafo successivo 4FABh; in sostanza, siamo passati dall'ultimo byte del paragrafo 4FAAh, al primo byte del paragrafo successivo 4FABh!

In definitiva, grazie agli ILN possiamo scandire tutto il primo MiB di RAM della modalità reale senza incappare nel problema del wrap around!

Sfruttando i concetti appena esposti, i compilatori C/C++ che supportano il modello HUGE permettono al programmatore di scrivere, in modalità reale, definizioni del tipo:
short int huge bigvect[40000];
Questa definizione crea un vettore da 40000 elementi di tipo intero con segno a 16 bit; tale vettore occupa quindi 80000 byte di memoria!

Per gestire un dato huge come bigvect, i compilatori C/C++ utilizzano proprio gli ILN; in questo modo possono scandire tutto il vettore senza il rischio del wrap around.
Supponendo che bigvect sia stato disposto in un data segment che parte dal paragrafo ABF2h con allineamento di tipo PARA, possiamo affermare che: A questo punto siamo arrivati all'ultima WORD del paragrafo 4BF2h, per cui il prossimo elemento verrà a trovarsi nel paragrafo successivo; infatti: e così via.

Oltre ai vantaggi appena illustrati, gli ILN presentano anche un aspetto negativo di cui il programmatore deve tenere conto; in pratica, l'utilizzo degli ILN provoca un sensibile calo delle prestazioni di un programma!
Per capire il perché, si deve tenere presente che la CPU lavora con indirizzi logici generici e non ha la più pallida idea di cosa sia un ILN. Tutto ciò che concerne l'uso degli ILN viene gestito via software dal compilatore C/C++; a tale proposito, vengono utilizzate una serie di complesse procedure che, per i motivi che conosciamo, comportano un sensibile rallentamento del programma.

27.8 La direttiva GROUP

In molti casi è possibile semplificare la struttura di un programma raggruppando tra loro due o più segmenti distinti; questa tecnica viene utilizzata, ad esempio, dai compilatori C/C++ per raggruppare dati temporanei (stack) e diverse categorie di dati statici. Si tratta quindi di una caratteristica concepita espressamente per i compilatori dei linguaggi di alto livello.
In generale, un programmatore Assembly non ha bisogno di ricorrere ad un simile espediente; in ogni caso si tratta di aspetti che è importante conoscere in quanto possono tornare utili quando si ha bisogno di interfacciare l'Assembly con i linguaggi di alto livello.

Al fine di permettere il raggruppamento di due o più segmenti di programma, l'Assembly mette a disposizione la direttiva GROUP; attraverso questa direttiva è possibile utilizzare un nome unico per gestire un insieme di program segments appartenenti ad uno stesso gruppo. La Figura 27.13 mostra un esempio pratico che fa uso della libreria EXELIB. Nel programma GROUP.ASM di Figura 27.13 notiamo la presenza di tre data segments denominati DSEG1, DSEG2 e DSEG3, tutti allineati al paragrafo; analizzando la loro struttura e assumendo che a DSEG1 venga assegnato il paragrafo BF2Ch, possiamo affermare che: Subito dopo la definizione dei tre data segments troviamo la direttiva:
DGROUP GROUP DSEG1 DSEG2 DSEG3
Questa direttiva dice all'assembler che è nostra intenzione utilizzare il nome simbolico DGROUP per indicare un unico program segment formato da DSEG1, DSEG2 e DSEG3; tenendo conto del fatto che DSEG1, DSEG2 e DSEG3 hanno tutti un allineamento di tipo PARA (e quindi partono tutti da indirizzi fisici multipli di 0010h), possiamo affermare che: Quindi, l'inizio di DGROUP coincide con l'inizio di DSEG1, mentre la fine di DGROUP coincide con la fine di DSEG3; inoltre, a causa dell'allineamento di tipo PARA di DSEG1, DSEG2 e DSEG3, la dimensione complessiva di DGROUP sarà:
0010h + 0010h + 0008h = 0028h byte
Appare evidente che, in modalità reale, tale dimensione non può essere superiore a 64 KiB.

A questo punto, possiamo utilizzare DGROUP come se fosse l'unico data segment del programma di Figura 27.13; possiamo scrivere, quindi: Per dimostrare in pratica i concetti appena esposti, il programma di Figura 27.13 visualizza sullo schermo le componenti Offset di varWord1, varWord2 e varWord3; queste informazioni vengono ottenute con tre metodi differenti che prevedono l'uso dell'istruzione LEA, dell'operatore OFFSET e del segment override esplicito DGROUP. La direttiva GROUP raggruppa i vari program segments tenendo conto del modo con cui il programmatore ha stabilito la disposizione in memoria dei segmenti stessi; per chiarire questo aspetto analizziamo il programma di Figura 27.14. In questo programma sono presenti cinque data segments denominati DSEG1, DSEG2, DSEG3, DSEG4 e DSEG5, tutti allineati al paragrafo; analizzando la loro struttura e assumendo che a DSEG1 venga assegnato il paragrafo BF2Ch, possiamo dire che: Subito dopo la definizione dei cinque data segments troviamo la direttiva:
DGROUP GROUP DSEG1 DSEG3 DSEG5
Questa direttiva dice all'assembler che è nostra intenzione utilizzare il nome simbolico DGROUP per indicare un unico program segment formato da DSEG1, DSEG3 e DSEG5, mentre DSEG2 e DSEG4 restano indipendenti; tenendo conto del fatto che DSEG1, DSEG3 e DSEG5 hanno tutti un allineamento di tipo PARA (e quindi partono tutti da indirizzi fisici multipli di 0010h), possiamo dire che: Eseguendo il programma di Figura 27.14 si nota che l'istruzione LEA calcola correttamente l'offset di varWord1, varWord3 e varWord5 rispetto a DGROUP; analogamente, l'istruzione LEA calcola correttamente l'offset di varWord2 rispetto a DSEG2 e l'offset di varWord4 rispetto a DSEG4;

Un'ultima considerazione riguarda il fatto che, chiaramente, la direttiva GROUP dovrebbe essere utilizzata per raggruppare segmenti di programma dello stesso tipo; non avrebbe molto senso raggruppare, ad esempio, segmenti di dati con segmenti di codice.
I compilatori C/C++ usano spesso la direttiva GROUP per raggruppare lo stack segment con il data segment referenziato da DS; ciò è possibile in quanto, nella gran parte dei casi, la somma delle dimensioni di questi due blocchi non raggiunge i 65536 byte.
Se il programmatore C vuole abilitare il raggruppamento del blocco stack e del blocco dati referenziato da DS, deve attivare una apposita opzione dal menu di configurazione del compilatore; nel caso del Borland C/C++ 3.1, questa opzione viene attivata selezionando Assume SS Equals DS dal menu Options.
Raggruppando lo stack e il blocco dati referenziato da DS, i compilatori C/C++ riescono spesso a gestire un programma in modo più efficiente; il programma di Figura 27.15 illustra un esempio pratico in Assembly. Al momento di caricare il programma in memoria, il SO pone CS=CODESEGM e fa puntare IP all'entry point rappresentato dall'etichetta start; inoltre, il SO pone SS=STACKSEGM e fa puntare SP alla fine di STACKSEGM (cioè, all'offset 0400h calcolato rispetto a STACKSEGM).
Appena il programmatore riceve il controllo pone DS=SS=DGROUP; in questo modo i dati statici e i dati temporanei del programma possono essere gestiti, indifferentemente, con offset calcolati rispetto a DS o a SS.
Anche SP deve essere modificato in modo che punti all'offset (di indice pari) più alto di DGROUP; ovviamente, tale offset deve essere calcolato rispetto a DGROUP e non rispetto a STACKSEGM (come fa il SO).
Naturalmente, è fondamentale che DATASEGM e STACKSEGM vengano disposti come si vede in Figura 27.15; in questo modo i dati statici vengono disposti nella parte iniziale di DGROUP, mentre i dati temporanei vengono disposti nella parte finale di DGROUP.