Assembly Base con MASM

Capitolo 29: Interfaccia con i linguaggi di alto livello


Nei precedenti capitoli abbiamo visto che i linguaggi di programmazione di alto livello si suddividono in interpretati e compilati; parallelamente, gli strumenti software che traducono in codice macchina i programmi scritti con un linguaggio di alto livello, si suddividono in interpreti e compilatori.
Se abbiamo la necessità di interfacciare l'Assembly con i linguaggi di alto livello, dobbiamo conoscere tutti i dettagli relativi alle convenzioni seguite dai compilatori e dagli interpreti per la generazione del codice macchina; riassumiamo allora gli aspetti principali che caratterizzano questi particolari strumenti software.

Un interprete svolge il proprio lavoro operando su una sola istruzione per volta; in sostanza, l'interprete legge una istruzione dal codice sorgente, la traduce in codice macchina e la fa eseguire alla CPU prima di passare all'istruzione successiva.
Per ogni singola istruzione da eseguire, l'interprete deve passare alla CPU tutte le informazioni necessarie, compresi eventuali riferimenti a dati o procedure, contenuti nell'istruzione stessa; ciò implica che l'interprete debba farsi carico di tutta la complessità (indirizzamenti, trasferimenti del controllo, etc) che caratterizza la struttura interna di un programma da eseguire.
Questo modo di procedere determina, inesorabilmente, una notevole lentezza nella fase di esecuzione dei programmi interpretati; è nota, ad esempio, l'esasperante lentezza di esecuzione dei programmi scritti in BASIC interpretato.
Appare evidente che un programma interpretato, per poter essere eseguito, necessita della presenza in memoria del relativo interprete; di conseguenza, anche la possibilità di distribuire un programma interpretato a diversi utenti, richiede che ciascuno degli utenti stessi disponga dell'apposito interprete.
Un programma interpretato è costituito, generalmente, da un file di testo scritto in formato ASCII; a meno di utilizzare un sistema di criptatura, questa situazione determina il rischio che un malintenzionato possa "carpire" il codice sorgente del programma stesso!

Un compilatore svolge il proprio lavoro di traduzione operando sull'intero codice sorgente di un programma; ciò significa che il compilatore legge un intero programma scritto con un linguaggio di alto livello e lo traduce completamente in codice macchina in modo da ottenere un cosiddetto eseguibile.
L'eseguibile così ottenuto è del tutto "autosufficiente" e può essere quindi inviato alla fase di esecuzione indipendentemente dalla presenza o meno del compilatore; ciò implica che il compilatore debba inserire nell'eseguibile tutta la complessità (indirizzamenti, trasferimenti del controllo, etc) che caratterizza la struttura interna dell'eseguibile stesso. In sostanza, è come se un programma compilato incorpori al suo interno un interprete; ogni istruzione di un programma compilato, fornisce quindi alla CPU tutte le informazioni necessarie per l'esecuzione.
Per poter essere eseguito, un programma compilato viene interamente caricato in memoria; ciò permette velocità di esecuzione nettamente superiori rispetto ai programmi interpretati.
Un programma compilato è costituito da un file in codice macchina e risulta quindi illeggibile con un editor ASCII; ciò implica che solo un hacker esperto può riuscire a risalire al codice sorgente del programma stesso!

Le considerazioni appena esposte evidenziano notevoli analogie tra i compilatori e gli assemblatori; anche gli assembler, infatti, partendo dal codice sorgente producono un programma in codice macchina destinato a diventare un eseguibile del tutto "autosufficiente". Questo aspetto ci fa intuire che solo i programmi compilati possono rendere possibile l'interfacciamento con l'Assembly; nel caso dei programmi interpretati, infatti, questa possibilità è vanificata dall'assenza dell'indispensabile object code!

Affinché sia possibile interfacciare un programma Assembly con un programma compilato, devono verificarsi due importanti condizioni: La prima condizione viene rispettata attraverso la definizione di un formato standard per i file oggetto; nel mondo DOS/Windows i due formati standard più usati sono il formato OMF (Object Module Format) e il formato COFF (Common Object File Format).
Il formato OMF viene supportato da numerosi assembler come NASM, MASM, TASM e da molti compilatori della Microsoft e della Borland; il formato COFF, invece, viene largamente utilizzato in ambiente Windows dagli strumenti di sviluppo della Microsoft come MASM, Visual C++, etc, ed è supportato anche da NASM. Per maggiori dettagli su questi due formati per i moduli oggetto si consiglia di scaricare la documentazione ufficiale disponibile nella sezione Documentazione tecnica di supporto al corso assembly dell’ Area Downloads di questo sito.

Per rispettare anche la seconda condizione, dobbiamo essere in grado di conoscere tutti i dettagli relativi alle convenzioni seguite dai compilatori per definire la struttura a basso livello di un programma eseguibile; attenendoci a queste convenzioni possiamo creare moduli Assembly interfacciabili con i moduli oggetto prodotti dai compilatori. Come è stato spiegato nei capitoli dal 24 al 27, queste convenzioni riguardano, in particolare: Uno degli scopi più importanti di queste convenzioni è quello di ottenere la massima semplificazione possibile nella complessa fase di progettazione di un compilatore.
In sostanza, al momento di convertire in codice macchina un programma scritto in C/C++, Pascal, FORTRAN, BASIC (compilato), etc, i compilatori si attengono ad una serie di convenzioni che possono essere considerate come veri e propri standard di linguaggio; in genere, questi standard vengono decisi dai produttori di CPU (come Intel e AMD) e da chi scrive i SO (come Microsoft, Apple, Sun, etc).

A partire dalla versione 5.x, il MASM offre il pieno supporto di queste convenzioni attraverso una serie di caratteristiche avanzate che, dal punto di vista del programmatore, si presentano come una vera e propria estensione del linguaggio Assembly; grazie all'uso di una sintassi evoluta, molto simile a quella dei linguaggi di alto livello, è possibile ottenere notevoli semplificazioni nel lavoro di interfacciamento tra l'Assembly e gli altri linguaggi.
Il compito fondamentale delle caratteristiche avanzate del MASM è quello di permettere all'assembler la generazione automatica di tutte quelle porzioni di codice macchina che, in base a quanto è stato appena detto, si possono considerare standard; in questo modo il programmatore ha la possibilità di delegare all'assembler la gestione di tutti quegli aspetti che coinvolgono i modelli di memoria, i segmenti di programma, le convenzioni per le procedure, etc.
Naturalmente, è anche possibile sfruttare le caratteristiche avanzate per rendere più semplice lo sviluppo di programmi interamente scritti in Assembly; in questo caso, però, bisogna osservare che queste "innovazioni" non suscitano particolare entusiasmo tra i puristi del linguaggio Assembly.

Si tenga presente, comunque, che in questo specifico settore, tra i vari assembler esistono purtroppo numerose incompatibilità spesso legate a banalissimi dettagli sintattici; la situazione è aggravata dal fatto che la sintassi di queste estensioni dell'Assembly varia anche da una versione all'altra dello stesso assembler. Per maggiori dettagli si consiglia di fare riferimento direttamente ai manuali dell'assembler che si intende utilizzare.

29.1 La direttiva .MODEL

Per poter accedere alle caratteristiche avanzate di MASM è necessario servirsi della direttiva .MODEL; questa direttiva, che deve essere inserita all'inizio di ogni modulo Assembly, ci permette di passare all'assembler una serie di importanti informazioni relative, in particolare, al modello di memoria da utilizzare e alle convenzioni di linguaggio per le procedure.

29.1.1 Il parametro memory_model

Tra tutte queste informazioni, l'unica obbligatoria è quella relativa al modello di memoria; i principali modelli di memoria sono stati esaminati nel Capitolo 27. Per specificare un determinato modello di memoria, la sintassi da utilizzare è:
.MODEL memory_model
Il parametro memory_model è uno tra quelli indicati dalla Figura 29.1; ogni modello di memoria indica il tipo di indirizzamento predefinito (NEAR o FAR) che l'assembler deve utilizzare per accedere alle procedure e ai dati del programma. Se vogliamo scrivere moduli Assembly destinati ad interfacciarsi con altri moduli C/C++, Pascal, BASIC, etc, dobbiamo ovviamente tenere conto delle convenzioni illustrate in Figura 29.1; analizziamo allora in dettaglio i vari casi.

29.1.2 Il parametro TINY

Consideriamo la direttiva:
.MODEL TINY
Come già sappiamo, questo modello di memoria è compatibile con la generazione di un eseguibile in formato COM (COre iMage); nel Capitolo 27 abbiamo visto che questo modello di memoria prevede la presenza di un unico segmento di programma contenente codice, dati e stack. In assenza di diverse indicazioni da parte del programmatore, tutti i dati e le procedure del programma vengono considerati di tipo NEAR.
I compilatori che supportano i modelli di memoria, creano un gruppo DGROUP che rappresenta l'unico segmento di programma presente; inoltre, il compilatore stesso pone CS=DS=SS=DGROUP.

29.1.3 Il parametro SMALL

Consideriamo la direttiva:
.MODEL SMALL
Questo modello di memoria prevede la presenza di un unico segmento di codice e di un unico segmento di dati; in assenza di diverse indicazioni da parte del programmatore, tutti i dati e le procedure del programma vengono considerati di tipo NEAR.
I compilatori che supportano i modelli di memoria, creano un gruppo DGROUP che comprende l'unico blocco dati seguito dal blocco stack; inoltre, il compilatore stesso pone DS=SS=DGROUP e fa puntare CS all'unico blocco codice (che contiene l'entry point). Se DGROUP supera i 64 KiB, allora il blocco stack viene separato da DGROUP; in tal caso il compilatore pone DS=DGROUP e fa puntare SS al blocco stack e CS all'unico blocco codice (che contiene l'entry point).

29.1.4 Il parametro MEDIUM

Consideriamo la direttiva:
.MODEL MEDIUM
Questo modello di memoria prevede la presenza di due o più segmenti di codice e di un unico segmento di dati; in assenza di diverse indicazioni da parte del programmatore, tutte le procedure del programma vengono considerate di tipo FAR, mentre tutti i dati vengono considerati di tipo NEAR.
I compilatori che supportano i modelli di memoria, creano un gruppo DGROUP che comprende l'unico blocco dati seguito dal blocco stack; inoltre, il compilatore stesso pone DS=SS=DGROUP e fa puntare CS al blocco codice che contiene l'entry point. Se DGROUP supera i 64 KiB, allora il blocco stack viene separato da DGROUP; in tal caso il compilatore pone DS=DGROUP e fa puntare SS al blocco stack e CS al blocco codice che contiene l'entry point.

29.1.5 Il parametro COMPACT

Consideriamo la direttiva:
.MODEL COMPACT
Questo modello di memoria prevede la presenza di un unico segmento di codice e di due o più segmenti di dati; in assenza di diverse indicazioni da parte del programmatore, tutte le procedure del programma vengono considerate di tipo NEAR, mentre tutti i dati vengono considerati di tipo FAR (ad eccezione dei "dati principali" del programma).
I compilatori che supportano i modelli di memoria, creano un gruppo DGROUP che comprende uno dei blocchi di dati (blocco dati principale) seguito dal blocco stack; inoltre, il compilatore stesso pone DS=SS=DGROUP e fa puntare CS all'unico blocco codice (che contiene l'entry point). Se DGROUP supera i 64 KiB, allora il blocco stack viene separato da DGROUP; in tal caso il compilatore pone DS=DGROUP e fa puntare SS al blocco stack e CS all'unico blocco codice (che contiene l'entry point). Appare evidente che tutti i dati contenuti in DGROUP possono essere acceduti con indirizzamenti di tipo NEAR; invece, l'accesso a tutti i dati contenuti negli altri segmenti esterni a DGROUP richiede indirizzamenti di tipo FAR.

