Assembly Base con NASM

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

Le versioni più recenti degli assembler come MASM, offrono 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 è 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.

Le caratteristiche avanzate possono essere sfruttate anche per rendere più semplice lo sviluppo di programmi interamente scritti in Assembly; in generale, però, bisogna osservare che queste "innovazioni" non suscitano particolare entusiasmo tra i puristi del linguaggio Assembly.
Proprio per questo motivo, gli sviluppatori del progetto NASM hanno deciso di non fornire alcun supporto per le caratteristiche avanzate disponibili con MASM; tutto il lavoro necessario per l'interfacciamento con i linguaggi di alto livello, viene lasciato nelle mani del programmatore!
Del resto, come abbiamo visto nei precedenti capitoli, gli strumenti avanzati forniti dagli assembler "commerciali" sono implementati attraverso le macro; lo scopo di questo capitolo è proprio quello di dimostrare come sia possibile "potenziare" NASM mediante la creazione di apposite macro.

29.1 I modelli di memoria

I compilatori dei linguaggi di alto livello, ricorrono spesso ai cosiddetti modelli di memoria; nel Capitolo 27 abbiamo visto che i principali modelli di memoria sono individuati, per convenzione, dalle denominazioni TINY, SMALL, MEDIUM, COMPACT e LARGE.
In base al modello di memoria selezionato, i compilatori assumono un comportamento predefinito per l'indirizzamento (NEAR o FAR) dei dati e delle procedure; la Figura 29.1 illustra tutti i dettagli su questo aspetto. 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.1 Indirizzamento predefinito nel modello 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.2 Indirizzamento predefinito nel modello 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.3 Indirizzamento predefinito nel modello 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.4 Indirizzamento predefinito nel modello 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.5 Indirizzamento predefinito nel modello 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.6 Indirizzamento predefinito nel modello 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.7 Indirizzamento predefinito nel modello FLAT

Il modello FLAT viene supportato da NASM 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.8 Memory model override

Molti linguaggi di programmazione evoluti, permettono al programmatore di aggirare le convenzioni di Figura 29.1; ad esempio, in un modulo C con modello SMALL, è possibile creare procedure del tipo:
void far func1(short int, char far *);
Questo prototipo afferma che func1 è una funzione di tipo FAR che richiede un parametro di tipo short int (intero con segno a 16 bit) e un altro di tipo "indirizzo FAR di un char" (cioè, indirizzo Seg:Offset di un intero con segno a 8 bit).
All'interno dei moduli C tutti questi aspetti vengono gestiti dal compilatore; all'interno dei moduli Assembly, invece, è compito del programmatore gestire il corretto indirizzamento dei dati e delle procedure.
Se vogliamo allora accedere a func1 da un modulo Assembly, dobbiamo attenerci rigorosamente al prototipo di tale procedura; supponendo, ad esempio, di aver definito i due dati varInt (intero con segno a 16 bit) e varChar (intero con segno a 8 bit), la chiamata Assembly di func1, in stile C, assume il seguente aspetto: Il passaggio dell'argomento "puntatore FAR a varChar" comporta l'inserimento nello stack dell'indirizzo completo Seg:Offset dello stesso varChar; si osservi che la componente Seg, come al solito, viene inserita nello stack prima della componente Offset.

È superfluo sottolineare che se non si rispettano tutte queste regole, si ottiene un programma che, in fase di esecuzione, va sicuramente in crash!

29.1.9 Convenzioni per il prolog code e l'epilog code

I vari linguaggi di programmazione, utilizzano una serie di convenzioni relative al passaggio degli argomenti alle procedure, alla pulizia dello stack e al modo di restituire il valore di ritorno; molti di questi aspetti sono stati illustrati nei capitoli precedenti e vengono riassunti dalla Figura 29.2. Come si può notare, la chiamata predefinita di una procedura BASIC, FORTRAN o Pascal, comporta l'inserimento degli argomenti nello stack a partire da quello più a sinistra; il compito di ripulire lo stack dagli argomenti ricade sulla procedura stessa (callee).

La chiamata predefinita di una procedura C, C++ o Prolog, comporta l'inserimento degli argomenti nello stack a partire da quello più a destra; il compito di ripulire lo stack dagli argomenti ricade sul caller.

STDCALL indica, non un linguaggio, bensì una convenzione largamente utilizzata in ambiente Windows a 32 bit; si tratta di 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 (callee); 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.

Per quanto riguarda, infine, le convenzioni per la restituzione dei valori di ritorno da parte delle procedure, vale tutto ciò che è stato illustrato nella Figura 25.10 del Capitolo 25.

29.1.10 Prolog code e epilog code override

