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:
- l'elemento 0 di bigvect si trova in ABF2h:0h
- l'elemento 1 di bigvect si trova in ABF2h:2h
- l'elemento 2 di bigvect si trova in ABF2h:4h
- l'elemento 3 di bigvect si trova in ABF2h:6h
- l'elemento 4 di bigvect si trova in ABF2h:8h
- l'elemento 5 di bigvect si trova in ABF2h:Ah
- l'elemento 6 di bigvect si trova in ABF2h:Ch
- l'elemento 7 di bigvect si trova in ABF2h:Eh
A questo punto siamo arrivati all'ultima WORD del paragrafo 4BF2h,
per cui il prossimo elemento verrà a trovarsi nel paragrafo successivo; infatti:
- l'elemento 8 di bigvect si trova in ABF3h:0h
- l'elemento 9 di bigvect si trova in ABF3h:2h
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:
- DSEG1 parte dall'indirizzo logico BF2Ch:0000h
- varWord1 si trova all'offset 0002h di DSEG1
- DSEG2 parte dall'indirizzo logico BF2Dh:0000h
- varWord2 si trova all'offset 0004h di DSEG2
- DSEG3 parte dall'indirizzo logico BF2Eh:0000h
- varWord3 si trova all'offset 0006h di DSEG3
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:
- DGROUP parte dall'indirizzo logico BF2Ch:0000h (lo stesso
di DSEG1)
- varWord1 si trova all'offset 0002h di DGROUP
- varWord2 si trova all'offset 0010h + 0004h = 0014h di
DGROUP
- varWord3 si trova all'offset 0020h + 0006h = 0026h di
DGROUP
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:
- DSEG1 parte dall'indirizzo logico BF2Ch:0000h
- varWord1 si trova all'offset 0000h di DSEG1
- DSEG2 parte dall'indirizzo logico BF2Dh:0000h
- varWord2 si trova all'offset 0000h di DSEG2
- DSEG3 parte dall'indirizzo logico BF2Eh:0000h
- varWord3 si trova all'offset 0000h di DSEG3
- DSEG4 parte dall'indirizzo logico BF2Fh:0000h
- varWord4 si trova all'offset 0000h di DSEG4
- DSEG5 parte dall'indirizzo logico BF30h:0000h
- varWord5 si trova all'offset 0000h di DSEG5
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:
- DGROUP parte dall'indirizzo logico BF2Ch:0000h (lo stesso
di DSEG1)
- varWord1 si trova all'offset 0000h di DGROUP
- varWord3 si trova all'offset 0020h + 0000h = 0020h di
DGROUP
- varWord5 si trova all'offset 0040h + 0000h = 0040h di
DGROUP
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.