29.1.6 Il parametro LARGE

Consideriamo la direttiva:
.MODEL LARGE
Questo modello di memoria prevede la presenza di due o più segmenti di codice e di due o più segmenti di dati; in assenza di diverse indicazioni da parte del programmatore, tutti i dati e le procedure del programma vengono considerati di tipo FAR (ad eccezione dei "dati principali" del programma).
I compilatori che supportano i modelli di memoria, creano un gruppo DGROUP che comprende uno dei blocchi di dati (blocco dati principale) seguito dal blocco stack; inoltre, il compilatore stesso pone DS=SS=DGROUP e fa puntare CS al blocco codice che contiene l'entry point. Se DGROUP supera i 64 KiB, allora il blocco stack viene separato da DGROUP; in tal caso il compilatore pone DS=DGROUP e fa puntare SS al blocco stack e CS al blocco codice che contiene l'entry point. Appare evidente che tutti i dati contenuti in DGROUP possono essere acceduti con indirizzamenti di tipo NEAR; invece, l'accesso a tutti i dati contenuti negli altri segmenti esterni a DGROUP richiede indirizzamenti di tipo FAR.

29.1.7 Il parametro HUGE

Si ricorda che il modello HUGE è del tutto simile al modello LARGE; l'unica differenza è data dal fatto che, nel modello HUGE, i compilatori (che supportano tale modello) utilizzano gli indirizzi logici normalizzati per permettere la gestione di grossi dati (come i vettori) che occupano più di 64 KiB in memoria.

29.1.8 Il parametro FLAT

Il modello FLAT viene supportato solo dalle versioni più recenti di MASM e deve essere utilizzato per lo sviluppo di applicazioni destinate ai SO a 32 bit come Windows 9x o superiore, OS/2, Linux, etc; questi sistemi operativi sono in grado di sfruttare l'architettura a 32 bit delle CPU 80386 e superiori.
Sfruttare l'architettura a 32 bit di una CPU significa poter indirizzare in modo lineare (flat) la memoria del computer attraverso l'uso di offset a 32 bit; un offset a 32 bit permette di rappresentare tutti i valori compresi tra 00000000h e FFFFFFFFh, con la possibilità quindi di poter accedere in modo lineare a ben 4 GiB di RAM!
Nel caso generale, i SO a 32 bit supportano la possibilità di far girare "contemporaneamente" più programmi (multitasking); per ciascun programma in esecuzione il SO crea uno spazio di indirizzamento "virtuale". Ad ogni programma in memoria viene fatto credere di essere l'unico in esecuzione, con tutto l'hardware del computer a sua completa disposizione; all'interno del suo spazio virtuale (flat segment) un programma assume una struttura del tutto simile a quella del modello di memoria SMALL.
In sostanza, all'interno del flat segment viene creato un unico blocco codice, un unico blocco dati e un unico blocco stack; il blocco dati e il blocco stack vengono, in genere, raggruppati tra loro attraverso la direttiva GROUP. In una situazione di questo genere, per l'accesso alle procedure e ai dati del programma vengono utilizzati indirizzamenti di tipo NEAR; è chiaro quindi che nel modello di memoria FLAT il termine NEAR indica un indirizzo logico formato dalla sola componente Offset la cui ampiezza è di 32 bit.
Il modello di memoria FLAT dei SO a 32 bit rappresenta un vero e proprio paradiso per tutti quei programmatori che provengono dall'inferno della segmentazione a 64 KiB dei SO a 16 bit; per una descrizione più dettagliata del modello di memoria FLAT si possono consultare le apposite sezioni di questo sito.

29.1.9 Memory model override

Se il programmatore vuole aggirare le convenzioni di Figura 29.1, deve passare all'assembler le opportune informazioni; nel caso, ad esempio, di un programma con modello di memoria SMALL, nessuno ci impedisce di definire procedure di tipo FAR. Tali procedure devono essere chiamate con una FAR call e devono contenere, di conseguenza, un FAR return; per obbligare l'assembler a rispettare queste condizioni dobbiamo usare il Model Modifier (modificatore del modello di memoria) FAR e, se necessario, anche l'operatore FAR PTR.
Supponiamo, ad esempio, di voler definire una procedura FAR di nome FarProc in un programma con modello di memoria SMALL; in questo caso la definizione della procedura deve iniziare con:
FarProc proc far
In questo modo l'assembler capisce che l'istruzione RET posta alla fine della procedura deve essere convertita nel codice macchina di un FAR return.
Naturalmente, al momento di chiamare la procedura FarProc dobbiamo anche ricordarci di scrivere:
call far ptr FarProc
In questo modo l'assembler capisce che l'istruzione CALL deve essere convertita nel codice macchina di una FAR call; al momento di eseguire la chiamata di FarProc, la CPU inserirà quindi nello stack l'indirizzo di ritorno completo Seg:Offset, anche se il contenuto di CS rimane invariato.

29.1.10 Il parametro language

Tra i parametri facoltativi che possiamo passare alla direttiva .MODEL c'è, in particolare, il parametro di linguaggio; attraverso questo parametro indichiamo all'assembler quale convenzione di linguaggio vogliamo utilizzare per la gestione delle procedure.
Come già sappiamo, queste convenzioni riguardano: il prolog code (codice di ingresso), l'epilog code (codice di uscita), le variabili locali e la pulizia dello stack; specificando il parametro di linguaggio possiamo delegare tutti questi aspetti all'assembler.
Per quanto riguarda la gestione del valore di ritorno, il programmatore deve attenersi alle convenzioni esposte nel Capitolo 25.

Per poter specificare un parametro di linguaggio la sintassi da utilizzare è:
.MODEL memory_model, language
Il parametro language è uno tra quelli illustrati in Figura 29.2; per ogni parametro di linguaggio la Figura 29.2 indica l'ordine di inserimento, nello stack, degli argomenti di una procedura e su chi ricade il compito di ripulire lo stack dagli argomenti stessi. Si tenga presente che i linguaggi C++ e Prolog utilizzano le stesse convenzioni del C.

In presenza del parametro di linguaggio l'assembler è in grado di generare automaticamente il prolog code e l'epilog code delle procedure; il tutto avviene in conformità alle convenzioni del linguaggio selezionato. Il programmatore gestisce tutta questa situazione attraverso l'uso di una sintassi evoluta, molto simile a quella dei linguaggi di alto livello; l'assembler elabora questa sintassi evoluta e la converte nell'opportuna sequenza di codici macchina.
Supponiamo, ad esempio, di voler creare un programma con modello di memoria SMALL conforme alle convenzioni Pascal per la gestione delle procedure; in questo caso dobbiamo scrivere:
.MODEL SMALL, PASCAL
Il parametro di linguaggio PASCAL dice all'assembler che in assenza di diverse indicazioni da parte del programmatore, ogni chiamata "evoluta" ad una procedura deve essere convertita in una sequenza di codici macchina che rispettano tutte le convenzioni del Pascal; in particolare, gli eventuali argomenti vengono automaticamente inseriti nello stack a partire dal primo (sinistra destra), mentre alla fine della procedura viene aggiunto all'istruzione RET un opportuno valore immediato da sommare a SP per la pulizia dello stack.