Come accade per i modelli di memoria, molti linguaggi di programmazione permettono di aggirare anche le convenzioni di Figura 29.2 sul prolog code e l'epilog code; ad esempio, in un modulo C è possibile creare funzioni del tipo:
short int far pascal func2(short int, short int);
Questo prototipo afferma che func2 è una procedura di tipo FAR che richiede due parametri di tipo short int e restituisce uno short int; inoltre, tale procedura segue le convenzioni Pascal per il prolog code e l'epilog code.
Se vogliamo chiamare allora func2 da un modulo Assembly, con i due argomenti varInt1 e varInt2, dobbiamo scrivere: Come si può notare, gli argomenti vengono inseriti nello stack a partire dal primo; inoltre, il compito di ripulire lo stack dagli argomenti viene lasciato alla procedura chiamata (callee).
Si noti la chiamata di FUNC2 con la procedura scritta in lettere maiuscole; ciò accade in quanto il Pascal è case-insensitive e non distingue quindi tra maiuscole e minuscole (maggiori dettagli nel Capitolo 31).

29.1.11 Le opzioni NEARSTACK e FARSTACK

Come è stato spiegato in precedenza, 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; i compilatori stessi permettono al programmatore di intervenire su questo aspetto attraverso apposite opzioni di configurazione denominate NEARSTACK e FARSTACK.
Quando si abilita l'opzione NEARSTACK, il blocco stack viene inserito in DGROUP (SS=DS); abilitando, invece, l'opzione FARSTACK, il blocco stack viene separato da DGROUP (SS diverso da DS).

Ovviamente, un programmatore che intende interfacciare un modulo Assembly a dei moduli scritti con i linguaggi di alto livello, deve tenere conto anche di tale aspetto; si tenga presente, comunque, che tutti i dettagli relativi alla creazione e gestione dello stack spettano al compilatore.
Più avanti verranno illustrati degli esempi pratici.

29.2 Sintassi avanzata per le procedure

Analizziamo ora un esempio pratico che ci permette di illustrare alcune macro per la creazione delle procedure attraverso una sintassi più evoluta; 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.

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.
In ogni caso, appare evidente il fatto che in base a quanto è stato esposto in precedenza, ogni linguaggio di programmazione utilizza apposite convenzioni per la gestione del prolog code e dell'epilog code; ciò suggerisce l'idea di creare apposite macro capaci di "applicare" automaticamente tali convenzioni!
Analizziamo allora gli strumenti che NASM ci mette a disposizione per risolvere il nostro problema; in particolare, fissiamo la nostra attenzione su ciò che è stato esposto nel Capitolo 24.

29.2.1 Macro per il prolog code e l'epilog code di una procedura

Osserviamo subito che una procedura C, CPP, Prolog, riceve gli argomenti a partire da quello più a destra (ultimo argomento); viceversa, una procedura Pascal, BASIC, FORTRAN, riceve gli argomenti a partire da quello più a sinistra (primo argomento).
Convenzionalmente, l'accesso a tali argomenti avviene tramite BP; a tale proposito, il contenuto originale di BP viene salvato nello stack e il contenuto di SP viene copiato nello stesso BP.
Una volta compiuti questi passi, gli eventuali argomenti risultano accessibili a partire da BP+4 per le procedure NEAR e BP+6 per le procedure FAR; analogamente, le eventuali variabili locali risultano accessibili a spiazzamenti negativi rispetto a BP.
Appare evidente che i procedimenti appena descritti sono basati sul rigoroso rispetto di precise convenzioni; di conseguenza, possiamo facilmente implementare tali convenzioni attraverso apposite macro!

Partiamo allora con la creazione di una serie di costanti globali il cui scopo è quello di codificare le convenzioni illustrate nelle Figure 29.1 e 29.2; possiamo scrivere, ad esempio: Il programmatore deve utilizzare queste costanti per indicare il tipo NEAR o FAR della procedura (parametro proc_type) e le convenzioni di linguaggio (parametro language); come si può notare, le convenzioni di linguaggio possono essere ricondotte a due grandi categorie a cui si aggiunge il caso particolare della convenzione STDCALL).

A questo punto creiamo una prima macro (procedure) il cui compito è quello di aprire la definizione di una procedura; all'interno di tale macro viene anche creato un nuovo context stack destinato a contenere nomi simbolici da condividere con altre macro.
La macro procedure assume il seguente aspetto: La macro procedure è dotata di 3 parametri che rappresentano: le convenzioni di linguaggio, il tipo di procedura (NEAR o FAR) e il nome della procedura; naturalmente, il programmatore deve specificare i primi due parametri attraverso le costanti simboliche create in precedenza.
Si può notare che viene creato un identificatore condiviso %$PROC_LANG che memorizza il primo parametro (%1); invece, l'identificatore condiviso %$STACK_BASE memorizza il secondo parametro (%2) che rappresenta lo spiazzamento iniziale degli argomenti rispetto a BP (si osservi, infatti, che NEARPROC=4 e FARPROC=6).
La terza costante condivisa %$ARGS_SIZE viene inizializzata a 0 e rappresenta la dimensione totale in byte di tutti gli argomenti richiesti dalla procedura; questa costante deve essere posizionata (e inizializzata a 0) nella macro procedure per gestire il caso delle procedure che non richiedono alcun argomento!
Nella parte finale di procedure notiamo la presenza di due istruzioni che già conosciamo; si tratta, infatti, delle istruzioni per l'inizializzazione del registro BP.

Per aprire, ad esempio, la definizione di una procedura FAR Pascal di nome provaProc1, possiamo scrivere:
procedure LANGPAS, FARPROC, provaProc1
Questa macro viene espansa in: Per la creazione dei nomi simbolici degli eventuali argomenti, scriviamo una nuova macro di nome arguments; tale macro assume il seguente aspetto: Come si può notare, ciascun argomento è formato da una coppia che comprende il nome simbolico e la dimensione in byte; possiamo affermare quindi che il numero totale di argomenti di cui la procedura ha bisogno è pari a ARGS_NUMB=%0/2.
All'interno della macro viene eseguito un loop con %REP per un numero di volte pari proprio a ARGS_NUMB; attraverso la direttiva %ROTATE vegnono definiti i nomi simbolici di ciascun parametro e la relativa posizione nello stack.
Si osservi che per tali definizioni viene utilizzata la direttiva %XDEFINE; se si impiegasse %DEFINE, al nome simbolico ARGS_BASE verrebbe assegnato sempre l'ultimo valore derivante dall'espansione di tutte le macro!
Un'altra caratteristica interessante della macro arguments è rappresentata dalla presenza delle direttive condizionali il cui scopo è quello di gestire le due principali convenzioni (C e Pascal) per la disposizione degli argomenti nello stack; come sappiamo, nella convenzione C la disposizione segue un ordine inverso rispetto al caso della convenzione Pascal.
Dopo il loop, viene creata una costante condivisa %$ARGS_SIZE che contiene la dimensione totale in byte di tutti gli argomenti ricevuti dalla procedura; tale costante sarà poi utilizzata per ripulire gli argomenti dallo stack (ricordiamo che per le procedure che non richiedono argomenti, si ha %$ARGS_SIZE=0).

Vediamo ora quello che succede se, nell'area di un programma destinata alla definizione delle procedure, scriviamo: La chiamata di queste macro viene espansa in: Come si può notare, abbiamo creato tutto il codice standard iniziale di una procedura C di tipo FAR, dotata di due argomenti di tipo WORD; nel corpo della procedura possiamo così accedere ai vari argomenti attraverso i nomi simbolici num1 e num2!

Per la creazione dei nomi simbolici delle eventuali variabili locali, scriviamo una nuova macro di nome localvars; tale macro assume il seguente aspetto: Si tratta di una macro del tutto simile ad arguments, che riceve gli argomenti in coppie (nome simbolico, dimensione in byte); ovviamente, questa volta i vari spiazzamenti vengono sottratti da BP!
Al termine del loop, la costante simbolica LOCV_BASE contiene la dimensione totale in byte di tutte le variabili locali create nello stack; tale costante viene poi sottratta da SP per creare fisicamente, nello stack, lo spazio necessario.

Vediamo ora quello che succede se, nell'area di un programma destinata alla definizione delle procedure, scriviamo: La chiamata di queste macro viene espansa in: Come si può notare, abbiamo creato tutto il codice standard iniziale di una procedura Pascal di tipo NEAR dotata di due argomenti (uno WORD e uno DWORD) e di due variabili locali di tipo WORD; nel corpo della procedura possiamo così accedere ai vari argomenti attraverso i nomi simbolici Integer1 e Longint1, e alle varie variabili locali attraverso i nomi simbolici LocalInt1 e LocalInt2!

Una procedura che segue le convenzioni C/Pascal termina con una istruzione che copia il contenuto di BP in SP per la rimozione dallo stack di eventuali variabili locali; questa istruzione può essere usata senza problemi anche in assenza di tali variabili.
Subito dopo è presente una istruzione PUSH BP (che ripristina il contenuto originale di BP) seguita da una istruzione per la restituzione del controllo al caller; tale istruzione è RET (o RETN) per le procedure NEAR e RETF per le procedure FAR.
La rimozione degli argomenti dallo stack spetta al caller nel caso della convenzione C e al callee nel caso delle convenzioni Pascal e STDCALL; possiamo creare allora la seguente macro per la chiusura della definizione di una procedura: Come si può notare, viene impiegata la costante condivisa %$STACK_BASE per stabilire il tipo di ritorno NEAR o FAR; invece, la costante condivisa %$PROC_LANG viene impiegata per stabilire a chi spetta la rimozione degli argomenti dallo stack. Se tale compito spetta al callee, allora il valore da sommare a SP è contenuto nella costante condivisa %$ARGS_SIZE (la quale viene impiegata come operando di RETN/RETF); se, invece, tale compito spetta al caller, allora l'aggiornamento di SP deve essere effettuato manualmente!
Si noti anche l'altro importante compito svolto da endprocedure, che consiste nel rimuovere il context stack precedentemente creato da procedure.