Il parametro STDCALL viene largamente utilizzato in ambiente Windows a 32 bit e rappresenta un misto tra le convenzioni C e Pascal.
Come già sappiamo, in C il passaggio degli argomenti ad una procedura avviene a partire dall'ultimo (destra sinistra), mentre la pulizia dello stack spetta al caller; questa convenzione permette di implementare procedure che accettano un numero variabile di argomenti.
In Pascal, invece, il passaggio degli argomenti ad una procedura avviene a partire dal primo (sinistra destra), mentre la pulizia dello stack spetta alla procedura stessa; la pulizia dello stack in stile Pascal è più veloce rispetto al caso del C (infatti, l'istruzione RET Imm16 permette di evitare la successiva istruzione ADD usata dal C per sommare un Imm16 a SP).
La convenzione STDCALL rappresenta un compromesso tra C e Pascal in quanto sfrutta la convenzione C per il passaggio degli argomenti e la convenzione Pascal per la pulizia dello stack.

29.1.11 Language override

Se il programmatore vuole aggirare le convenzioni illustrate in Figura 29.2, deve passare le opportune informazioni all'assembler; in sostanza, se stiamo utilizzando, ad esempio, il parametro di linguaggio PASCAL, possiamo ugualmente definire procedure che seguono le convenzioni C. Per ottenere questo risultato dobbiamo inserire il Language Modifier (modificatore di linguaggio) C direttamente nella definizione di quelle procedure che vogliono sfruttare le convenzioni C; la sintassi da utilizzare viene illustrata più avanti.

29.1.12 I parametri NEARSTACK e FARSTACK

Come è stato appena spiegato, i compilatori che supportano i modelli di memoria tendono a raggruppare, se ciò è possibile, il blocco dati principale con il blocco stack, ottenendo così il gruppo DGROUP; questo è anche il comportamento predefinito dell'Assembly quando si utilizzano le caratteristiche avanzate (cioè, quando si utilizza la direttiva .MODEL). Per rendere esplicito tale comportamento, il programmatore può servirsi del parametro NEARSTACK da passare alla direttiva .MODEL; possiamo scrivere, ad esempio:
.MODEL SMALL, PASCAL, NEARSTACK
Se, invece, vogliamo costringere l'Assembly a tenere il blocco stack distinto da DGROUP, possiamo servirci del parametro FARSTACK da passare alla direttiva .MODEL; possiamo scrivere, ad esempio:
.MODEL LARGE, C, FARSTACK
Più avanti verranno illustrati degli esempi pratici.

29.2 Sintassi avanzata per le procedure

Analizziamo ora un esempio pratico che ci permette di illustrare la sintassi avanzata di MASM per la gestione delle procedure; a tale proposito, supponiamo di voler creare un programma con modello di memoria LARGE, conforme alle convenzioni C per le procedure.
In Assembly "classico", tutti gli aspetti relativi al modello di memoria e alle convenzioni di linguaggio, sono a carico del programmatore; in particolare, è nostro compito gestire il tipo di chiamata (NEAR o FAR) di una procedura, nonché altri aspetti importanti come il prolog code e l'epilog code.

Supponiamo ora che il nostro programma contenga una procedura dotata del seguente prototipo, espresso secondo la sintassi del C:
int far TestProc(long param1, int param2, int param3);
Questa procedura quindi è dotata dei tre parametri: param1 (intero con segno a 32 bit), param2 (intero con segno a 16 bit) e param3 (intero con segno a 16 bit); la procedura, inoltre, termina restituendo un intero con segno a 16 bit.
Supponiamo poi che TestProc abbia bisogno di due variabili locali chiamate locVar1 (intero con segno a 32 bit) e locVar2 (intero con segno a 16 bit); per gestire la chiamata di questa procedura definiamo le quattro variabili visibili in Figura 29.3. In linguaggio C la chiamata di TestProc, con i primi tre argomenti visibili in Figura 29.3, assume il seguente aspetto:
retVal = TestProc(varDword, varWord1, varWord2);
Come si può notare, il valore di ritorno di TestProc viene salvato in retVal; naturalmente, in base alle convenzioni che già conosciamo, il compilatore C genera il codice macchina necessario per trasferire in retVal il valore a 16 bit che TestProc ha inserito in AX.

La precedente chiamata C viene convertita dal compilatore nel codice macchina che corrisponde alla seguente sequenza di istruzioni Assembly: Nel rispetto delle convenzioni C, gli argomenti vengono passati alla procedura a partire dall'ultimo; la sequenza di inserimento nello stack è rappresentata quindi dai 2 byte di varWord2, seguiti dai 2 byte di varWord1 e dai 4 byte di varDword.
Quando il controllo torna al caller, viene effettuata la pulizia dello stack; siccome abbiamo inserito 8 byte nello stack, la pulizia consiste nel sommare il valore immediato 8 a SP.
La procedura TestProc restituisce il suo valore di ritorno in AX; in Assembly il compito di leggere questo valore e memorizzarlo in retVal spetta, ovviamente, al programmatore.

Per analizzare in dettaglio quello che accade nello stack, partiamo dalla condizione SP=0202h e supponiamo che l'indirizzo di ritorno sia Seg:Offset=0CF2h:003Bh; la chiamata FAR di TestProc comporta i seguenti passi eseguiti dalla CPU: Il salto FAR ci porta direttamente all'interno di TestProc; la Figura 29.4 mostra l'aspetto che assume questa procedura in stile Assembly classico: Per giustificare questa situazione ricordiamoci innanzi tutto che appena entrati in TestProc abbiamo SP=01F6h; le prime tre istruzioni presenti all'interno di TestProc sono le seguenti: Come già sappiamo, queste tre istruzioni ci servono per accedere ai parametri e alle variabili locali di TestProc; quando la CPU incontra queste tre istruzioni esegue le seguenti operazioni: Quest'ultima operazione serve per ottenere 6 byte di spazio nello stack, da destinare alle variabili locali; come è stato detto in precedenza, TestProc ha bisogno di due variabili locali chiamate simbolicamente locVar1 (4 byte) e locVar2 (2 byte).

Tutte le operazioni che comprendono l'inserimento degli argomenti nello stack e le tre precedenti istruzioni, rappresentano il prolog code di TestProc; al termine del prolog code otteniamo quindi BP=01F4h e SP=01EEh. Per maggiore chiarezza, tutta questa situazione viene illustrata in Figura 29.5. L'aspetto importante da ricordare è dato dal fatto che con questo sistema, i parametri di TestProc vengono a trovarsi a spiazzamenti positivi rispetto a BP, mentre le variabili locali vengono a trovarsi a spiazzamenti negativi rispetto a BP; dalla Figura 29.5, infatti, ricaviamo le seguenti informazioni: È importante osservare che param1 si trova all'offset BP+6 e occupa 4 byte nello stack; di conseguenza, param2 si troverà 4 byte più avanti e cioè all'offset BP+10. Analogamente, la variabile locale locVar1 occupa 4 byte e quindi si trova all'offset BP-4; la variabile locale locVar2 occupa, invece, 2 byte e si trova quindi all'offset BP-6 (cioè, 2 byte più indietro).
Si tenga presente che in Figura 29.5, i valori 002B14FFh per locVar1 e 01BCh per locVar2 sono puramente simbolici; in assenza di inizializzazione, tutte le variabili locali di un programma hanno un contenuto casuale dovuto al fatto che lo stack viene continuamente scritto e sovrascritto.

Qualcuno potrebbe avere dei dubbi in relazione agli offset delle due variabili locali locVar1 e locVar2; per chiarire questi dubbi bisogna osservare innanzi tutto che la porzione di memoria visibile in Figura 29.5 viene disposta, come al solito, con gli indirizzi crescenti da destra verso sinistra. Ricordiamoci, inoltre, che l'offset di una locazione di memoria coincide con l'offset del suo BYTE meno significativo; di conseguenza, la variabile locale locVar1 parte dall'offset BP-4 ed occupa nello stack i 4 BYTE che si trovano agli offset BP-4, BP-3, BP-2 e BP-1. Lo stesso discorso vale anche per locVar2; questa variabile locale, infatti, parte dall'offset BP-6 ed occupa nello stack i 2 BYTE che si trovano agli offset BP-6 e BP-5.
Le considerazioni appena svolte dimostrano ancora una volta quanto sia importante in questi casi tracciare su un foglio di carta uno schema dello stack come quello visibile in Figura 29.5; se si prova a svolgere mentalmente tutti i calcoli descritti in precedenza, si va incontro sicuramente a numerosi errori. Un ulteriore aiuto al programmatore deriva poi dall'uso delle macro alfanumeriche, visibili in Figura 29.5, per la gestione dei parametri e delle variabili locali di TestProc.

Passiamo ora alla descrizione della fase di terminazione di TestProc con conseguente restituzione del controllo al caller. Prima di tutto, all'interno di TestProc, dobbiamo caricare in AX il return value della procedura; nel nostro caso utilizziamo AX in quanto dobbiamo restituire un intero con segno a 16 bit.
A questo punto comincia l'epilog code di TestProc; il primo compito da svolgere consiste nel rimuovere dallo stack le variabili locali. Questo compito, che spetta sempre alla procedura (indipendentemente dalle convenzioni di linguaggio), viene svolto dall'istruzione:
mov sp, bp
In questo modo si ottiene SP=01F4h; si può anche scrivere:
add sp, 6
Questo secondo metodo, però, ci espone a dei potenziali errori in quanto ci obbliga a ricordare esattamente quanti byte di variabili locali avevamo richiesto allo stack; naturalmente, il primo metodo funziona solo se BP non ha subito alcuna modifica "imprevista".

Il secondo compito da svolgere consiste nel ripristinare BP ponendo BP=oldBP; questo lavoro viene svolto dall'istruzione:
pop bp
Subito dopo l'estrazione di old BP dallo stack, la CPU somma 2 byte a SP e ottiene SP=01F6h; a questo punto viene incontrato un FAR return che comporta le seguenti operazioni svolte dalla CPU: Subito dopo il salto FAR ci ritroviamo nell'istruzione associata all'indirizzo di ritorno; come abbiamo visto in precedenza (chiamata Assembly di TestProc), questa istruzione è:
add sp, 8
In questo modo stiamo restituendo allo stack gli 8 byte utilizzati per il passaggio degli argomenti a TestProc; come sappiamo, infatti, in C questo compito spetta al caller.
Subito dopo questa istruzione otteniamo SP=0202h e in questo modo abbiamo ripristinato il valore che SP aveva prima della chiamata di TestProc; al termine di tutte queste fasi il registro AX contiene il valore di ritorno della procedura.

29.2.1 Riscrittura di TestProc con la sintassi avanzata

Le considerazioni appena svolte si riferiscono al caso in cui il "rude" programmatore Assembly si assuma l'onore e l'onere di gestire in proprio il prolog code e l'epilog code delle procedure; come si può constatare, questi concetti una volta appresi risultano abbastanza semplici da applicare.

Vediamo ora come si modifica la situazione nel momento in cui decidiamo di servirci della sintassi avanzata di MASM; come è stato spiegato in precedenza, per usufruire di queste innovazioni dobbiamo utilizzare la direttiva .MODEL. Nel nostro caso, il programma deve quindi iniziare con:
.MODEL LARGE, C
La prima novità che ci offre la sintassi avanzata di MASM è rappresentata dalla possibilità di dichiarare, anche in Assembly, un prototipo per ogni procedura del programma che stiamo scrivendo.
Come accade per i linguaggi di alto livello, i prototipi permettono all'assembler di verificare la correttezza delle chiamate alle procedure generando, se necessario, gli opportuni messaggi di errore; in sostanza, incontrando una chiamata ad una procedura, l'assembler verifica se il numero e il tipo degli argomenti che stiamo passando coincide con le informazioni presenti nel prototipo.
La struttura generale di un prototipo di procedura assume in Assembly il seguente aspetto:
nome_procedura PROTO [model_modifier] [language_modifier] [argument_list]
Le parentesi quadre indicano che questi parametri sono tutti facoltativi.

Il parametro argument_list (lista argomenti) elenca le caratteristiche degli argomenti da passare alla procedura (parametri formali); ciascun argomento della lista deve essere specificato con la seguente sintassi:
nome_simbolico : tipo
Il nome_simbolico è facoltativo e in genere, come accade nel linguaggio C, viene omesso; il tipo può essere, come al solito, BYTE, WORD, DWORD, etc. I vari argomenti della lista sono separati tra loro da virgole.
L'ordine con il quale gli argomenti vengono disposti nel prototipo non ha niente a che vedere con le convenzioni per il passaggio degli argomenti alle procedure; indipendentemente, quindi, dal parametro di linguaggio utilizzato, gli argomenti di TestProc vanno disposti sempre nello stesso ordine.
Il modificatore di modello di memoria (NEAR o FAR) deve essere inserito solo se vogliamo aggirare l'analogo parametro specificato nella direttiva .MODEL; lo stesso discorso vale per il modificatore di linguaggio (C, PASCAL, etc).
Il luogo ideale per l'inserimento dei prototipi delle procedure è sicuramente la parte iniziale del modulo Assembly (si può anche ricorrere ad un include file da includere all'inizio del modulo che ne ha bisogno); naturalmente, il prototipo di una procedura deve precedere la chiamata della procedura stessa.