Nel caso, ad esempio, della precedente procedura provaProc3, la macro endprocedure viene espansa in: Da tutte le considerazioni appena esposte, risulta evidente che per la definizione di una procedura, l'ordine di chiamata delle macro deve essere, rigorosamente, procedure, arguments, localvars, endprocedure; ovviamente, procedure e endprocedure sono obbligatorie, mentre arguments e localvars sono opzionali!

29.2.2 Macro per la chiamata di una procedura

Il potentissimo preprocessore di NASM può essere utilizzato anche per rendere più evoluta la chiamata delle procedure; in pratica, possiamo scrivere una macro che ci permette di chiamare le procedure secondo una sintassi molto simile a quella dei linguaggi di alto livello.
Bisogna ricordare che una procedura C riceve gli argomenti a partire da quello più a destra, mentre una procedura Pascal riceve gli argomenti a partire da quello più a sinistra; possiamo scrivere allora la seguente macro: Come si può notare, il meccanismo è sempre lo stesso utilizzato per le macro arguments e localvars; osserviamo che nel caso delle procedure C, %ROTATE riceve il valore -1 in modo che i vari argomenti, ruotando verso destra, vengano inseriti nello stack a partire dall'ultimo!

Nel caso, ad esempio, della precedente procedura provaProc3, supponendo di aver definito una variabile varWord1 di tipo WORD e una variabile vardDword1 di tipo DWORD, possiamo effettuare chiamate del tipo:
callproc LANGPAS, NEARPROC, provaProc3, word [varWord1], dword [varDword1]
Questa macro viene espansa in:

29.2.3 Libreria di macro per le procedure C e Pascal

A questo punto, possiamo facilmente crearci una libreria di macro destinata alla gestione semplificata delle procedure C e Pascal; come abbiamo visto all'inizio del capitolo, molti altri linguaggi utilizzano queste stesse convenzioni.
Tradizionalmente, le librerie di macro vengono disposte in appositi file che recano l'estensione .MAC; nel nostro caso, otteniamo la libreria LIBPROC.MAC visibile in Figura 29.6. Se un programma Assembly ha bisogno di questa libreria, non deve fare altro che specificare la direttiva:
%include "libproc.mac"
Un aspetto importante della libreria di Figura 29.6, è dato dal fatto che i nomi simbolici utilizzati per i parametri e le variabili locali, non possono essere ridefiniti; in sostanza, se una procedura utilizza un parametro chiamato arg1, allora tale nome non può essere utilizzato in un'altra procedura!
Se tutto ciò ci crea dei problemi, possiamo facilmente risolvere la situazione approfittando del fatto che tra le macro procedure e endprocedure, risulta attivo un context stack; di conseguenza, non dobbiamo fare altro che anteporre il simbolo %$ a tutti i nomi simbolici che vogliamo ridefinire!
Possiamo scrivere, ad esempio: In questo modo, in un'altra procedura possiamo riutilizzare i nomi %$Integer1, %$LocalInt1, etc.

29.2.4 Riscrittura di TestProc con una sintassi più avanzata

Torniamo ora alla procedura TestProc di Figura 29.4 e riscriviamola con l'ausilio della libreria LIBPROC.MAC di Figura 29.6; tenendo conto del prototipo C di tale procedura
int far TestProc(long param1, int param2, int param3);
otteniamo il risultato visibile in Figura 29.7. Come si può notare, le quattro macro che abbiamo creato in precedenza, permettono di automatizzare tutta la fase relativa al prolog code e all'epilog code, riducendo praticamente a zero la possibilità di commettere errori; il programmatore può così concentrarsi sul solo corpo della procedura.

A questo punto, supponendo di aver creato le variabili varDword (di tipo DWORD), varWord1 e varWord2 (di tipo WORD), possiamo effettuare chiamate del tipo:
callproc LANGC, PROCFAR, TestProc, dword [varDword], word [varWord1], word [varWord2]
Al termine della chiamata, il valore di ritorno è disponibile nel registro AX.

29.3 Indirizzamento delle variabili locali di una procedura

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; abbiamo visto, infatti, che non avrebbe senso trattare una variabile locale (dinamica) definita nello stack, come se fosse una variabile statica definita in un segmento di programma.
Se, ad esempio, varWord1 è una variabile statica, definita quindi in un segmento di programma, allora una istruzione del tipo:
mov bx, varWord1
trasferisce in BX l'offset di varWord1, mentre una istruzione del tipo:
mov bx, [varWord1]
trasferisce in BX il contenuto a 16 bit della locazione di memoria che si trova all'indirizzo DS:varWord1.

Tenendo conto del significato di locVar2 (variabile locale a 16 bit della procedura TestProc di Figura 29.7), una istruzione del tipo:
mov bx, locVar2
viene espansa in:
mov bx, [bp-6]
Questa istruzione carica in BX il contenuto di locVar2 e cioè, la WORD memorizzata all'indirizzo SS:(BP-6)!

In pratica, il calcolo "tradizionale" dell'offset funziona solo in relazione ad identificatori definiti 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.

Il metodo vivamente raccomandato per risalire all'offset di una variabile locale, consiste (come già sappiamo) nel ricorso all'istruzione LEA (Load Effective Address); nel caso, ad esempio, di locVar2, possiamo scrivere:
lea bx, locVar2
L'assembler espande la macro locVar2 e ottiene l'istruzione:
lea bx, [bp-6]
Questa istruzione calcola BP-6 (senza alterare BP) e carica il risultato in BX; ovviamente, tale risultato è proprio l'offset di locVar2 all'interno del blocco stack (referenziato da SS).

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

Il problema appena esposto si presenta anche quando, dall'interno di una procedura A, dobbiamo chiamare una procedura B la quale richiede come argomento l'offset di un parametro o di una variabile locale della stessa procedura A; anche in un caso del genere, conviene servirsi dell'istruzione LEA.
Vediamo un esempio pratico volutamente contorto. Supponiamo di avere una procedura ProcA la quale passa ad una procedura ProcB gli offset di tre sue variabili locali di tipo DWORD; la procedura ProcB restituisce in EAX la somma dei contenuti delle tre variabili locali ricevute per indirizzo.
La struttura di ProcA è la seguente: Quindi, ProcA crea tre variabili locali loc1A, loc2A, loc3A e le inizializza con tre DWORD; poi carica i rispettivi offset nei tre registri AX, BX, CX e li passa a ProcB.
Il compito di ProcB è quello di sommare le tre DWORD ricevute per indirizzo, restituendo poi il risultato a ProcA attraverso EAX; la struttura di ProcB è la seguente: Come è stato spiegato in precedenza, la struttura di queste procedure di esempio è volutamente contorta; osserviamo, infatti, che il numero di istruzioni usate è notevolmente superiore al necessario.
ProcB carica in BX ciascuno degli offset ricevuti come argomento e può così accedere per indirizzo alle variabili locali di ProcA; tali variabili esistono anche quando il controllo passa a ProcB grazie al fatto che ci troviamo in presenza di una chiamata innestata (in sostanza, le variabili locali di ProcA saranno distrutte solo dopo che ProcB avrà restituito il controllo)!
Ricordando che gli offset ricevuti da ProcB sono calcolati rispetto a SS, dobbiamo ricordarci di ricorrere, se necessario, al segment override; questo è proprio il caso del nostro esempio in quanto il puntatore BX non viene associato automaticamente a SS.

29.4 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 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.4.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:
SEGMENT _DATA ALIGN=2 PUBLIC USE16 CLASS=DATA
Siccome il blocco _DATA è dotato di nome e attributi standard, per poterlo definire possiamo servirci di una apposita macro del tipo:
%define STD_DATA SEGMENT _DATA ALIGN=2 PUBLIC USE16 CLASS=DATA
Quando l'assembler incontra questa macro, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla macro STD_DATA è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

29.4.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 resw 1
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:
SEGMENT _BSS ALIGN=2 PUBLIC USE16 CLASS=BSS
Siccome il blocco _BSS è dotato di nome e attributi standard, per poterlo definire possiamo servirci di una apposita macro del tipo:
%define STD_BSS SEGMENT _BSS ALIGN=2 PUBLIC USE16 CLASS=BSS
Quando l'assembler incontra questa macro, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla macro STD_BSS è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

29.4.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:
SEGMENT _CONST ALIGN=2 PUBLIC USE16 CLASS=CONST
Siccome il blocco _CONST è dotato di nome e attributi standard, per poterlo definire possiamo servirci di una apposita macro del tipo:
%define STD_CONST SEGMENT _CONST ALIGN=2 PUBLIC USE16 CLASS=CONST
Quando l'assembler incontra questa macro, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla macro STD_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.4.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.4.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:
SEGMENT _STACK ALIGN=16 STACK USE16 CLASS=STACK
Siccome il blocco _STACK è dotato di nome e attributi standard, per poterlo definire possiamo servirci di una apposita macro del tipo:
%define STD_STACK SEGMENT _STACK ALIGN=16 STACK USE16 CLASS=STACK
Quando l'assembler incontra questa macro, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla macro STD_STACK è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

29.4.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:
GROUP DGROUP _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).