Per applicare a TestProc le cose appena dette, con MASM dobbiamo scrivere:
TestProc PROTO FAR C :DWORD, :WORD, :WORD
Osserviamo che i modificatori di linguaggio e di modello di memoria coincidono con quelli presenti nella direttiva .MODEL e quindi sono superflui; il prototipo di TestProc può essere riscritto quindi come:
TestProc PROTO :DWORD, :WORD, :WORD
Se vogliamo essere pignoli possiamo anche specificare i nomi simbolici dei parametri formali di TestProc; in questo caso dobbiamo scrivere:
TestProc PROTO param1 :DWORD, param2 :WORD, param3 :WORD
Una volta dichiarato il prototipo, possiamo passare alla definizione della procedura; nel caso generale la definizione di una procedura inizia con la seguente intestazione, in versione MASM:
nome_procedura PROC [model_modifier] [language_modifier] [argument_list]
Il MASM richiede che l'intestazione di una procedura sia identica al prototipo; di conseguenza, gli eventuali parametri model_modifier e language_modifier specificati nel prototipo devono essere specificati nello stesso ordine anche nell'intestazione della procedura.
In generale, possiamo notare che l'intestazione della procedura è del tutto simile al suo prototipo; questa volta la lista argument_list deve specificare anche i nomi simbolici che intendiamo utilizzare per i parametri formali della procedura.
I parametri model_modifier e language_modifier devono essere specificati solo se vogliamo aggirare gli analoghi parametri passati alla direttiva .MODEL.

All'interno della procedura possiamo definire le necessarie variabili locali attraverso la direttiva LOCAL; la sintassi generale da utilizzare è la seguente:
LOCAL nome_var1 : tipo, nome_var2 : tipo, ...
Sia il prolog code che l'epilog code non vengono più gestiti dal programmatore; tutto questo lavoro, infatti, viene delegato all'assembler.

Applichiamo ora tutti questi concetti al caso di TestProc; la Figura 29.6 illustra il nuovo aspetto assunto dalla procedura TestProc con l'uso della sintassi avanzata di MASM. La prima cosa da osservare riguarda il fatto che questa volta, come accade nei linguaggi di alto livello, la definizione di TestProc deve specificare anche la lista dei parametri formali che la procedura richiede; è chiaro che il numero e il tipo dei parametri di questa lista deve coincidere esattamente con le analoghe informazioni specificate nel prototipo di TestProc.
Subito dopo l'intestazione di TestProc incontriamo la direttiva LOCAL; come si può notare, questa direttiva ci permette di utilizzare una sintassi semplicissima per la definizione delle variabili locali. Nel Capitolo 24 abbiamo anche visto che la direttiva LOCAL viene impiegata per dichiarare etichette con visibilità locale all'interno del corpo di una macro alfanumerica.

In Figura 29.6 notiamo che dopo la direttiva LOCAL inizia subito il corpo della procedura; inoltre, al termine di TestProc troviamo solamente il caricamento in AX del valore di ritorno e l'istruzione RET.
Come è stato spiegato in precedenza, il prolog code e l'epilog code devono essere omessi in quanto vengono gestiti direttamente dall'assembler; dopo aver svolto il proprio lavoro, l'assembler ottiene lo stesso risultato visibile in Figura 29.4!
Affinché l'assembler possa gestire automaticamente il prolog code e l'epilog code di una procedura, dobbiamo servirci di una apposita sintassi avanzata per la chiamata della procedura stessa; a tale proposito, viene resa disponibile una potente direttiva di MASM denominata INVOKE!

29.3 La direttiva INVOKE

Con le caratteristiche avanzate di MASM, le procedure possono essere chiamate attraverso l'uso di una sintassi molto simile a quella dei linguaggi di alto livello; a tale proposito, dobbiamo servirci della apposita direttiva INVOKE.

La sintassi generale da utilizzare con il MASM è:
INVOKE nome_procedura, [argument_list]
La argument_list rappresenta, ovviamente, la lista dei nomi degli argomenti (parametri attuali) da passare alla procedura; i vari nomi sono separati da virgole.
È importante notare anche la presenza della virgola dopo il nome_procedura.

Applicando questi concetti alla procedura TestProc, nel caso del MASM otteniamo: Come si può notare, il programmatore deve preoccuparsi esclusivamente della chiamata della procedura e del salvataggio dell'eventuale valore di ritorno; tale salvataggio deve essere effettuato subito dopo la chiamata della procedura per evitare che il contenuto di AX possa essere inavvertitamente sovrascritto da qualche altra istruzione.
Tutto il lavoro legato al prolog code e all'epilog code viene svolto dall'assembler che, incontrando questo tipo di chiamata, produce tutto il necessario codice macchina; nel nostro caso, l'assembler nota la presenza dei parametri LARGE e C associati alla direttiva .MODEL e genera quindi tutto il codice macchina necessario per inserire gli argomenti nello stack (a partire dall'ultimo), per la chiamata FAR di TestProc e per la pulizia dello stack a carico del caller (somma del valore 8 a SP).
Per renderci conto del lavoro svolto dall'assembler possiamo richiedere, come al solito, il listing file del modulo che vogliamo assemblare.

Sicuramente, uno dei vantaggi più interessanti delle caratteristiche avanzate di MASM è rappresentato dal fatto che per adattare un intero modulo Assembly ad un diverso modello di memoria e/o ad una diversa convenzione di linguaggio, basta modificare solamente gli opportuni parametri passati alla direttiva .MODEL; nel nostro caso possiamo scrivere, ad esempio:
.MODEL SMALL, PASCAL
In un caso del genere l'assembler converte automaticamente tutte le procedure da FAR a NEAR e utilizza le convenzioni del linguaggio Pascal per la gestione delle procedure stesse; inoltre, gli indirizzi NEAR vengono utilizzati anche per l'accesso ai dati del programma.

29.4 L'operatore ADDR di MASM

Nel Capitolo 25, dedicato ai sottoprogrammi, è stata messa in evidenza la delicata situazione che si viene a creare quando all'interno di una procedura dobbiamo gestire l'offset di una variabile locale; in particolare, abbiamo visto che in un caso del genere non avrebbe senso utilizzare l'operatore OFFSET. Questo operatore, infatti, lavora solo in fase di assemblaggio e quindi richiede un operando che deve essere un identificatore definito staticamente nel programma; come sappiamo, tali identificatori hanno un offset ben preciso che viene assegnato dall'assembler (o dal linker nel caso di rilocazione) e rimane fisso per tutta la fase di esecuzione di un programma.
La situazione cambia radicalmente nel caso di una variabile locale; le variabili locali di una procedura vengono create nello stack nel preciso momento in cui la procedura stessa viene chiamata dal caller. Questo significa che se chiamiamo 10 volte una stessa procedura, può capitare che ad ogni chiamata le sue variabili locali vengano create sempre ad offset differenti rispetto alla chiamata precedente; tutto dipende, infatti, dall'offset a cui punta SP nello stack al momento della chiamata (un caso emblematico è rappresentato dalle chiamate ricorsive).
Bisogna anche ricordare che le variabili locali vengono distrutte al termine della procedura che le ha create; ciò significa che una variabile locale esiste (ed è quindi visibile) solo all'interno della procedura a cui appartiene.
Se, ad esempio, all'interno di TestProc (vedi Figura 29.4) vogliamo caricare in AX l'offset di locVar1, non possiamo assolutamente scrivere:
mov ax, offset locVar1
Questa istruzione, infatti, carica in AX un valore privo di senso; per risolvere il problema possiamo utilizzare l'istruzione LEA scrivendo:
lea ax, locVar1
L'assembler espande la macro locVar1 e ottiene l'istruzione:
lea ax, [bp-4]
Questa istruzione calcola BP-4 (senza alterare il contenuto di BP) e carica il risultato in AX; ovviamente, tale risultato è proprio l'offset di locVar1 all'interno del blocco stack (referenziato da SS).

Come è stato spiegato nel Capitolo 16, bisogna stare molto attenti al fatto che la sintassi richiesta dall'istruzione LEA potrebbe confondere le idee al programmatore; l'istruzione precedente carica in AX l'offset rappresentato da BP-4 e non il contenuto a 16 bit della locazione di memoria che si trova all'indirizzo logico SS:(BP-4).

Il problema appena esposto si presenta anche quando nella argument_list di INVOKE è presente l'offset di qualche variabile locale. Supponiamo, ad esempio, di dover chiamare una procedura Proc2 dall'interno di TestProc; se dobbiamo passare a Proc2 il parametro param2 e l'offset di locVar2, dovremmo scrivere:
invoke Proc2, param2, offset locVar2
Questa istruzione produce un risultato privo di senso che in genere manda in crash il programma; per risolvere il problema possiamo scrivere, invece: La direttiva INVOKE di MASM permette di rendere più compatto questo codice grazie all'operatore ADDR; l'operatore ADDR, infatti, equivale all'operatore OFFSET per le variabili statiche e all'istruzione LEA per le variabili locali (dinamiche). Nel caso di MASM possiamo quindi scrivere:
invoke Proc2, param2, addr locVar2
Bisogna ricordare, però, che INVOKE, nello svolgere il proprio lavoro, potrebbe sovrascrivere i registri citati in precedenza in questo capitolo; è necessario quindi usare questa direttiva con molta cautela.

29.5 Convenzioni per i segmenti di programma