29.4.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:
SEGMENT FAR_DATA ALIGN=16 PRIVATE USE16 CLASS=FAR_DATA
Eventualmente, il programmatore può assegnare anche un nome differente al segmento di programma (è proibito utilizzare nomi riservati come _DATA o _BSS); siccome il blocco FAR_DATA è dotato di attributi standard, per poterlo definire possiamo servirci di una apposita macro del tipo: Quando l'assembler incontra questa macro, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla macro STD_FAR_DATA è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

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.4.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:
SEGMENT FAR_BSS ALIGN=16 PRIVATE USE16 CLASS=FAR_BSS
Eventualmente, il programmatore può assegnare anche un nome differente al segmento di programma (è proibito utilizzare nomi riservati come _DATA o _BSS); siccome il blocco FAR_BSS è dotato di attributi standard, per poterlo definire possiamo servirci di una apposita macro del tipo: Quando l'assembler incontra questa macro, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla macro STD_FAR_BSS è rappresentata dall'inizio del blocco successivo o dalla fine del modulo.

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.4.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:
SEGMENT _TEXT ALIGN=2 PUBLIC USE16 CLASS=CODE
Siccome il blocco _TEXT è dotato di nome e attributi standard, per poterlo definire possiamo servirci di una apposita macro del tipo:
%define STD_CODE SEGMENT _TEXT ALIGN=2 PUBLIC USE16 CLASS=CODE
Quando l'assembler incontra questa macro, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla macro STD_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.4.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:
SEGMENT name_TEXT ALIGN=2 PUBLIC USE16 CLASS=CODE
Siccome il blocco name_TEXT è dotato di nome e attributi standard, per poterlo definire possiamo servirci di una apposita macro del tipo:
%define STD_name_CODE(name) SEGMENT name %+ _TEXT ALIGN=2 PUBLIC USE16 CLASS=CODE
Combinando questa macro con quella precedente, otteniamo la seguente macro destinata a creare qualsiasi blocco _TEXT o name_TEXT: Quando l'assembler incontra questa macro, la espande e ottiene un segmento di programma avente il nome e gli attributi descritti in precedenza; la fine di un blocco aperto dalla macro STD_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; per convenzione, i compilatori utilizzano il nome del modulo in cui si trova il blocco codice, seguito dalla stringa _TEXT. Ad esempio, all'interno di un modulo C chiamato LIB1.C, il compilatore può creare un segmento di codice denominato LIB1_TEXT.

29.4.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 esposte in precedenza.
Può anche capitare di imbattersi in qualche compilatore che, per i segmenti di programma, utilizza nomi leggermente differenti rispetto a quelli standard (ad esempio, CONST invece di _CONST); nei capitoli successivi vedremo come comportarci in casi del genere.

Tutte le macro per la segmentazione standard appena illustrate, possono essere inserite in un apposito file chiamato, ad esempio, STDSEGM.MAC; la struttura di tale file è visibile nella Figura 29.8.

29.5 Esempi pratici

Per analizzare in pratica i concetti appena esposti, proviamo a riscrivere gli esempi presentati nel Capitolo 27; più avanti verranno illustrati i dettagli relativi ai programmi Assembly distribuiti su più moduli.
Lo scopo degli esempi che seguono è quello di simulare con l'Assembly, la struttura interna che i compilatori assegnano ai programmi scritti con i linguaggi di alto livello; attraverso il map file, il programmatore può analizzare il tipo e la disposizione dei segmenti di programma creati dall'assembler.

29.5.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:
GROUP DGROUP _TEXT _DATA _CONST _BSS
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.9 utilizziamo i concetti appena esposti per riscrivere il programma illustrato nella Figura 27.4 del Capitolo 27. La disposizione dei segmenti illustrata in Figura 29.9 è estremamente importante; se non si rispetta quest'ordine, non risulta possibile la generazione di un eseguibile in formato COM!
Il blocco _TEXT deve essere il primo in assoluto in quanto contiene l'entry point che, come sappiamo, deve trovarsi rigorosamente all'offset 0100h; inoltre, i 0100h byte che precedono l'entry point devono essere rigorosamente non inizializzati!

In un eseguibile in formato COM deve essere presente un unico segmento di programma; proprio per questo motivo, la direttiva GROUP raggruppa tutti i quattro segmenti di programma presenti.
I blocchi di dati inizializzati (_DATA e _CONST) devono precedere i blocchi di dati non inizializzati (_BSS e _STACK); bisogna ribadire che in un programma in formato COM il compito di creare il blocco stack spetta al SO. Disponendo i blocchi di dati non inizializzati proprio alla fine del programma, facciamo in modo che la memoria ad essi necessaria venga assegnata dal SO nel momento in cui inizia la fase di esecuzione; a tale proposito, si veda il significato del campo MinimumAllocation nella Figura 13.8 del Capitolo 13.

Rispettando le convenzioni per il modello di memoria TINY, nell'esempio di Figura 29.9 utilizziamo solo dati e procedure di tipo NEAR; alcuni linker si rifiutano di generare eseguibili in formato COM contenenti qualsiasi tipo di salto FAR (comprese le FAR call)!

29.5.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.
Il compilatore provvede poi ad inserire tutti i blocchi di dati NEAR in un gruppo DGROUP; se abilitiamo l'opzione NEARSTACK, il compilatore inserisce anche il blocco _STACK in DGROUP attraverso la direttiva:
GROUP DGROUP _DATA _CONST _BSS _STACK
Subito dopo l'entry point il compilatore inserisce tutte le istruzioni necessarie affinché, in fase di esecuzione, risulti:
CS = _TEXT, DS = SS = DGROUP, ES = PSP
Se abilitiamo l'opzione FARSTACK, allora il compilatore esclude il blocco _STACK da DGROUP attraverso la direttiva:
GROUP DGROUP _DATA _CONST _BSS
Subito dopo l'entry point il compilatore inserisce tutte le istruzioni necessarie affinché, in fase di esecuzione, risulti:
CS = _TEXT, DS = DGROUP, ES = PSP, SS = _STACK
In Figura 29.10 utilizziamo i concetti appena esposti per riscrivere il programma illustrato nella Figura 27.6 del Capitolo 27. Nell'esempio di Figura 29.10 stiamo simulando l'opzione NEARSTACK; di conseguenza, il blocco _STACK viene inserito in DGROUP. Subito dopo l'entry point dobbiamo allora provvedere ad inizializzare correttamente i registri DS, SS e SP; ovviamente, dobbiamo porre:
DS = SS = DGROUP
Il registro SP deve puntare alla fine del blocco DGROUP; ricordando che un indirizzo logico Seg:Offset equivale all'indirizzo fisico (Seg*16)+Offset e assumendo, per comodità, che tutti i blocchi siano allineati al paragrafo (componente Offset pari a 0000h), possiamo affermare che la distanza in byte tra l'inizio del blocco _STACK e l'inizio del blocco _DATA è pari a:
(_STACK * 16) - (_DATA * 16)
A questo valore dobbiamo aggiungere poi STACK_SIZE ottenendo così la dimensione totale in byte del blocco DGROUP; possiamo affermare allora che:
SP = ((_STACK * 16) - (_DATA * 16)) + STACK_SIZE
Nel caso in cui venga scelta l'opzione FARSTACK, tutti questi calcoli non sono più necessari; infatti, in presenza di un segmento di programma con attributo di combinazione _STACK, l'inizializzazione della coppia SS:SP spetta al linker e al SO!

Rispettando le convenzioni per il modello di memoria SMALL, tutti i dati e le procedure devono essere di tipo NEAR; in ogni caso, il programmatore è libero di lavorare con dati e/o procedure di tipo FAR.
In Figura 29.10, ad esempio, la procedura printStr2 richiede un indirizzo FAR in modo da poter visualizzare stringhe definite in vari segmenti di programma; di conseguenza, all'interno di tale procedura, è importantissimo preservare il contenuto originale di DS!

29.5.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: _TEXT, name_TEXT, _DATA, _CONST, _BSS e _STACK.
Il compilatore provvede poi ad inserire tutti i blocchi di dati NEAR in un gruppo DGROUP; se abilitiamo l'opzione NEARSTACK, il compilatore inserisce anche il blocco _STACK in DGROUP attraverso la direttiva:
GROUP DGROUP _DATA _CONST _BSS _STACK
Subito dopo l'entry point il compilatore inserisce tutte le istruzioni necessarie affinché, in fase di esecuzione, risulti:
CS = _TEXT, DS = SS = DGROUP, ES = PSP
Se abilitiamo l'opzione FARSTACK, allora il compilatore esclude il blocco _STACK da DGROUP attraverso la direttiva:
GROUP DGROUP _DATA _CONST _BSS
Subito dopo l'entry point il compilatore inserisce tutte le istruzioni necessarie affinché, in fase di esecuzione, risulti:
CS = _TEXT, DS = DGROUP, ES = PSP, SS = _STACK
In Figura 29.11 utilizziamo i concetti appena esposti per riscrivere il programma illustrato nella Figura 27.8 del Capitolo 27. Nell'esempio di Figura 29.11 stiamo simulando l'opzione FARSTACK; di conseguenza, il blocco _STACK viene escluso da DGROUP e la coppia SS:SP viene inizializzata dal SO.

Questa volta, la modalità di indirizzamento predefinita è NEAR per i dati che si trovano in DGROUP e FAR per le procedure; il programmatore è tenuto a gestire in modo esplicito eventuali dati FAR e procedure NEAR.
In Figura 29.11 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 C2h Imm16 del NEAR return; per tutte le altre procedure, le chiamate predefinite sono di tipo FAR con conseguente FAR return (codice macchina CAh Imm16).