Nel corso degli anni, l'ambiente DOS/Windows è sempre stato dominato (nel bene e nel male) dai compilatori Microsoft; questa "posizione dominante" ha permesso alla stessa Microsoft di imporre una serie di convenzioni relative alla struttura interna dei programmi. In particolare, sono state definite le caratteristiche complete dei program segment utilizzati dai compilatori per l'organizzazione a basso livello dei programmi; l'insieme di tutte queste convenzioni ha dato vita a dei veri e propri standard a cui si sono adeguati molti altri produttori di compilatori.
Per facilitare l'interfacciamento tra l'Assembly e i linguaggi di alto livello, gli assembler come MASM offrono il supporto completo della segmentazione standard utilizzata dai compilatori; le caratteristiche avanzate di MASM coprono anche questo importante aspetto.

Per ottimizzare al massimo la compattezza e l'efficienza dei programmi, i compilatori si servono di un particolare sistema di segmentazione che comporta anche la suddivisione del codice e dei dati in differenti categorie; analizziamo in dettaglio i vari tipi di program segment disponibili.

29.5.1 Il segmento principale per i dati statici inizializzati

Nei linguaggi di alto livello, i dati statici inizializzati sono le variabili definite al di fuori di qualsiasi procedura; tali variabili vengono anche inizializzate al momento stesso della loro definizione.
In C possiamo avere, ad esempio:
int var_word1 = 12500;
In Pascal possiamo avere, ad esempio:
CONST var_word1: Integer = 12500;
In Assembly possiamo avere, ad esempio:
var_word1 dw 12500
Questi dati appartengono alla categoria delle variabili globali del programma; a ciascuna di esse viene assegnato un indirizzo che rimane fisso (statico) per tutta la fase di esecuzione. La durata di queste variabili è pari a quella dell'intero programma; la loro visibilità è limitata al modulo di appartenenza, ma può essere estesa anche ad altri moduli.

Nei limiti del possibile, i compilatori cercano di inserire tutti i dati statici inizializzati in un apposito program segment avente le seguenti caratteristiche: Siccome il blocco _DATA è dotato di nome e attributi standard, per poterlo definire possiamo servirci della apposita direttiva semplificata:
.DATA
Quando l'assembler incontra questa direttiva, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla direttiva .DATA è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

29.5.2 Il segmento principale per i dati statici non inizializzati

Nei linguaggi di alto livello, i dati statici non inizializzati sono le variabili definite al di fuori di qualsiasi procedura; tali variabili non vengono inizializzate durante la definizione, per cui il loro contenuto iniziale dipende dalle convenzioni del linguaggio utilizzato.
In C possiamo avere, ad esempio:
int var_temp;
In Pascal possiamo avere, ad esempio:
VAR var_temp: Integer;
In Assembly possiamo avere, ad esempio:
var_temp dw ?
Questi dati appartengono alla categoria delle variabili globali del programma; a ciascuna di esse viene assegnato un indirizzo che rimane fisso (statico) per tutta la fase di esecuzione. La durata di queste variabili è pari a quella dell'intero programma; la loro visibilità è limitata al modulo di appartenenza, ma può essere estesa anche ad altri moduli.

Nei limiti del possibile, i compilatori cercano di inserire tutti i dati statici non inizializzati in un apposito program segment avente le seguenti caratteristiche: Siccome il blocco _BSS è dotato di nome e attributi standard, per poterlo definire possiamo servirci della apposita direttiva semplificata:
.DATA?
Quando l'assembler incontra questa direttiva, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla direttiva .DATA? è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

29.5.3 Il segmento principale per i dati statici costanti

Consideriamo la seguente istruzione relativa al linguaggio C:
printf("Premere un tasto per continuare");
Questa istruzione chiama la funzione di libreria printf per visualizzare sullo schermo la stringa "Premere un tasto per continuare"; tale stringa viene usata come argomento da passare a printf, ma non è associata ad alcuna variabile di tipo stringa.
Il compilatore C tratta questi particolari dati come variabili globali prive di nome; tali variabili vengono inserite in un apposito program segment riservato ai cosiddetti dati costanti (in effetti, una stringa come "Premere un tasto per continuare" è un dato costante in quanto viene definita una volta per tutte e non può subire modifiche durante la fase di esecuzione del programma).

Nei limiti del possibile, i compilatori cercano di inserire tutti i dati statici costanti in un apposito program segment avente le seguenti caratteristiche: Siccome il blocco CONST è dotato di nome e attributi standard, per poterlo definire possiamo servirci della apposita direttiva semplificata:
.CONST
Quando l'assembler incontra questa direttiva, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla direttiva .CONST è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

Vista l'importanza dell'argomento, consideriamo un altro esempio rappresentato dalla seguente definizione C:
char *pt_str = "Inserire un numero";
Quando il compilatore C incontra questa definizione, riserva 19 byte di memoria (possibilmente nel blocco CONST) destinati a contenere i 18 caratteri della stringa più lo zero finale (stringa C); successivamente, il compilatore richiede 2 byte di memoria (possibilmente nel blocco _DATA) e li assegna alla variabile puntatore pt_str. Nei 2 byte assegnati a pt_str viene caricato l'offset iniziale della stringa; a questo punto il compilatore ha creato una variabile globale pt_str che è un puntatore NEAR alla stringa "Inserire un numero".
Si potrebbe obiettare che pt_str non può essere un puntatore NEAR in quanto punta ad una stringa che si trova in un segmento dati differente; a tale proposito, si veda più avanti la sezione 29.5.5.
Anche se la precedente definizione viene posta all'interno di una funzione, il compilatore crea pt_str nello stack (variabile locale), ma posiziona la stringa ugualmente nel blocco CONST; questo comportamento è legato al fatto che le stringhe occupano troppo spazio nello stack.
Possiamo dire quindi che, in questo caso, pt_str è una variabile locale, mentre la stringa è, in ogni caso, un dato statico che non viene distrutto al termine della funzione. Tutto ciò significa che in C è perfettamente lecito restituire al caller l'indirizzo iniziale della stringa assegnata a pt_str; il caller, infatti, riceve l'indirizzo di un dato statico che esiste per tutta la fase di esecuzione del programma. È un errore, invece, restituire al caller l'indirizzo di pt_str; infatti, pt_str è una variabile locale che quindi viene distrutta al termine della funzione che l'ha creata.

Per quanto riguarda i dati costanti di tipo numerico, bisogna dire che a seconda della loro dimensione in byte, i compilatori possono decidere se inserirli nel blocco CONST o direttamente nel codice macchina; consideriamo, ad esempio, la seguente istruzione C:
printf("Diametro = %f\n", circonferenza / 3.14);
Un compilatore C a 16 bit potrebbe anche decidere di inserire il valore 3.14 nel blocco CONST; un compilatore C a 32 bit potrebbe, invece, decidere di inserire il valore 3.14 direttamente nel codice macchina (in formato IEEE per i numeri reali a 32 bit).

29.5.4 Il segmento di stack per i dati dinamici