29.5.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.
Il compilatore provvede poi ad inserire tutti i blocchi di dati NEAR in un gruppo DGROUP; se abilitiamo l'opzione NEARSTACK, il compilatore inserisce anche il blocco _STACK in DGROUP attraverso la direttiva:
GROUP DGROUP _DATA _CONST _BSS _STACK
Subito dopo l'entry point il compilatore inserisce tutte le istruzioni necessarie affinché, in fase di esecuzione, risulti:
CS = _TEXT, DS = SS = DGROUP, ES = PSP
Se abilitiamo l'opzione FARSTACK, allora il compilatore esclude il blocco _STACK da DGROUP attraverso la direttiva:
GROUP DGROUP _DATA _CONST _BSS
Subito dopo l'entry point il compilatore inserisce tutte le istruzioni necessarie affinché, in fase di esecuzione, risulti:
CS = _TEXT, DS = DGROUP, ES = PSP, SS = _STACK
In Figura 29.12 utilizziamo i concetti appena esposti per riscrivere il programma illustrato nella Figura 27.10 del Capitolo 27. Nell'esempio di Figura 29.12 stiamo simulando l'opzione FARSTACK; di conseguenza, il blocco _STACK viene escluso da DGROUP e la coppia SS:SP viene inizializzata dal SO.

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; il programmatore è tenuto a gestire in modo esplicito eventuali procedure FAR.

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.5.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: _TEXT, name_TEXT, _DATA, _CONST, _BSS, _STACK, FAR_DATA e FAR_BSS.
Il compilatore provvede poi ad inserire tutti i blocchi di dati NEAR in un gruppo DGROUP; se abilitiamo l'opzione NEARSTACK, il compilatore inserisce anche il blocco _STACK in DGROUP attraverso la direttiva:
GROUP DGROUP _DATA _CONST _BSS _STACK
Subito dopo l'entry point il compilatore inserisce tutte le istruzioni necessarie affinché, in fase di esecuzione, risulti:
CS = _TEXT, DS = SS = DGROUP, ES = PSP
Se abilitiamo l'opzione FARSTACK, allora il compilatore esclude il blocco _STACK da DGROUP attraverso la direttiva:
GROUP DGROUP _DATA _CONST _BSS
Subito dopo l'entry point il compilatore inserisce tutte le istruzioni necessarie affinché, in fase di esecuzione, risulti:
CS = _TEXT, DS = DGROUP, ES = PSP, SS = _STACK
In Figura 29.13 utilizziamo i concetti appena esposti per riscrivere il programma illustrato nella Figura 27.12 del Capitolo 27. Nell'esempio di Figura 29.13 stiamo simulando l'opzione FARSTACK; di conseguenza, il blocco _STACK viene escluso da DGROUP e la coppia SS:SP viene inizializzata dal SO.

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; il programmatore è tenuto a gestire in modo esplicito eventuali procedure NEAR.

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.5.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:
SEGMENT _DATA ALIGN=4 PUBLIC USE32 CLASS=DATA
Questo segmento di programma può essere ottenuto attraverso la seguente macro:
%define STD_DATA32 SEGMENT _DATA ALIGN=4 PUBLIC USE32 CLASS=DATA
Per i dati statici non inizializzati viene riservato un segmento di programma avente le seguenti caratteristiche:
SEGMENT _BSS ALIGN=4 PUBLIC USE32 CLASS=BSS
Questo segmento di programma può essere ottenuto attraverso la seguente macro:
%define STD_BSS32 SEGMENT _BSS ALIGN=4 PUBLIC USE32 CLASS=BSS
Per i dati statici costanti viene riservato un segmento di programma avente le seguenti caratteristiche:
SEGMENT _CONST ALIGN=4 PUBLIC USE32 CLASS=CONST
Questo segmento di programma può essere ottenuto attraverso la seguente macro:
%define STD_CONST32 SEGMENT _CONST ALIGN=4 PUBLIC USE32 CLASS=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:
SEGMENT _STACK ALIGN=4 STACK USE32 CLASS=STACK
Questo segmento di programma può essere ottenuto attraverso la seguente macro:
%define STD_STACK32 SEGMENT _STACK ALIGN=4 STACK USE32 CLASS=STACK
Per il codice viene riservato un segmento di programma avente le seguenti caratteristiche:
SEGMENT _TEXT ALIGN=4 PUBLIC USE32 CLASS=CODE
Questo segmento di programma può essere ottenuto attraverso la seguente macro:
%define STD_CODE32 SEGMENT _TEXT ALIGN=4 PUBLIC USE32 CLASS=CODE
L'assembler provvede a raggruppare tutte queste informazioni in un apposito blocco DGROUP avente le seguenti caratteristiche:
GROUP DGROUP _TEXT _DATA _CONST _BSS _STACK
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.5.7 Programmi Assembly distribuiti su due o più file

In relazione ai programmi Assembly distribuiti su due o più file, valgono tutte le considerazioni già esposte nel Capitolo 28; è possibile quindi impiegare le direttive GLOBAL e EXTERN secondo le regole che già conosciamo.

29.6 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.

29.7 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.