Come sappiamo, il blocco stack viene utilizzato per la creazione e la distruzione dinamica dei dati temporanei di un programma (come le variabili locali definite all'interno di una procedura).

I compilatori che supportano l'uso dello stack creano un apposito program segment avente le seguenti caratteristiche: Siccome il blocco STACK è dotato di nome e attributi standard, per poterlo definire possiamo servirci della apposita direttiva semplificata:
.STACK [size]
Quando l'assembler incontra questa direttiva, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla direttiva .STACK è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.
Il parametro facoltativo size, se è presente, indica la dimensione in byte che vogliamo assegnare allo stack; è fondamentale che venga specificato sempre un numero pari di byte in modo da garantire il corretto allineamento di SP. In assenza del parametro size gli assembler utilizzano una dimensione predefinita che, in genere, è pari a 1024 byte.

29.5.5 Il gruppo DGROUP

Dopo aver distribuito i dati di un programma nei vari segmenti descritti in precedenza, i compilatori definiscono un raggruppamento avente le seguenti caratteristiche:
DGROUP GROUP _DATA, CONST, _BSS, STACK
Successivamente, il compilatore stesso pone:
DS = SS = DGROUP
Questo modo di procedere dei compilatori, comporta notevoli vantaggi in termini di efficienza e compattezza del codice macchina che viene prodotto.
Prima di tutto, osserviamo che referenziando DGROUP con DS, si fa in modo che tutti i dati contenuti in _DATA, CONST, _BSS e STACK possano essere gestiti attraverso indirizzi di tipo NEAR; come sappiamo, tali indirizzi sono formati dalla sola componente Offset e quindi, oltre ad occupare meno memoria di un indirizzo FAR, sono anche più veloci da gestire.
Un altro aspetto importante è dato dal fatto che DGROUP comprende, nell'ordine, i dati inizializzati contenuti in _DATA e CONST, seguiti dai dati non inizializzati contenuti in _BSS e dai dati temporanei contenuti in STACK; disponendo DGROUP come ultimo segmento del programma, i compilatori possono ottimizzare notevolmente le esigenze di memoria. Il file eseguibile generato dal compilatore comprende solamente i blocchi _DATA e CONST (oltre ai blocchi di codice e ad eventuali blocchi di dati FAR che verranno illustrati nel seguito); la memoria necessaria per i blocchi _BSS e STACK verrà richiesta solo durante la fase di esecuzione del programma (a tale proposito, si veda il significato del campo MinimumAllocation nella Figura 13.8 del Capitolo 13).

I compilatori che supportano i modelli di memoria, permettono al programmatore di abilitare o disabilitare l'inserimento del blocco stack in DGROUP; ad esempio, il Borland C/C++ 3.1 dispone dell'opzione Assume SS equal DS (quindi, abilitando tale opzione, il blocco stack verrà inserito in DGROUP).
Come è stato spiegato in precedenza, anche l'Assembly permette questo tipo di scelta; a tale proposito, abbiamo visto che è necessario utilizzare i parametri NEARSTACK e FARSTACK (da passare alla direttiva .MODEL).

29.5.6 I segmenti aggiuntivi per i dati statici inizializzati

Se il blocco _DATA non è sufficiente per contenere tutti i dati statici inizializzati di un programma, il compilatore crea uno o più blocchi aggiuntivi ciascuno dei quali ha le seguenti caratteristiche: Siccome il blocco FAR_DATA è dotato di attributi standard, per poterlo definire possiamo servirci della apposita direttiva semplificata:
.FARDATA [name]
Quando l'assembler incontra questa direttiva, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla direttiva .FARDATA è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.
Il parametro facoltativo name, se è presente, permette di assegnare un nome al segmento di programma (è proibito utilizzare nomi riservati come _DATA o _BSS); in assenza di tale parametro, gli assembler utilizzano il nome predefinito FAR_DATA.

Come si può notare, questo particolare blocco ha l'attributo di combinazione PRIVATE; ciò significa che se definiamo numerosi blocchi FAR_DATA, ciascuno in un differente modulo, tutti questi blocchi non verranno fusi tra loro (mentre se si trovano tutti nello stesso modulo, daranno vita ad un unico blocco FAR_DATA).
In pratica, se in un programma creiamo 10 blocchi FAR_DATA distribuiti in 10 moduli differenti, otteniamo 10 blocchi FAR_DATA distinti; di conseguenza, anche se non stiamo utilizzando nomi diversi per ciascun blocco, non esiste la possibilità di confondere tra loro i vari blocchi FAR_DATA.

Naturalmente, tutti i dati inizializzati presenti all'interno dei blocchi FAR_DATA vengono considerati di tipo FAR; per poter gestire questi dati è necessario quindi specificare i loro indirizzi completi Seg:Offset.
La situazione appena descritta implica che potrebbe presentarsi la necessità di utilizzare DS per referenziare un blocco FAR_DATA; in casi del genere bisogna sempre ricordarsi di ripristinare il contenuto originario di DS. I compilatori si servono normalmente di DS per gestire DGROUP; ciò significa che dopo aver utilizzato DS per referenziare un blocco FAR_DATA, dobbiamo ripristinarlo in modo da ottenere di nuovo DS=DGROUP.

29.5.7 I segmenti aggiuntivi per i dati statici non inizializzati

Se il blocco _BSS non è sufficiente per contenere tutti i dati statici non inizializzati di un programma, il compilatore crea uno o più blocchi aggiuntivi ciascuno dei quali ha le seguenti caratteristiche: Siccome il blocco FAR_BSS è dotato di attributi standard, per poterlo definire possiamo servirci della apposita direttiva semplificata:
.FARDATA? [name]
Quando l'assembler incontra questa direttiva, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla direttiva .FARDATA? è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.
Il parametro facoltativo name, se è presente, permette di assegnare un nome al segmento di programma (è proibito utilizzare nomi riservati come _DATA o _BSS); in assenza di tale parametro, gli assembler utilizzano il nome predefinito FAR_BSS.

Come si può notare, questo particolare blocco ha l'attributo di combinazione PRIVATE; ciò significa che se definiamo numerosi blocchi FAR_BSS, ciascuno in un differente modulo, tutti questi blocchi non verranno fusi tra loro (mentre se si trovano tutti nello stesso modulo, daranno vita ad un unico blocco FAR_BSS).
In pratica, se in un programma creiamo 10 blocchi FAR_BSS distribuiti in 10 moduli differenti, otteniamo 10 blocchi FAR_BSS distinti; di conseguenza, anche se non stiamo utilizzando nomi diversi per ciascun blocco, non esiste la possibilità di confondere tra loro i vari blocchi FAR_BSS.

Naturalmente, tutti i dati non inizializzati presenti all'interno dei blocchi FAR_BSS vengono considerati di tipo FAR; per poter gestire questi dati è necessario quindi specificare i loro indirizzi completi Seg:Offset.
Tutto ciò implica che potrebbe presentarsi la necessità di utilizzare DS per referenziare un blocco FAR_BSS; in casi del genere bisogna sempre ricordarsi di ripristinare il contenuto originario di DS. I compilatori si servono normalmente di DS per gestire DGROUP; ciò significa che dopo aver utilizzato DS per referenziare un blocco FAR_BSS, dobbiamo ripristinarlo in modo da ottenere di nuovo DS=DGROUP.

29.5.8 Il segmento principale per il codice

Per gestire il blocco codice contenente l'entry point e le istruzioni principali, i compilatori creano un apposito program segment avente le seguenti caratteristiche: Siccome il blocco _TEXT è dotato di nome e attributi standard, per poterlo definire possiamo servirci della apposita direttiva semplificata:
.CODE
Quando l'assembler incontra questa direttiva, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla direttiva .CODE è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

Come si può facilmente intuire, al momento di caricare il programma in memoria per la fase di esecuzione, il SO pone CS=_TEXT.

29.5.9 I segmenti aggiuntivi per il codice

Se il blocco _TEXT non è sufficiente per contenere tutto il codice del programma, il compilatore crea uno o più blocchi aggiuntivi ciascuno dei quali ha le seguenti caratteristiche: Siccome il blocco name_TEXT è dotato di nome e attributi standard, per poterlo definire possiamo servirci della apposita direttiva semplificata:
.CODE [name]
Quando l'assembler incontra questa direttiva, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla direttiva .CODE [name] è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

Il parametro facoltativo name, se è presente, indica il nome personalizzato che vogliamo dare al segmento di codice; in assenza di questo parametro viene utilizzato il nome del modulo in cui si trova il blocco codice, seguito dalla stringa _TEXT. Se, ad esempio, viene incontrata una direttiva .CODE in un modulo chiamato LIB1.ASM, si ottiene il nome LIB1_TEXT.
Di conseguenza, se nel modulo LIB1.ASM sono presenti 10 blocchi .CODE privi del parametro name, verranno creati 10 blocchi chiamati LIB1_TEXT; questi 10 blocchi hanno tutti lo stesso nome e gli stessi attributi (compreso PUBLIC), per cui verranno fusi in un unico blocco LIB1_TEXT.
Se vogliamo avere 10 blocchi name_TEXT distinti, dobbiamo inserirli in 10 moduli differenti (uno per modulo), oppure dobbiamo assegnare loro 10 nomi differenti.

29.5.10 Considerazioni generali sulle convenzioni per i segmenti di programma

È necessario tenere presente che non tutti i compilatori seguono le convenzioni appena esposte sui segmenti di programma; considerando, ad esempio, le numerose varianti del BASIC (compilato), può anche capitare che i diversi compilatori di tale linguaggio seguano convenzioni tra loro incompatibili.
Se abbiamo la necessità di sfruttare tutte queste caratteristiche avanzate, dobbiamo quindi munirci di un adeguato compilatore; fortunatamente, tutti i compilatori più diffusi sul mercato sono in grado di supportare le convenzioni seguite da MASM.

Un altro aspetto importante è dato dal fatto che solo in presenza della direttiva .MODEL e delle varie direttive avanzate (per i segmenti di programma, per la chiamata delle procedure, etc) gli assembler come MASM sono in grado di svolgere automaticamente tutto il loro lavoro (compresa la generazione del prolog code, dell'epilog code, del gruppo DGROUP, etc); in caso contrario, tutto questo lavoro spetta al programmatore.

Infine, è necessario ricordare un aspetto che, se trascurato, può portare alla scrittura di programmi che in fase di esecuzione vanno subito in crash; tale aspetto riguarda il fatto che i compilatori, ovviamente, provvedono autonomamente alla inizializzazione di DS (e, se necessario, anche di ES). In Assembly, invece, questo importantissimo compito spetta al programmatore; in generale, la cosa migliore da fare consiste nel seguire le convenzioni dei compilatori che pongono DS=DGROUP.

29.6 Variabili simboliche create dall'assembler

Per facilitare ulteriormente la gestione dei segmenti standard da parte del programmatore, gli assembler come MASM creano una serie di variabili simboliche da utilizzare durante la fase di esecuzione; la Figura 29.7 elenca le variabili supportate da MASM. Esistono anche numerose altre variabili simboliche che, però, variano da assembler ad assembler; a tale proposito, si consiglia di consultare i manuali dell'assembler che si intende utilizzare. Ad esempio, se in un determinato momento della fase di esecuzione CS sta referenziando il blocco di codice principale _TEXT, allora in quello stesso momento abbiamo:
@code = _TEXT
Se, in un momento successivo, l'esecuzione salta ad un blocco di codice chiamato LIB1_TEXT, allora in quello stesso momento abbiamo:
@code = LIB1_TEXT
Se in un determinato momento della fase di esecuzione stiamo accedendo ad un blocco FAR_DATA (blocco privato di dati FAR inizializzati), allora in quello stesso momento abbiamo:
@fardata = FAR_DATA
Se stiamo utilizzando il parametro (predefinito) NEARSTACK, allora appare evidente che la variabile @stack equivale al nome DGROUP; se, invece, stiamo utilizzando il parametro FARSTACK, allora la variabile @stack equivale al nome STACK.

Subito dopo l'entry point di un programma che fa uso delle caratteristiche avanzate di MASM, DGROUP viene generalmente assegnato a DS; a quel punto abbiamo quindi:
@data = DGROUP

29.7 Esempi pratici

Per analizzare in pratica i concetti appena esposti, utilizziamo le caratteristiche avanzate di MASM per riscrivere gli esempi presentati nel Capitolo 27; più avanti verranno illustrati i dettagli relativi ai programmi Assembly distribuiti su più moduli.

29.7.1 Modello TINY

La struttura classica di un programma con modello di memoria TINY prevede la presenza di un solo blocco contenente codice, dati e stack; per rendere il programma più ordinato e leggibile, ci viene data la possibilità di distribuire le varie informazioni nei segmenti: _TEXT, _DATA, CONST e _BSS.
Naturalmente, questa suddivisione è del tutto fittizia; infatti, il compilatore provvederà a raggruppare tutte queste informazioni in un apposito blocco DGROUP avente le seguenti caratteristiche:
DGROUP GROUP _TEXT, _DATA, CONST, _BSS
Successivamente, l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: DGROUP, DS: DGROUP, SS: DGROUP
Come sappiamo, in un programma con modello di memoria TINY destinato ad essere convertito in un eseguibile in formato COM, il compito di creare il blocco stack spetta al SO; lo stesso SO provvede all'inizializzazione di tutti i registri di segmento ponendo:
CS = DGROUP, DS = DGROUP, ES = DGROUP, SS = DGROUP
Inoltre, lo stesso SO pone:
IP = 0100h, SP = FFFEh
In Figura 29.8 utilizziamo i concetti appena esposti per riscrivere il programma illustrato nella Figura 27.4 del Capitolo 27. Il prototipo di printStr2 è molto importante in quanto specifica un modello di indirizzamento FAR (per le procedure), differente da quello predefinito NEAR; grazie a tale prototipo, l'assembler è in grado di generare automaticamente le FAR call a printStr2 e i FAR return dalla stessa procedura.

All'interno di printStr2 il contenuto originale di DS non viene preservato in quanto tale precauzione è superflua; osserviamo, infatti, che gli indirizzi delle stringhe stampate da questa procedura, hanno una componente Seg=DGROUP che, ovviamente, non altera il contenuto di DS!

In Figura 29.8 si nota subito l'assenza delle direttive ASSUME, del prolog code e dell'epilog code per le procedure; tutti questi aspetti vengono gestiti automaticamente dall'assembler.

29.7.2 Modello SMALL

La struttura classica di un programma con modello di memoria SMALL prevede la presenza di un solo blocco codice, un solo blocco dati e un solo blocco stack; per rendere il programma più ordinato e leggibile, ci viene data la possibilità di distribuire le varie informazioni nei segmenti: _TEXT, _DATA, CONST, _BSS e STACK.
In fase di assemblaggio, l'assembler provvederà a raggruppare le varie categorie di dati in un apposito blocco DGROUP avente le seguenti caratteristiche:
DGROUP GROUP _DATA, CONST, _BSS, STACK
Notiamo che questa volta (e anche per i modelli di memoria illustrati nel seguito) il blocco _TEXT viene escluso da DGROUP; se vogliamo escludere anche il blocco STACK, dobbiamo passare il parametro FARSTACK alla direttiva .MODEL.
In presenza del parametro NEARSTACK (o in assenza del parametro FARSTACK), l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: _TEXT, DS: DGROUP, SS: DGROUP
Il SO effettua quindi le seguenti inizializzazioni:
CS = _TEXT, DS = ES = PSP, SS = STACK = DGROUP
In presenza del parametro FARSTACK, l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: _TEXT, DS: DGROUP, SS: STACK
Il SO effettua quindi le seguenti inizializzazioni:
CS = _TEXT, DS = ES = PSP, SS = STACK
Il programmatore ha quindi l'importante compito di inizializzare DS e, se necessario, anche ES; chiaramente, la cosa più ovvia da fare consiste nel porre:
DS = DGROUP
In Figura 29.9 vengono utilizzate le direttive avanzate per riscrivere il programma illustrato nella Figura 27.5 del Capitolo 27. Notiamo subito che, questa volta, il programmatore ha il compito di creare il blocco stack; a tale proposito, utilizziamo la direttiva .STACK con un parametro che rappresenta la dimensione in byte di tale segmento di programma.
Questa tecnica ci impedisce di definire dati statici all'interno del blocco stack; se vogliamo fare una cosa del genere, dobbiamo tornare al metodo tradizionale per la definizione dei segmenti di programma. Nel nostro caso, seguendo le convenzioni MASM dobbiamo definire un blocco avente: nome STACK, allineamento PARA, combinazione STACK e classe 'STACK'; si tenga presente, però, che in questo modo si impedisce all'assembler di generare correttamente il gruppo DGROUP!

In questo esempio, la procedura printStr2 visualizza stringhe definite in vari segmenti di programma; di conseguenza, è importantissimo preservare il contenuto originale di DS!

Come al solito, analizzando il listing file si può osservare in dettaglio tutto il lavoro svolto dall'assembler per l'espansione delle direttive avanzate; notiamo, in particolare, il valore che l'assembler assegna alla variabile simbolica @stack. In caso di NEARSTACK l'assembler pone:
@stack = DGROUP
In caso di FARSTACK l'assembler pone:
@stack = STACK

29.7.3 Modello MEDIUM

La struttura classica di un programma con modello di memoria MEDIUM prevede la presenza di due o più blocchi di codice, un solo blocco dati e un solo blocco stack; per rendere il programma più ordinato e leggibile, ci viene data la possibilità di distribuire le varie informazioni nei segmenti: name_TEXT, _DATA, CONST, _BSS e STACK.
In fase di assemblaggio, l'assembler provvederà a raggruppare le varie categorie di dati in un apposito blocco DGROUP avente le seguenti caratteristiche:
DGROUP GROUP _DATA, CONST, _BSS, STACK
Se vogliamo escludere il blocco STACK dal gruppo DGROUP, dobbiamo passare il parametro FARSTACK alla direttiva .MODEL.
In presenza del parametro NEARSTACK (o in assenza del parametro FARSTACK), l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: name_TEXT, DS: DGROUP, SS: DGROUP
Il SO effettua quindi le seguenti inizializzazioni:
CS = name_TEXT, DS = ES = PSP, SS = STACK = DGROUP
In presenza del parametro FARSTACK, l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: name_TEXT, DS: DGROUP, SS: STACK
Il SO effettua quindi le seguenti inizializzazioni:
CS = name_TEXT, DS = ES = PSP, SS = STACK
Il programmatore ha quindi l'importante compito di inizializzare DS e, se necessario, anche ES; chiaramente, la cosa più ovvia da fare consiste nel porre:
DS = DGROUP
In Figura 29.10 vengono utilizzate le direttive avanzate per riscrivere il programma illustrato nella Figura 27.8 del Capitolo 27. Questa volta, la modalità di indirizzamento predefinita è NEAR per i dati che si trovano in DGROUP e FAR per le procedure; se vogliamo definire procedure di tipo NEAR, dobbiamo inserire il modificatore di modello NEAR nel relativo prototipo.
In Figura 29.10 notiamo che printStr1 viene definita, appunto, di tipo NEAR; analizzando il listing file possiamo notare che l'assembler, per ogni chiamata di questa procedura, genera il codice macchina di una NEAR call. All'interno di printStr1 si può anche notare il codice macchina C3h del NEAR return; per tutte le altre procedure, le chiamate predefinite sono di tipo FAR con conseguente FAR return (codice macchina CBh).

Sempre attraverso il listing file possiamo constatare la presenza dei tre segmenti di codice emedium_TEXT, CODESEGM2 e CODESEGM3; nei modelli di memoria che prevedono la presenza di due o più segmenti di codice, le convenzioni MASM stabiliscono che il nome del blocco codice principale sia composto dal nome del modulo che contiene la direttiva .CODE (nel nostro caso, EMEDIUM), seguito dalla stringa _TEXT.

È anche interessante analizzare il significato che la variabile @code assume nei vari punti del listato di Figura 29.10; come sappiamo, questa variabile rappresenta il program segment correntemente referenziato da CS.
Possiamo affermare allora che:

29.7.4 Modello COMPACT

La struttura classica di un programma con modello di memoria COMPACT prevede la presenza di un solo blocco codice, due o più blocchi di dati e un solo blocco stack; per rendere il programma più ordinato e leggibile, ci viene data la possibilità di distribuire le varie informazioni nei segmenti: _TEXT, _DATA, CONST, _BSS, STACK, FAR_DATA e FAR_BSS.
In fase di assemblaggio, l'assembler provvederà a raggruppare le varie categorie di dati in un apposito blocco DGROUP avente le seguenti caratteristiche:
DGROUP GROUP _DATA, CONST, _BSS, STACK
Se vogliamo escludere il blocco STACK dal gruppo DGROUP, dobbiamo passare il parametro FARSTACK alla direttiva .MODEL.
In presenza del parametro NEARSTACK (o in assenza del parametro FARSTACK), l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: _TEXT, DS: DGROUP, SS: DGROUP
Il SO effettua quindi le seguenti inizializzazioni:
CS = _TEXT, DS = ES = PSP, SS = STACK = DGROUP
In presenza del parametro FARSTACK, l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: _TEXT, DS: DGROUP, SS: STACK
Il SO effettua quindi le seguenti inizializzazioni:
CS = _TEXT, DS = ES = PSP, SS = STACK
Il programmatore ha quindi l'importante compito di inizializzare DS e, se necessario, anche ES; chiaramente, la cosa più ovvia da fare consiste nel porre:
DS = DGROUP
In Figura 29.11 vengono utilizzate le direttive avanzate per riscrivere il programma illustrato nella Figura 27.10 del Capitolo 27. Questa volta, la modalità di indirizzamento predefinita è NEAR per i dati che si trovano in DGROUP, FAR per i dati che si trovano nei blocchi FAR_DATA, FAR_BSS e NEAR per le procedure; se vogliamo definire procedure di tipo FAR, dobbiamo inserire il modificatore di modello FAR nel relativo prototipo.

Attraverso il listing file possiamo constatare la presenza dei due segmenti di dati FAR denominati DATASEGM2 e DATASEGM3; se non avessimo usato questi nomi personalizzati, avremmo ottenuto due blocchi denominati entrambi FAR_DATA (tenuti distinti a causa dell'attributo di combinazione PRIVATE).

Nel caso della procedura printStr1, i suoi due parametri di tipo WORD possono essere sostituiti da un unico parametro di tipo DWORD che rappresenta, ovviamente, un indirizzo FAR (con la componente Offset nella WORD meno significativa); nell'ipotesi che tale parametro venga chiamato strAddr, il codice per il caricamento dell'indirizzo della stringa in DS:DX diventa:
lds dx, strAddr
È necessario ribadire che le istruzioni come LDS, LES, LFS, etc, funzionano in modo corretto solo se l'operando sorgente (come strAddr) contiene una coppia Seg:Offset che rispetta le convenzioni Intel!

29.7.5 Modelli LARGE e HUGE

La struttura classica di un programma con modello di memoria LARGE prevede la presenza di due o più blocchi di codice, due o più blocchi di dati e un solo blocco stack; per rendere il programma più ordinato e leggibile, ci viene data la possibilità di distribuire le varie informazioni nei segmenti: name_TEXT, _DATA, CONST, _BSS, STACK, FAR_DATA e FAR_BSS.
In fase di assemblaggio, l'assembler provvederà a raggruppare le varie categorie di dati in un apposito blocco DGROUP avente le seguenti caratteristiche:
DGROUP GROUP _DATA, CONST, _BSS, STACK
Se vogliamo escludere il blocco STACK dal gruppo DGROUP, dobbiamo passare il parametro FARSTACK alla direttiva .MODEL.
In presenza del parametro NEARSTACK (o in assenza del parametro FARSTACK), l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: name_TEXT, DS: DGROUP, SS: DGROUP
Il SO effettua quindi le seguenti inizializzazioni:
CS = name_TEXT, DS = ES = PSP, SS = STACK = DGROUP
In presenza del parametro FARSTACK, l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: name_TEXT, DS: DGROUP, SS: STACK
Il SO effettua quindi le seguenti inizializzazioni:
CS = name_TEXT, DS = ES = PSP, SS = STACK
Il programmatore ha quindi l'importante compito di inizializzare DS e, se necessario, anche ES; chiaramente, la cosa più ovvia da fare consiste nel porre:
DS = DGROUP
In Figura 29.12 vengono utilizzate le direttive avanzate per riscrivere il programma illustrato nella Figura 27.12 del Capitolo 27. Questa volta, la modalità di indirizzamento predefinita è NEAR per i dati che si trovano in DGROUP, FAR per i dati che si trovano nei blocchi FAR_DATA, FAR_BSS e FAR per le procedure; se vogliamo definire procedure di tipo NEAR, dobbiamo inserire il modificatore di modello NEAR nel relativo prototipo (come accade per printStr1).

Attraverso il listing file possiamo constatare la presenza del gruppo DGROUP e di due ulteriori segmenti di dati FAR denominati DATASEGM2 e DATASEGM3; se non avessimo usato questi nomi personalizzati, avremmo ottenuto due blocchi denominati entrambi FAR_DATA (tenuti distinti a causa dell'attributo di combinazione PRIVATE).
Il segmento di codice principale viene chiamato automaticamente elarge_TEXT (in quanto stiamo lavorando con un modello di memoria che prevede la presenza di due o più blocchi di codice); per gli altri due segmenti di codice vengono utilizzati i nomi che noi abbiamo imposto e cioè: CODESEGM2 e CODESEGM3.

Il modello HUGE, come sappiamo, è del tutto simile al modello LARGE; i compilatori che supportano il modello HUGE lo utilizzano per permettere al programmatore di definire singoli dati che superano la dimensione di 64 KiB.
Naturalmente, il compilatore gestisce questa situazione attraverso apposite procedure capaci di manipolare i dati huge; in Assembly, tali procedure devono essere scritte dal programmatore.

29.7.6 Modello FLAT

Il modello FLAT è stato espressamente concepito per la scrittura di programmi destinati a girare sotto un SO a 32 bit; l'utilizzo di tale modello di memoria permette quindi di sfruttare direttamente gli indirizzamenti caratterizzati da una componente Offset a 32 bit!

Il modello FLAT è formalmente identico al modello TINY; tutte le informazioni possono essere quindi distribuite nei segmenti: _TEXT, _DATA, CONST, _BSS e STACK. La differenza sostanziale sta nel fatto che, all'interno di questi segmenti, gli spiazzamenti sono sempre a 32 bit!

Analizziamo ora le convenzioni relative alla segmentazione standard nel modello FLAT; si noti, in particolare, l'attributo USE32 (offset a 32 bit) e l'attributo di allineamento DWORD (ottimale per le CPU a 32 bit).

Per i dati statici inizializzati viene riservato un segmento di programma avente le seguenti caratteristiche: Questo segmento di programma può essere ottenuto attraverso la direttiva semplificata:
.DATA
Per i dati statici non inizializzati viene riservato un segmento di programma avente le seguenti caratteristiche: Questo segmento di programma può essere ottenuto attraverso la direttiva semplificata:
.DATA?
Per i dati statici costanti viene riservato un segmento di programma avente le seguenti caratteristiche: Questo segmento di programma può essere ottenuto attraverso la direttiva semplificata:
.CONST
In analogia al modello di memoria TINY, anche nel modello FLAT il compito di creare il blocco stack spetta al SO; nel caso in cui si abbia la necessità di definire direttamente tale blocco, viene riservato un segmento di programma avente le seguenti caratteristiche: Questo segmento di programma può essere ottenuto attraverso la direttiva semplificata:
.STACK [size]
Per il codice viene riservato un segmento di programma avente le seguenti caratteristiche: Questo segmento di programma può essere ottenuto attraverso la direttiva semplificata:
.CODE
L'assembler provvede a raggruppare tutte queste informazioni in un apposito blocco DGROUP avente le seguenti caratteristiche:
DGROUP GROUP _TEXT, _DATA, CONST, _BSS, STACK
Successivamente, l'assembler genera automaticamente la seguente direttiva ASSUME:
ASSUME CS: DGROUP, DS: DGROUP, SS: DGROUP
Il SO provvede all'inizializzazione di tutti i registri di segmento ponendo:
CS = DGROUP, DS = DGROUP, ES = DGROUP, SS = DGROUP
I segmenti di dati FAR_DATA e FAR_BSS e i segmenti di codice name_TEXT perdono significato; non bisogna dimenticare, infatti, che nel modello FLAT ogni programma viene inserito in un segmento virtuale da 4 GiB (flat segment) all'interno del quale vengono sistemati i dati, il codice e lo stack.
Per muoverci all'interno di un flat segment utilizziamo indirizzi NEAR rappresentati da offset a 32 bit che possono assumere tutti i valori compresi tra 00000000h e FFFFFFFFh; in questo modo è possibile esplorare tutto il segmento virtuale da 4 GiB. Per maggiori dettagli sul modello FLAT si può fare riferimento alle apposite sezioni di questo sito.

29.8 Programmi Assembly distribuiti su due o più file

In relazione all'uso delle caratteristiche avanzate nei programmi Assembly distribuiti su due o più file, valgono tutte le considerazioni già esposte nel Capitolo 28; è possibile quindi impiegare le direttive PUBLIC e EXTRN secondo le regole che già conosciamo.

In precedenza è stato anche sottolineato il fatto che, quando si usano le caratteristiche avanzate, la direttiva EXTRN per le procedure può essere sostituita efficacemente con la direttiva PROTO (cioè, con il prototipo di procedura); in effetti, questo metodo è caldamente consigliato.
Ad esempio, un modulo Assembly che ha bisogno di una procedura esterna avente le seguenti caratteristiche:
printStr1 PROTO FAR :WORD, :WORD
può servirsi proprio di tale prototipo per dare all'assembler tutte le necessarie informazioni; appare evidente, infatti, che i dettagli forniti da PROTO sono ben più consistenti rispetto a ciò che si può ricavare da una semplice direttiva:
EXTRN printStr1

29.9 Le istruzioni ENTER e LEAVE

Analizzando alcuni listati Assembly, ci si può imbattere in due particolari istruzioni, ENTER e LEAVE, che ancora non conosciamo; si tratta di due istruzioni concepite espressamente per automatizzare la generazione del prolog code e dell'epilog code di una procedura!

Nel caso generale di una procedura dotata di parametri e di n byte di variabili locali, il codice di "ingresso" è costituito dalle seguenti istruzioni: Come già sappiamo, grazie a queste istruzioni possiamo trovare i parametri della procedura a spiazzamenti positivi rispetto a BP e le variabili locali a spiazzamenti negativi rispetto a BP; la fase appena descritta rappresenta la creazione del cosiddetto stack frame della procedura.

Al termine della stessa procedura, il codice di "uscita" è costituito dalle seguenti istruzioni: Lo scopo di queste istruzioni è quello di ripristinare SP e BP prima di restituire il controllo al caller; la fase appena descritta rappresenta la distruzione dello stack frame della procedura e comprende anche la rimozione dei parametri dallo stack (che può competere al caller o alla procedura stessa).

Nel caso in cui si stia utilizzando una CPU 80186 o superiore, è possibile sostituire il codice di "ingresso" con l'istruzione ENTER e il codice di "uscita" con l'istruzione LEAVE; queste due istruzioni, come detto, sono disponibili solo con le CPU 80186 e superiori ed hanno proprio lo scopo di rendere automatica la generazione del codice relativo allo stack frame di una procedura dotata di parametri e variabili locali.

L'istruzione ENTER ha il codice macchina C8h e richiede due operandi; il primo operando è di tipo Imm16 mentre il secondo è di tipo Imm8.
Il primo operando indica il numero di byte da sottrarre a SP/ESP per l'allocazione nello stack della memoria da destinare alle variabili locali; il secondo operando è un numero compreso tra 0 e 31, che indica il "livello di innesto", cioè l'eventuale presenza di stack frame precedenti da innestare nel nuovo stack frame creato da ENTER. Attraverso questo meccanismo, i linguaggi come il Pascal permettono la definizione di procedure all'interno di altre procedure (procedure locali); per i linguaggi che non supportano la creazione di procedure locali, il secondo parametro vale 0.

Applicando questi concetti alla procedura TestProc di Figura 29.4 (6 byte di variabili locali), tutto il codice di "ingresso" viene sostituito dall'istruzione:
ENTER 6, 0
Quando la CPU incontra questa istruzione, salva BP nello stack, copia SP in BP e sottrae 6 byte a SP; in modalità protetta a 32 bit la CPU lavora sui registri ESP, EBP

L'istruzione LEAVE ha il codice macchina C9h e non richiede alcun operando; il compito di questa istruzione è quello di ripristinare i registri BP/EBP, SP/ESP precedentemente modificati da ENTER.
Nel caso della procedura TestProc di Figura 29.4, tutto il codice di "uscita" viene sostituito dall'istruzione:
LEAVE
Quando la CPU incontra questa istruzione, copia BP in SP e ripristina BP; in modalità protetta a 32 bit la CPU lavora sui registri ESP, EBP.

Come sappiamo, se si utilizzano le caratteristiche avanzate di MASM, tutto il codice necessario per il prolog code e l'epilog code viene generato automaticamente dall'assembler; in tal caso, lo stesso assembler può anche decidere di impiegare ENTER e LEAVE.

29.10 Considerazioni finali

Appare evidente che le convenzioni illustrate in questo capitolo, hanno principalmente lo scopo di semplificare il lavoro di interfacciamento tra l'Assembly e i linguaggi di alto livello; naturalmente, nessuno ci impedisce di sviluppare programmi interamente scritti in Assembly, che fanno uso delle convenzioni adottate dai compilatori.
I puristi dell'Assembly ritengono, però, che non avrebbe molto senso utilizzare in questo modo un linguaggio di basso livello; in effetti, se si ha bisogno di tutte queste caratteristiche avanzate, si fa molto prima a passare ad un linguaggio di alto livello.
Si tenga anche presente che quando si deve scrivere un modulo Assembly particolarmente complesso, le varie convenzioni illustrate in questo capitolo possono rivelarsi del tutto inadeguate; in una situazione del genere gli stessi manuali degli assembler come NASM e MASM, consigliano, in particolare, di ricorrere alla sintassi classica per i segmenti di programma.
In ogni caso, tutto dipende dai gusti personali; l'importante è capire che per poter utilizzare tutte queste caratteristiche avanzate è necessaria una conoscenza piuttosto solida del linguaggio Assembly e delle convenzioni illustrate in questo capitolo.