Assembly Base con MASM

Capitolo 12: Struttura di un programma Assembly


In questo capitolo vengono illustrati in dettaglio tutti gli strumenti che il linguaggio Assembly ci mette a disposizione per permetterci di definire la struttura generale di un programma; in particolare, vedremo come si deve procedere per creare i vari segmenti di programma e studieremo le caratteristiche delle informazioni (codice, dati e stack) destinate a riempire i segmenti stessi.

Nei precedenti capitoli è stato detto che un programma destinato ad essere eseguito nella modalità reale 8086, deve essere suddiviso in uno o più blocchi chiamati program segments (segmenti di programma); all'interno di questi segmenti vengono distribuite le informazioni che nel loro insieme formano appunto un programma.
Nel caso più semplice e intuitivo, ciascun segmento di programma viene riservato ad un solo tipo di informazioni; possiamo avere quindi, segmenti riservati al solo codice, segmenti riservati ai soli dati statici e segmenti riservati ai soli dati temporanei (stack).
In modalità protetta, il programmatore può assegnare a ciascun segmento di programma particolari attributi che definiscono la modalità di accesso al segmento stesso; si possono avere in questo modo, segmenti con attributo readable (leggibile), writable (scrivibile) e executable (eseguibile). Un segmento di codice, ad esempio, può essere "etichettato" come executable e readable, per cui, eventuali dati statici definiti al suo interno non possono essere modificati; analogamente, un segmento riservato ai dati statici può essere etichettato come readable e writable, in modo da renderlo accessibile in lettura e in scrittura.
In modalità reale 8086, invece, un qualunque segmento di programma è allo stesso tempo, leggibile, scrivibile ed eseguibile; ciò implica che un qualunque segmento di un programma DOS può contenere al suo interno un misto di codice, dati e stack. Nei prossimi capitoli vedremo che, in particolari circostanze, può essere conveniente scrivere un programma Assembly formato da un unico segmento destinato a contenere qualsiasi tipo di informazione; nel caso generale però, si può ottenere un enorme aumento della flessibilità e della chiarezza di un programma, suddividendolo in almeno tre segmenti che ci permettono di separare in modo ordinato, il codice, i dati e lo stack.

La filosofia dell'Assembly consiste nel lasciare al programmatore la massima libertà di scelta; l'importante è che il programmatore stesso sappia esattamente ciò che sta facendo.

12.1 Struttura generale di un segmento di programma

La Figura 12.1 illustra la struttura generale che assume un segmento di un programma Assembly. Come possiamo notare, un segmento di programma viene "aperto" da un nome simbolico (NomeSegmento), seguito dalla direttiva SEGMENT; il segmento di programma viene poi "chiuso" dallo stesso nome simbolico, seguito dalla direttiva ENDS (end of segment).
In Assembly, le direttive sono parole riservate attraverso le quali il programmatore impartisce dei veri e propri ordini all'assembler; in questo modo è possibile "pilotare" il lavoro che l'assembler stesso deve svolgere.

Un nome simbolico come NomeSegmento, viene chiamato identificatore in quanto serve per identificare simbolicamente una qualche entità del programma; nel caso generale, un identificatore è sempre associato ad una direttiva che specifica il tipo di entità da identificare. Nel caso di Figura 12.1, ad esempio, la direttiva SEGMENT permette all'assembler di classificare NomeSegmento come l'identificatore di un segmento di programma; in assenza di questa direttiva, l'assembler produce un messaggio di errore per informarci che non è in grado di capire a cosa si riferisca il nome simbolico NomeSegmento.

Se si eccettuano alcuni casi particolari, in generale il nome di un identificatore viene scelto a piacere dal programmatore; le regole da rispettare sono le stesse imposte da tutti i linguaggi di programmazione. Un identificatore è formato da una sequenza di codici ASCII che deve iniziare obbligatoriamente con una lettera dell'alfabeto; al suo interno l'identificatore può contenere un misto di lettere dell'alfabeto e cifre numeriche. È proibito, invece, l'uso di spazi, segni di punteggiatura, simboli matematici e altri simboli speciali del codice ASCII; l'unica eccezione riguarda il simbolo underscore ('_') che è associato al codice ASCII 95.
Le regole appena enunciate sono del tutto generali e risultano compatibili quindi con qualsiasi assembler; esistono poi altre regole particolari che variano però da assembler ad assembler.

Il linguaggio Assembly "puro" è case-insensitive e questo significa che non distingue tra lettere maiuscole e lettere minuscole; per l'Assembly, i nomi simbolici Variabile1, VARIABILE1, variabile1, etc, rappresentano tutti lo stesso identificatore. Lo stesso discorso vale per i mnemonici delle varie istruzioni dell'Assembly; non c'è differenza quindi tra PUSH e push o tra POP e pop.
La situazione cambia nel momento in cui si deve interfacciare l'Assembly con i linguaggi di alto livello; in questo caso, infatti, bisogna tenere presente che certi linguaggi come il Pascal e il BASIC sono case-insensitive, mentre altri linguaggi come il C sono case-sensitive. Come vedremo in un apposito capitolo, per potersi adattare alle convenzioni seguite da questi linguaggi, l'Assembly permette al programmatore di abilitare o meno la distinzione tra lettere maiuscole e lettere minuscole negli identificatori; si tenga anche presente che in molti linguaggi di alto livello, il carattere underscore usato negli identificatori, può assumere un significato speciale.

Tornando alla Figura 12.1, è necessario sottolineare che per motivi di stile e di chiarezza, è meglio scegliere per l'identificatore di un segmento di programma, un nome che abbia attinenza con l'uso che si vuole fare del segmento stesso; nel caso, ad esempio, di un segmento di codice, possiamo scegliere dei nomi come SEGM_CODICE, SEGCODE, CODESEGM, etc, mentre nel caso di un segmento di dati possiamo scegliere dei nomi come SEGM_DATI, SEGDATA, DATASEGM, etc. Naturalmente, è proibito usare per gli identificatori nomi di parole riservate come SEGMENT, ADD, PUSH, POP, etc; si tenga presente che esistono anche nomi predefiniti come CODE, DATA, STACK, CODESEG, DATASEG, STACKSEG, etc, che vengono impiegati quando si interfaccia l'Assembly con i linguaggi di alto livello.

Per il programmatore, l'identificatore di un segmento di programma ha un significato importantissimo. Ricordiamo a tale proposito che non appena un programma viene caricato in memoria, ad ogni segmento di programma viene assegnato un determinato indirizzo fisico iniziale; a questo indirizzo fisico iniziale corrisponde univocamente un indirizzo logico normalizzato Seg:Offset. Ebbene: Supponiamo, ad esempio, di avere un segmento di programma chiamato CODESEGM, che in fase di caricamento in memoria, viene fatto partire dall'indirizzo fisico 0BFA2h; da questo indirizzo fisico ricaviamo univocamente l'indirizzo logico normalizzato 0BFAh:0002h. Possiamo dire allora che durante la fase di esecuzione del programma si ha:
CODESEGM = 0BFAh
Una volta che un programma è stato caricato in memoria, tutti gli indirizzi Seg:Offset assegnati alle varie informazioni statiche (dati statici e istruzioni), rimangono fissi per tutta la fase di esecuzione; questo discorso vale quindi anche per la componente Seg assegnata ad ogni segmento di programma. Da queste considerazioni si deduce allora che l'identificatore di un segmento di programma contiene un valore immediato (costante) di tipo intero senza segno a 16 bit; questo identificatore può essere impiegato come operando sorgente di tipo Imm16 nelle istruzioni che coinvolgono operandi a 16 bit. Riprendendo il precedente esempio, possiamo scrivere quindi istruzioni del tipo:
mov ax, CODESEGM
(trasferimento dati da Imm16 a Reg16).

Per definire in modo dettagliato tutte le caratteristiche di un segmento di programma, dobbiamo servirci di una serie di cosiddetti attributi che, come si vede in Figura 12.1, formano una lista che deve essere disposta subito dopo la direttiva SEGMENT; l'ordine con il quale vengono disposti i vari attributi non ha alcuna importanza. Esaminando gli attributi e il contenuto dei vari segmenti di programma, l'assembler ricava una numerosa serie di informazioni che verranno poi passate al linker; il compito del linker è quello di utilizzare queste informazioni, per rendere materialmente effettive tutte le richieste che abbiamo impartito all'assembler.
Gli attributi da assegnare ad un segmento di programma sono tutti facoltativi; l'assembler provvede ad assegnare un valore predefinito a tutti gli attributi non specificati dal programmatore. Analizziamo ora in modo approfondito il significato dei vari attributi di un segmento di programma.

12.1.1 L'attributo "Allineamento"

Attraverso l'attributo Allineamento, il programmatore può richiedere al linker che un determinato segmento di programma venga disposto in memoria a partire da un indirizzo fisico avente particolari requisiti di allineamento; questo aspetto è molto importante in quanto, come sappiamo, codice e dati correttamente allineati in memoria, vengono acceduti alla massima velocità possibile dalla CPU. Questo attributo opera solo sull'indirizzo fisico iniziale di un segmento di programma e non sulle informazioni contenute al suo interno; è chiaro però che, ad esempio, un segmento dati allineato ad un indirizzo pari, facilita il lavoro del programmatore che ha la necessità di allineare ad indirizzi pari anche i dati presenti nel segmento stesso.

La tabella di Figura 12.2 illustra in dettaglio i diversi valori che può assumere l'attributo Allineamento e gli effetti prodotti da questi valori sull'indirizzo fisico iniziale di un segmento di programma. Per chiarire questi importanti concetti, vediamo subito un esempio pratico; supponiamo a tale proposito di richiedere al DOS l'esecuzione di un programma formato nell'ordine, da un blocco codice chiamato CODESEGM, da un blocco dati chiamato DATASEGM e da un blocco stack chiamato STACKSEGM. Come già sappiamo, il DOS provvede innanzi tutto a trovare un blocco di memoria le cui dimensioni devono essere sufficienti a contenere il programma da eseguire; una volta che il DOS ha caricato il programma in questo blocco di memoria, grazie al lavoro svolto dal linker i vari segmenti si vengono a trovare disposti in modo da rispettare i requisiti di allineamento che noi abbiamo richiesto.
Nel nostro esempio supponiamo che il primo segmento del programma sia CODESEGM e supponiamo, inoltre, che tale blocco, dopo il caricamento in memoria, termini esattamente all'indirizzo fisico 0ADA4h; a questo punto ci interessa sapere come viene disposto il successivo blocco DATASEGM.
La Figura 12.3 illustra proprio il tipo di indirizzo fisico iniziale che viene assegnato a DATASEGM in funzione dell'attributo di allineamento che abbiamo scelto per questo segmento di programma; osserviamo che il blocco CODESEGM è stato colorato in verde, mentre il blocco DATASEGM è stato colorato in celeste. Sul lato sinistro della Figura 12.3 vengono mostrati gli indirizzi fisici a 20 bit associati alle varie celle di memoria; sul lato destro vengono mostrati i corrispondenti indirizzi logici normalizzati. È importante ribadire che in modalità reale, ad ogni indirizzo fisico a 20 bit possono corrispondere uno o più indirizzi logici generici; ad esempio, dall'indirizzo fisico 0ADA5h possiamo ricavare gli indirizzi logici 0ADAh:0005h, 0AD9h:0015h, 0AD8h:0025h, etc.
Normalizzando, invece, gli indirizzi logici, si ottiene una corrispondenza biunivoca con gli indirizzi fisici; in questo caso, infatti, all'indirizzo fisico 0ADA5h corrisponde univocamente l'indirizzo logico normalizzato 0ADAh:0005h e viceversa.

In Figura 12.3a vediamo quello che succede nel caso in cui per DATASEGM venga scelto un allineamento di tipo BYTE; questa situazione si verifica quando il programmatore apre il blocco DATASEGM con una linea del tipo:
DATASEGM SEGMENT BYTE
In questo caso stiamo richiedendo che DATASEGM venga allineato al prossimo indirizzo fisico libero, successivo al blocco CODESEGM; come si può notare in Figura 12.3a, il prossimo indirizzo fisico libero successivo a 0ADA4h è ovviamente 0ADA5h. Il blocco DATASEGM viene quindi disposto in memoria a partire dall'indirizzo fisico 0ADA5h; l'indirizzo logico normalizzato associato a 0ADA5h è 0ADAh:0005h e ciò significa che:
DATASEGM = 0ADAh
Possiamo dire quindi che DATASEGM viene indirizzato attraverso coppie Seg:Offset la cui componente Seg vale 0ADAh; come già sappiamo, questa componente Seg è associata all'indirizzo fisico multiplo di 16 (0ADA0h) che più si avvicina per difetto a 0ADA5h. Possiamo anche dire che il segmento di programma DATASEGM viene inserito nel segmento di memoria n. 0ADAh a partire dal byte n. 0005h; di conseguenza, gli offset all'interno di DATASEGM partono dal valore minimo 0005h.
L'indirizzo fisico 0ADA5h da cui inizia DATASEGM è chiaramente un indirizzo dispari; come si può facilmente intuire, questa situazione si rivela adeguata per una CPU con architettura a 8 bit, mentre può creare problemi di allineamento dei dati nel caso di CPU con architettura a 16 bit o superiore.
Osserviamo inoltre che con l'allineamento di tipo BYTE per i segmenti, si ottiene la massima compattezza possibile per un programma; infatti, i vari segmenti vengono disposti in modo consecutivo e contiguo, senza buchi di memoria tra un segmento e l'altro.

In Figura 12.3b vediamo quello che succede nel caso in cui per DATASEGM venga scelto un allineamento di tipo WORD; questa situazione si verifica quando il programmatore apre il blocco DATASEGM con una linea del tipo:
DATASEGM SEGMENT WORD
In questo caso stiamo richiedendo che DATASEGM venga allineato al prossimo indirizzo fisico libero, successivo al blocco CODESEGM e multiplo intero di 2 byte; come si può notare in Figura 12.3b, il prossimo indirizzo fisico libero successivo a 0ADA4h e multiplo intero di 2 è ovviamente 0ADA6h. Il blocco DATASEGM viene quindi disposto in memoria a partire dall'indirizzo fisico 0ADA6h; l'indirizzo logico normalizzato associato a 0ADA6h è 0ADAh:0006h e ciò significa che:
DATASEGM = 0ADAh
Possiamo dire quindi che DATASEGM viene indirizzato attraverso coppie Seg:Offset la cui componente Seg vale 0ADAh; questa componente Seg è associata all'indirizzo fisico multiplo di 16 (0ADA0h) che più si avvicina per difetto a 0ADA6h. Possiamo anche dire che il segmento di programma DATASEGM viene inserito nel segmento di memoria n. 0ADAh a partire dal byte n. 0006h; di conseguenza, gli offset all'interno di DATASEGM partono dal valore minimo 0006h.
L'indirizzo fisico 0ADA6h da cui inizia DATASEGM è chiaramente un indirizzo pari; come si può facilmente intuire, questa situazione si rivela adeguata per tutte le CPU con architettura a 16 bit o inferiore, mentre può creare problemi di allineamento dei dati nel caso di CPU con architettura a 32 bit o superiore.
Osserviamo inoltre che per poter soddisfare i requisiti di allineamento che abbiamo richiesto, viene lasciato un buco di memoria tra CODESEGM e DATASEGM; questo buco è pari a 1 byte e rimane inutilizzato.

In Figura 12.3c vediamo quello che succede nel caso in cui per DATASEGM venga scelto un allineamento di tipo DWORD; questa situazione si verifica quando il programmatore apre il blocco DATASEGM con una linea del tipo:
DATASEGM SEGMENT DWORD
In questo caso stiamo richiedendo che DATASEGM venga allineato al prossimo indirizzo fisico libero, successivo al blocco CODESEGM e multiplo intero di 4 byte; come si può notare in Figura 12.3c, il prossimo indirizzo fisico libero successivo a 0ADA4h e multiplo intero di 4 è ovviamente 0ADA8h (infatti, 0ADA6h pur essendo pari non è divisibile per 4). Il blocco DATASEGM viene quindi disposto in memoria a partire dall'indirizzo fisico 0ADA8h; l'indirizzo logico normalizzato associato a 0ADA8h è 0ADAh:0008h e ciò significa che:
DATASEGM = 0ADAh
Possiamo dire quindi che DATASEGM viene indirizzato attraverso coppie Seg:Offset la cui componente Seg vale 0ADAh; questa componente Seg è associata all'indirizzo fisico multiplo di 16 (0ADA0h) che più si avvicina per difetto a 0ADA8h. Possiamo anche dire che il segmento di programma DATASEGM viene inserito nel segmento di memoria n. 0ADAh a partire dal byte n. 0008h; di conseguenza, gli offset all'interno di DATASEGM partono dal valore minimo 0008h.
L'indirizzo fisico 0ADA8h da cui inizia DATASEGM è chiaramente un indirizzo pari multiplo intero di 4; come si può facilmente intuire, questa situazione si rivela adeguata per tutte le CPU con architettura a 32 bit o inferiore.
Osserviamo inoltre che per poter soddisfare i requisiti di allineamento che abbiamo richiesto, viene lasciato un buco di memoria tra CODESEGM e DATASEGM; questo buco è pari a 3 byte e rimane inutilizzato.

In Figura 12.3d vediamo quello che succede nel caso in cui per DATASEGM venga scelto un allineamento di tipo PARA; questa situazione si verifica quando il programmatore apre il blocco DATASEGM con una linea del tipo:
DATASEGM SEGMENT PARA
In questo caso stiamo richiedendo che DATASEGM venga allineato al prossimo indirizzo fisico libero, successivo a CODESEGM e multiplo intero di 16 byte; come si può notare in Figura 12.3d, il prossimo indirizzo fisico libero successivo a 0ADA4h e multiplo intero di 16 è ovviamente 0ADB0h (ricordiamo, infatti, che tutti gli indirizzi fisici multipli di 16, espressi in esadecimale, hanno sempre la cifra meno significativa che vale 0). Il blocco DATASEGM viene quindi disposto in memoria a partire dall'indirizzo fisico 0ADB0h; l'indirizzo logico normalizzato associato a 0ADB0h è 0ADBh:0000h e ciò significa che:
DATASEGM = 0ADBh
Possiamo dire quindi che DATASEGM viene indirizzato attraverso coppie Seg:Offset la cui componente Seg vale 0ADBh; questa componente Seg è associata all'indirizzo fisico multiplo di 16 (0ADB0h) che più si avvicina per difetto a 0ADB0h. L'allineamento di tipo PARA presenta quindi l'importante caratteristica di far coincidere l'inizio di un segmento di programma con l'inizio di un segmento di memoria; nel caso del nostro esempio, l'inizio (0ADB0h) del segmento di programma DATASEGM coincide con l'inizio del segmento di memoria n. 0ADBh e quindi, gli offset all'interno dello stesso DATASEGM partono dal valore minimo possibile 0000h.
L'indirizzo fisico 0ADB0h da cui inizia DATASEGM è chiaramente un indirizzo pari multiplo intero di 16 (e quindi anche multiplo intero di 4 e di 2); come si può facilmente intuire, questa situazione si rivela adeguata per tutte le CPU con architettura a 128 bit o inferiore.
Osserviamo inoltre che per poter soddisfare i requisiti di allineamento che abbiamo richiesto, viene lasciato un buco di memoria tra CODESEGM e DATASEGM; questo buco è pari a ben 11 byte e rimane inutilizzato.

Un programmatore Assembly degno di questo nome, deve necessariamente conoscere e padroneggiare i concetti appena esposti; come vedremo però nel seguito del capitolo, anche i principianti possono stare tranquilli, in quanto tutti questi aspetti, vengono gestiti automaticamente dall'assembler e dal linker.

In modalità reale, gli attributi di allineamento più utilizzati sono: BYTE, WORD, DWORD e PARA, mentre gli attributi PAGE e MEMPAGE vengono largamente usati in modalità protetta; si tenga presente che non tutti i linker offrono il supporto per un allineamento di tipo MEMPAGE.

Dalle considerazioni appena esposte risulta chiaramente che in modalità reale, l'attributo di allineamento più adatto per tutte le circostanze è sicuramente PARA, in quanto 16 è un multiplo intero anche di 4 e di 2 (oltre che ovviamente di 1); l'unico piccolo difetto dell'attributo PARA è legato alla eventualità di un leggero spreco di memoria come risulta evidente anche nell'esempio di Figura 12.3d. Se il programmatore definisce un segmento di programma privo dell'attributo di allineamento, MASM utilizza il valore predefinito PARA; per motivi di stile e di chiarezza, si consiglia vivamente di specificare sempre tutti gli attributi dei segmenti di programma. In questo modo facilitiamo il compito di chi deve eventualmente occuparsi della manutenzione e dell'aggiornamento del codice sorgente che noi abbiamo scritto; indicando esplicitamente tutti gli attributi di un segmento di programma, evitiamo anche il rischio di scrivere programmi che dipendono dal comportamento predefinito dei diversi assembler.

12.1.2 L'attributo "Combinazione"

Nel caso più semplice, un programma Assembly è interamente contenuto all'interno di un unico file; nel seguito del capitolo e nei capitoli successivi, verrà utilizzato il termine modulo per indicare ciascuno dei file che contengono il codice sorgente di un programma Assembly.
All'interno di un singolo modulo, uno stesso segmento di programma può essere aperto e chiuso più volte; tutto ciò significa che, ad esempio, un segmento aperto dalla linea:
DATASEGM SEGMENT WORD
dopo essere stato chiuso, può essere nuovamente riaperto all'interno dello stesso modulo. Le varie parti che formano DATASEGM devono condividere tutte lo stesso nome e gli stessi identici attributi; in fase di assemblaggio del programma, l'assembler provvede a disporre le varie parti una di seguito all'altra in modo da formare un unico segmento di programma La dimensione in byte del segmento risultante è pari alla somma delle dimensioni delle varie parti che formano il segmento stesso; naturalmente, questa somma non deve essere superiore a 65536 byte.

La situazione cambia radicalmente nel caso più generale di un programma Assembly formato da due o più moduli; anche in questo caso, il programmatore ha la possibilità di "spezzare" un segmento di programma in due o più parti che possono trovarsi, sia all'interno di uno stesso modulo, sia in moduli differenti.
In una situazione di questo genere, si presenta il problema di come debbano essere combinate tra loro le varie parti che formano un segmento di programma e che si trovano distribuite in moduli differenti; per poter gestire questa situazione con la massima flessibilità possibile, l'Assembly ci mette a disposizione l'attributo Combinazione. Attraverso l'attributo Combinazione, il programmatore può stabilire se e come il linker debba combinare tra loro i vari segmenti che formano un programma distribuito su due o più moduli; la Figura 12.4 illustra i diversi valori che può assumere questo attributo e gli effetti prodotti da tali valori sui segmenti presenti nel programma. È necessario ribadire che l'attributo Combinazione opera solo sui segmenti di programma distribuiti su due o più moduli; questo attributo non ha quindi alcun effetto su un segmento di programma suddiviso in due o più parti disposte tutte all'interno dello stesso modulo.

Per analizzare i vari casi che si possono presentare, supponiamo di avere un programma Assembly distribuito in due moduli chiamati MAIN.ASM e LIB1.ASM; possiamo disporre, ad esempio, in MAIN.ASM il programma principale e in LIB1.ASM una libreria di sottoprogrammi chiamabili dallo stesso programma principale.
Supponiamo ora che in entrambi i moduli, sia presente un segmento di programma aperto da una linea del tipo:
DATIPRIVATI SEGMENT WORD PRIVATE
In questo caso, i due segmenti DATIPRIVATI, nonostante abbiano lo stesso nome e gli stessi attributi, verranno tenuti separati dal linker; ciò è dovuto alla presenza dell'attributo PRIVATE. Il segmento DATIPRIVATI presente in LIB1.ASM risulta invisibile nel modulo MAIN.ASM e viceversa; questo discorso vale anche per i due identificatori DATIPRIVATI che, essendo definiti in due moduli differenti, non entrano in conflitto tra loro.
Se i due segmenti DATIPRIVATI si trovano definiti all'interno dello stesso modulo, vengono uniti tra loro dall'assembler nonostante la presenza dell'attributo PRIVATE; in questo modo al linker verrà passato un unico segmento di programma DATIPRIVATI.

Supponiamo ora che in entrambi i moduli, sia presente un segmento di programma aperto da una linea del tipo:
DATIPUBBLICI SEGMENT WORD PUBLIC
In questo caso, i due segmenti DATIPUBBLICI vengono uniti tra loro dal linker in modo da formare un unico segmento di programma; ciò è dovuto alla presenza dell'attributo PUBLIC.
I due segmenti di programma possono anche differire tra loro per l'attributo Allineamento; possiamo assegnare, ad esempio, al primo segmento un allineamento WORD e al secondo segmento un allineamento PARA. Il linker provvede ad unire i due segmenti rispettando anche gli attributi di allineamento che abbiamo richiesto per ciascun segmento.

L'attributo STACK è del tutto simile a PUBLIC; la differenza fondamentale sta nel fatto che in presenza di un segmento di programma con attributo STACK, il linker provvede anche ad inizializzare la coppia SS:SP.
Supponiamo che nei due moduli del nostro esempio siano presenti due segmenti aperti con una linea del tipo:
STACKSEGM SEGMENT PARA STACK
Supponiamo inoltre che il primo segmento abbia una dimensione di 32 byte e che il secondo segmento abbia una dimensione di 48 byte; questi due segmenti vengono uniti dal linker che ottiene così un unico segmento STACKSEGM grande 32+48=80 byte (cioè 0050h byte). Grazie, infatti, all'allineamento PARA e alle dimensioni (32 e 64) multiple di 16, i due segmenti vengono disposti dal linker in modo consecutivo e contiguo; in sostanza, tra un segmento e l'altro non è presente alcun buco di memoria.
A questo punto il linker inizializza SS:SP ponendo SP=0050h (massima capacità in byte del segmento risultante); per SS il linker utilizza un valore simbolico che verrà poi "aggiustato" dal DOS. È chiaro, infatti, che solo al momento di caricare il programma in memoria sarà possibile conoscere la vera componente Seg assegnata a STACKSEGM; appare anche intuitivo il fatto che i segmenti con attributo STACK si rivelano particolarmente utili per la creazione dello stack segment di un programma.
Nel caso del MASM si sconsiglia vivamente la creazione di dati statici inizializzati nei segmenti di tipo STACK distribuiti su due o più moduli; tutti questi argomenti verranno ripresi in modo approfondito nel seguito del capitolo e nei capitoli successivi.

Supponiamo ora che in entrambi i moduli MAIN.ASM e LIB1.ASM, sia presente un segmento di programma aperto da una linea del tipo:
DATICOMUNI SEGMENT DWORD COMMON
In questo caso, i due segmenti DATICOMUNI vengono sovrapposti tra loro dal linker in modo che condividano entrambi lo stesso indirizzo fisico iniziale (che in questo caso è allineato alla DWORD); il segmento risultante ha quindi una dimensione in byte pari a quella del segmento più grande coinvolto nella sovrapposizione.
Questa situazione implica che le informazioni contenute nel primo segmento DATICOMUNI, verranno sovrascritte (tutte o in parte) dalle informazioni contenute nel secondo segmento DATICOMUNI che viene sovrapposto al primo; bisogna prestare quindi particolare attenzione quando si definiscono dati inizializzati in questo tipo di segmenti.
Supponiamo, ad esempio, che il primo segmento contenga 35 byte di dati e che il secondo segmento contenga 20 byte di dati; subito dopo la sovrapposizione, si ottiene un segmento risultante con i primi 20 byte del primo segmento che vengono sovrascritti dai 20 byte del secondo segmento. Osserviamo inoltre che la dimensione del segmento risultante è pari a quella del primo segmento e cioè, 35 byte.
I segmenti di tipo COMMON vengono utilizzati, ad esempio, dal BASIC per la condivisione dei dati tra due o più moduli; di conseguenza, come vedremo in un apposito capitolo, i segmenti COMMON sono necessari anche per la condivisione dei dati tra moduli BASIC e moduli Assembly.

Analizziamo infine i segmenti con attributo AT XXXXh; come è stato già spiegato, attraverso questo potente attributo, il programmatore ha la possibilità di posizionare un segmento di programma direttamente all'indirizzo logico iniziale XXXXh:0000h, cioè all'indirizzo fisico assoluto XXXX0h allineato al paragrafo.
Ricordando, ad esempio, che la ROM BIOS del computer viene mappata in RAM in una finestra da 64 KiB che parte dall'indirizzo logico F000h:0000h, possiamo accedere a questa finestra attraverso un apposito segmento di programma aperto da una linea del tipo:
BIOSDATA SEGMENT AT 0F000h
(il perché dello zero alla sinistra di F000h verrà spiegato più avanti).

Per i segmenti di tipo AT XXXXh, gli altri attributi in genere vengono omessi; in caso contrario, il MASM richiede che AT XXXXh sia l'ultimo attributo della lista.
Non è permesso definire dati inizializzati in un segmento di programma di tipo AT XXXXh; in caso contrario, l'assembler genera un semplice messaggio di avvertimento per informarci che le varie inizializzazioni verranno ignorate.

Se il programmatore definisce un segmento di programma privo dell'attributo Combinazione, il MASM si serve dell'attributo predefinito PRIVATE.

12.1.3 L'attributo "Dimensione"

L'attributo Dimensione si riferisce all'ampiezza in bit delle componenti Offset utilizzate per indirizzare un segmento di programma; la Figura 12.5 illustra i due valori disponibili per questo attributo. Come già sappiamo, in modalità reale gli offset devono essere compresi tra 0000h e FFFFh, per cui dobbiamo utilizzare l'attributo USE16; possiamo definire, ad esempio, un segmento del tipo:
DATASEGM SEGMENT WORD PUBLIC USE16
I segmenti di questo tipo devono avere, ovviamente, una ampiezza massima di 64 KiB in quanto, con gli offset a 16 bit non possiamo accedere ad informazioni che si trovano oltre questo limite.
L'attributo USE32 viene utilizzato negli ambienti operativi che supportano la modalità protetta a 32 bit; in tali ambienti si lavora con offset a 32 bit che permettono di indirizzare, teoricamente, segmenti da 232=4 GiB!

Se il programmatore definisce un segmento di programma privo dell'attributo Dimensione, il MASM si serve di un attributo predefinito che dipende dalla eventuale presenza di apposite direttive che specificano il set di istruzioni che vogliamo utilizzare; in assenza di queste direttive, MASM assume che il programmatore voglia utilizzare il set di istruzioni della 8086, per cui l'attributo Dimensione predefinito è USE16.
Alternativamente possiamo indicare in modo esplicito le direttive illustrate in Figura 12.6; questa figura mostra anche l'ampiezza predefinita per gli offset, che MASM utilizza in funzione del set di istruzioni che abbiamo specificato. Queste direttive vengono inserite in genere all'inizio di ogni modulo e hanno il solo scopo di specificare il set di istruzioni che vogliamo utilizzare; è chiaro quindi che un pessimo programma che utilizza la direttiva .8086, non migliora di certo con la direttiva .586!
Un programma con direttiva .8086 gira su qualsiasi CPU di classe 8086 o superiore, mentre un programma con direttiva .586 non può girare su CPU di classe 80486 o inferiore; si tenga anche presente che esistono numerose altre direttive Processor per le CPU e le FPU, che verranno illustrate nei capitoli successivi.

12.1.4 L'attributo "Classe"

Attraverso l'attributo Classe, il programmatore ha la possibilità di imporre al linker l'ordine con il quale disporre i vari segmenti che formano un programma; questo attributo è formato da una stringa racchiusa tra apici singoli. Nel mondo dell'Assembly vengono largamente utilizzate le classi consigliate dal MASM come, ad esempio, 'DATA', 'CODE', 'STACK', etc; queste classi hanno il pregio di indicare chiaramente il tipo di informazioni contenute in un segmento di programma. Le classi MASM diventano obbligatorie quando si interfaccia l'Assembly con i linguaggi di alto livello; se, invece, dobbiamo scrivere un programma in puro Assembly, nessuno ci impedisce di utilizzare classi del tipo 'MELA', 'CILIEGIA', 'LIMONE', etc.

Per capire il principio di funzionamento dell'attributo Classe, bisogna dire innanzi tutto che il linker esamina i vari segmenti di programma nello stesso ordine con il quale sono stati disposti nel codice sorgente dal programmatore; supponiamo allora che il nostro programma sia distribuito nei due moduli MAIN.ASM e LIB1.ASM. Nel modulo MAIN.ASM sono presenti i seguenti segmenti di programma:
LOCALDATA SEGMENT PARA PRIVATE USE16 'DATA'
DATASEGM SEGMENT PARA PUBLIC USE16 'DATA'
CODESEGM SEGMENT PARA PUBLIC USE16 'CODE'
STACKSEGM SEGMENT PARA STACK USE16 'STACK'
Nel modulo LIB1.ASM sono presenti i seguenti segmenti di programma:
CODESEGM SEGMENT PARA PUBLIC USE16 'CODE'
LOCALDATA SEGMENT PARA PRIVATE USE16 'DATA'
DATASEGM SEGMENT PARA PUBLIC USE16 'DATA'
STACKSEGM SEGMENT PARA STACK USE16 'STACK'
Il linker parte dal modulo MAIN.ASM e incontra per primo il segmento LOCALDATA di classe 'DATA'; questo segmento è PRIVATE, per cui non verrà combinato con altri eventuali segmenti LOCALDATA presenti nel modulo LIB1.ASM.
Siccome LOCALDATA è di classe 'DATA', il linker va a cercare tutti gli altri segmenti di classe 'DATA'; nel modulo LIB1.ASM il linker aveva già incontrato un secondo segmento LOCALDATA, che essendo PRIVATE non viene combinato con il precedente LOCALDATA. Nel modulo MAIN.ASM viene incontrato DATASEGM che viene unito, invece, con il DATASEGM del modulo LIB1.ASM.
Non essendoci altri segmenti di classe 'DATA', il linker passa al segmento CODESEGM di classe 'CODE' presente nel modulo MAIN.ASM; questo segmento viene unito con il segmento CODESEGM presente in LIB1.ASM.
Non essendoci altri segmenti di classe 'CODE', il linker passa al segmento STACKSEGM di classe 'STACK' presente nel modulo MAIN.ASM; questo segmento viene unito con il segmento STACKSEGM presente in LIB1.ASM.
Alla fine il linker genera un programma eseguibile la cui struttura interna è la seguente:
LOCALDATA SEGMENT PARA PRIVATE USE16 'DATA'
LOCALDATA SEGMENT PARA PRIVATE USE16 'DATA'
DATASEGM SEGMENT PARA PUBLIC USE16 'DATA'
CODESEGM SEGMENT PARA PUBLIC USE16 'CODE'
STACKSEGM SEGMENT PARA STACK USE16 'STACK'
Come possiamo notare, il linker ha ordinato i vari segmenti disponendo, prima quelli di classe 'DATA', poi quelli di classe 'CODE' e infine quelli di classe 'STACK'. Il metodo di ordinamento dei segmenti di programma può essere gestito anche attraverso le direttive illustrate in Figura 12.7. La direttiva .SEQ (predefinita) indica al linker che i segmenti di programma devono essere disposti nello stesso ordine stabilito dal programmatore nel codice sorgente; in presenza dell'attributo Classe, i vari segmenti vengono ordinati sequenzialmente e raggruppati per classi.
La direttiva .ALPHA indica al linker che i segmenti di programma devono essere disposti in ordine alfabetico rispetto ai loro nomi; in presenza dell'attributo Classe, i vari segmenti vengono ordinati alfabeticamente e raggruppati per classi.
La direttiva .DOSSEG indica al linker che i segmenti di programma devono essere disposti secondo lo standard DOS e cioè, prima i segmenti di codice, poi i segmenti di dati statici e infine lo stack; la direttiva .DOSSEG è obsoleta e non dovrebbe essere più utilizzata.

L'ordinamento dei segmenti di programma può essere gestito anche attraverso un metodo diretto che permette al programmatore di indicare al linker come disporre i vari blocchi; tale metodo è basato sull'uso dei cosiddetti dummy segments (letteralmente, segmenti fantoccio). Si tratta di segmenti di programma vuoti che hanno il solo scopo di imporre al linker la sequenza di ordinamento dei segmenti stessi; riprendendo il precedente esempio relativo ai due moduli MAIN.ASM e LIB1.ASM, proprio all'inizio del modulo MAIN.ASM possiamo disporre i dummy segments mostrati in Figura 12.8. Siccome il linker incontra per primi questi tre dummy segments, alla fine genera un programma eseguibile la cui struttura interna è la seguente:
CODESEGM SEGMENT PARA PUBLIC USE16 'CODE'
DATASEGM SEGMENT PARA PUBLIC USE16 'DATA'
LOCALDATA SEGMENT PARA PRIVATE USE16 'DATA'
LOCALDATA SEGMENT PARA PRIVATE USE16 'DATA'
STACKSEGM SEGMENT PARA STACK USE16 'STACK'
Come si può notare, questa volta i segmenti di programma vengono ordinati ponendo per primi quelli di classe 'CODE', seguiti poi da quelli di classe 'DATA' e infine da quelli di classe 'STACK'; si tenga presente che in determinate circostanze, l'ordinamento dei segmenti di un programma Assembly assume una importanza enorme.

12.1.5 Considerazioni finali sui segmenti di programma

Per chiudere questa prima parte del capitolo, si possono riassumere alcuni concetti importanti sui segmenti di programma.
In modalità reale 8086, un segmento di programma non può contenere più di 65536 byte di informazioni (64 KiB); se abbiamo bisogno, ad esempio, di più di 64 KiB di dati, dobbiamo ripartirli in due o più segmenti di dati. Lo stesso discorso vale per i segmenti di codice e per i segmenti di stack; in relazione allo stack bisogna dire che generalmente un programma ha bisogno al massimo di qualche migliaio di byte per i dati temporanei, per cui un solo segmento di stack è più che sufficiente (la gestione di un programma con due o più segmenti di stack è piuttosto impegnativa e richiede una notevole padronanza del linguaggio Assembly).
Le considerazioni appena esposte si applicano anche alla combinazione dei segmenti di programma attraverso gli attributi illustrati in Figura 12.4; tutti i segmenti che scaturiscono da queste combinazioni, devono avere una dimensione complessiva che non può superare i 64 KiB.

Non ci si deve preoccupare se i concetti sin qui esposti appaiono ancora poco chiari; tutto ciò che riguarda i segmenti di programma e il loro contenuto, verrà compreso meglio attraverso svariati esempi pratici che verranno presentati nel seguito del capitolo e nei capitoli successivi.

Dopo aver esaminato in dettaglio le caratteristiche generali dei segmenti di programma, possiamo occuparci ora di tutto ciò che riguarda il contenuto dei segmenti stessi; vediamo quindi come bisogna procedere per inserire codice, dati e stack nei segmenti che formano un programma Assembly.

12.2 Definizione dei dati statici elementari dell'Assembly

L'Assembly, più che un linguaggio di programmazione, è un insieme di strumenti attraverso i quali si può fare praticamente di tutto; per essere precisi, bisogna dire che l'Assembly è uno strato software intermedio, attraverso il quale il programmatore può accedere in modo molto semplice ai potenti e complessi strumenti forniti dalla CPU. Lavorare in Assembly significa quindi dialogare direttamente con il mondo binario della CPU; tutto ciò si ripercuote anche sul procedimento che bisogna seguire per definire i dati statici di un programma Assembly.

Abbiamo già visto che i dati statici sono così definiti, in quanto esistono staticamente per tutta la fase di esecuzione di un programma; in sostanza, a ciascun dato statico di un programma, viene assegnata una locazione fissa di memoria che permane per tutta la fase di esecuzione del programma stesso. Nei linguaggi di alto livello, i dati statici sono quelli che vengono definiti al di fuori di qualsiasi sottoprogramma (procedura o funzione); come vedremo in un capitolo successivo, i dati definiti all'interno di un sottoprogramma vengono in genere sistemati nello stack in modo da poterli creare quando il sottoprogramma viene chiamato e distruggere quando il sottoprogramma stesso termina.

Quando si lavora con i linguaggi di alto livello, si ha l'impressione che i relativi compilatori ed interpreti siano in grado di distinguere tra diversi tipi di dati; la Figura 12.9, ad esempio, illustra una serie di definizioni di dati statici in un programma scritto in linguaggio C. Come si può notare in Figura 12.9, sembrerebbe che i compilatori C siano in grado di distinguere tra numeri interi con o senza segno, numeri con la virgola (reali), stringhe alfanumeriche, etc; in realtà, i compilatori e gli interpreti C, Pascal, FORTRAN, BASIC, etc, non fanno altro che simulare via software la distinzione che effettivamente esiste tra i diversi tipi di dati.
Nel momento in cui il codice sorgente viene tradotto in codice macchina, questa distinzione scompare; come già sappiamo, infatti, la CPU è in grado di maneggiare esclusivamente numeri binari e non ha la più pallida idea di cosa sia un numero reale, una stringa, una lettera dell'alfabeto, etc.

In relazione alla fase di definizione di un qualsiasi dato statico, la CPU ha bisogno di conoscere tre informazioni fondamentali che sono: Supponiamo, ad esempio, di avere un segmento dati chiamato DATASEGM; all'offset 002Dh di questo segmento è presente un dato statico da 16 bit, che contiene il valore iniziale 3AC8h. Le tre informazioni fondamentali associate a questo dato sono quindi, l'offset 002Dh, l'ampiezza in bit 16 e il contenuto iniziale 3AC8h.
Il programmatore Assembly deve attenersi a queste esigenze della CPU; ciò significa che per ogni dato statico che definiamo in un programma, dobbiamo specificare in modo chiaro l'offset, l'ampiezza in bit e il valore iniziale. A tale proposito, dobbiamo servirci di una apposita sintassi che assume la seguente forma:
NomeSimbolico DIRETTIVA valore_iniziale
Il NomeSimbolico viene scelto a piacere dal programmatore e deve presentare le caratteristiche già discusse in relazione ai nomi dei segmenti di programma; questo nome è facoltativo e il suo scopo è quello di permettere al programmatore di individuare con estrema facilità l'offset del dato a cui il nome stesso fa riferimento. Come già sappiamo, infatti, l'assembler tratta NomeSimbolico come un Disp16 attraverso il quale si può accedere al contenuto della corrispondente locazione di memoria; ogni volta che l'assembler incontra NomeSimbolico in una istruzione, calcola automaticamente il relativo Disp16 da inserire nel codice macchina dell'istruzione stessa.
Il NomeSimbolico è seguito da una DIRETTIVA che specifica l'ampiezza in bit del dato che stiamo creando; le direttive disponibili con le CPU 80x86 vengono illustrate dalla Figura 12.10. Attraverso le direttive di Figura 12.10, poste all'interno di un qualsiasi segmento di programma, possiamo chiedere all'assembler di creare locazioni di memoria di ampiezza specificata, riservate alle variabili statiche del nostro programma; tali locazioni di memoria sono destinate a contenere generici numeri binari compresi tra i limiti min. e max. specificati dalla stessa Figura 12.10.

Le direttive di Figura 12.10 sono in genere seguite da un valore immediato che prende il nome di valore inizializzante; consideriamo, ad esempio, la seguente linea posta all'interno di un segmento di programma chiamato DATASEGM:
Variabile1 DW 2F8Dh
Quando l'assembler incontra questa linea all'interno di DATASEGM, capisce che in quel preciso punto, cioè in quel preciso offset interno a DATASEGM, deve riservare uno spazio pari a 16 bit da riempire con il valore iniziale 2F8Dh. Nel momento in cui il programma viene caricato in memoria, in corrispondenza dell'offset interno a DATASEGM, individuato dal nome Variabile1, sarà presente una locazione da 16 bit contenente il valore iniziale 2F8Dh; questa locazione potrà quindi essere utilizzata nelle istruzioni, come operando di tipo Mem.
Ricordando le cose dette nel precedente capitolo, possiamo anche scrivere:
PesoNetto DW (2 + 181) - 4 + (6 * (10 / 5))
L'importante è che l'espressione inizializzante sia formata esclusivamente da valori immediati; questa espressione, infatti, deve essere risolta dall'assembler in fase di assemblaggio del programma.

Se non vogliamo specificare un valore iniziale, possiamo scrivere:
Variabile1 DW ?
In questo caso, il contenuto iniziale di Variabile1 è casuale o, come si dice in gergo, "sporco"; in sostanza, quando l'assembler incontra il simbolo '?' associato alla definizione di Variabile1, non effettua alcuna inizializzazione della corrispondente locazione di memoria.

Le direttive DF e DP vengono utilizzate in modalità protetta per creare indirizzi FAR Seg:Offset a 48 bit, con componente Seg a 16 bit e componente Offset a 32 bit; in modalità reale, queste due direttive definiscono semplicemente locazioni di memoria da 48 bit.

Osservando la Figura 12.10 possiamo notare che è possibile definire dati aventi una ampiezza in bit maggiore dell'architettura della CPU; anche con una 80386, ad esempio, possiamo scrivere:
Var64 DQ 3D668AC1FF283AB2h
Nell'ipotesi che Var64 si trovi all'offset 00F2h di un segmento di programma gestito con DS, si ottiene per questa variabile la disposizione in memoria mostrata in Figura 12.11. Naturalmente, in questo caso dobbiamo ricordarci che la 80386 può maneggiare via hardware numeri binari formati al massimo da 32 bit; di conseguenza, se vogliamo utilizzare Var64 in una istruzione, dobbiamo "spezzare" questo dato in tante parti da 8, da 16 o da 32 bit.
Utilizzando, ad esempio, gli address size operators mostrati nel precedente capitolo, possiamo scrivere istruzioni del tipo:
MOV EAX, DWORD PTR DS:Var64[0]
Osservando la Figura 12.11 possiamo notare che l'operando sorgente di questa istruzione fa chiaramente riferimento ai primi 32 bit della locazione di memoria che si trova all'offset:
00F2h + 0000h = 00F2h
In questo caso vengono quindi copiati in EAX i 32 bit meno significativi di Var64 (EAX=FF283AB2h).
Analogamente, possiamo scrivere l'istruzione:
MOV EBX, DWORD PTR DS:Var64[4]
Osservando la Figura 12.11 possiamo notare che l'operando sorgente di questa istruzione fa chiaramente riferimento ai primi 32 bit della locazione di memoria che si trova all'offset:
00F2h + 0004h = 00F6h
In questo caso vengono quindi copiati in EBX i 32 bit più significativi di Var64 (EBX=3D668AC1h).

12.2.1 Formati IEEE per i numeri in floating point

Gli assembler più evoluti, come MASM, permettono di utilizzare anche un numero reale come valore inizializzante; in questo caso, possono essere impiegate solo le tre direttive DD, DQ e DT. È possibile scrivere, ad esempio:
PiGreco DD 3.14
Quando l'assembler incontra una definizione del genere, converte il numero reale 3.14 in una apposita codifica binaria a 32 bit; a titolo di curiosità, il numero reale 3.14 viene codificato a 32 bit come 01000000010010001111010111000011b.
Il sistema di codifica binaria dei cosiddetti floating point numbers (numeri reali in virgola mobile), è stato stabilito dall'IEEE (Institute of Electrical and Electronics Engineers); la Figura 12.12 illustra i tre formati principali che tutti i linguaggi di programmazione forniscono per questi numeri e le relative caratteristiche (ampiezza in bit, limite inferiore, limite superiore, numero di cifre significative dopo la virgola). La Figura 12.12 mostra i limiti inferiore e superiore dei numeri reali positivi; per ottenere i corrispondenti limiti inferiore e superiore dei numeri reali negativi, basta mettere il segno meno davanti ai valori min. e max.

Per poter lavorare seriamente con i numeri reali, è necessario disporre di una apposita FPU o Fast Processing Unit (unità di elaborazione rapida); la FPU è in grado, infatti, di operare via hardware direttamente sui numeri reali, eseguendo su di essi, ad altissima velocità, complessi calcoli matematici che coinvolgono, funzioni logaritmiche, trigonometriche, esponenziali, etc.
La CPU, invece, opera esclusivamente su valori binari che codificano numeri interi con o senza segno; su questi numeri la CPU può solo eseguire operazioni elementari come addizioni, sottrazioni, negazioni, scorrimenti dei bit, etc. Tutto ciò significa che se definiamo il dato PiGreco mostrato in precedenza, la CPU lo tratterà come un normalissimo numero binario a 32 bit; se proprio vogliamo operare sui numeri reali con una semplice CPU, dobbiamo procedere via software scrivendo complesse procedure per la simulazione dei numeri in floating point.
Tutte le CPU 80486 DX e superiori, sono dotate di FPU incorporata; con le vecchie CPU, invece, era necessario acquistare a parte una apposita FPU che veniva identificata dalle sigle 8087, 80187, 80287, etc.

Tutti gli aspetti relativi alla FPU e ai numeri in virgola mobile, verranno analizzati nella sezione Assembly Avanzato.

12.2.2 Nuove direttive MASM per i dati statici

Le direttive mostrate in Figura 12.10 sono standard e sono riconosciute quindi da tutti gli assembler destinati al mondo delle CPU 80x86; se abbiamo la necessità di scrivere programmi compatibili con diversi assembler, o se abbiamo a disposizione un vecchio assembler, dobbiamo necessariamente servirci delle direttive standard di Figura 12.10.
Il MASM ha introdotto da tempo una serie di direttive alternative, che permettono di definire i vari formati di dati, utilizzando una sintassi molto simile a quella dei linguaggi di alto livello. La Figura 12.13 illustra la sintassi di queste nuove direttive MASM; per i floating point vengono mostrati in questa figura solo i limiti min. e max. positivi. Queste nuove direttive aiutano il programmatore a scrivere programmi più comprensibili, in quanto rendono evidente l'uso che vogliamo fare di un determinato dato; possiamo scrivere, ad esempio:
Dislivello SBYTE -75
È chiaro però che, indipendentemente dall'aspetto formale, la sostanza non cambia; dal punto di vista della CPU, infatti, Dislivello indica una locazione di memoria da 8 bit che contiene il valore binario iniziale 10110101b (cioè 181); questo valore è la rappresentazione a 8 bit in complemento a 2 del numero negativo -75.
Per indicare a chi legge il nostro programma, che vogliamo interpretare 10110101b come -75, utilizziamo la direttiva SBYTE; se, invece, vogliamo interpretare 10110101b come +181, possiamo utilizzare la direttiva BYTE. Questa distinzione quindi è puramente formale; la sostanza è che in entrambi i casi, il contenuto binario di Dislivello è 10110101b.
Lo stesso discorso vale naturalmente per i numeri reali definiti con le direttive REAL4, REAL8 e REAL10; questi numeri vengono visti dalla CPU come generici numeri binari costituiti da una sequenza rispettivamente di 32, 64 e 80 bit. A dimostrazione del fatto che l'unico tipo di dato gestibile dalla CPU è quello intero, possiamo tranquillamente sommare tra loro un DWORD con un REAL4 senza che l'assembler generi alcun messaggio di errore; per la CPU, infatti, sia il DWORD che il REAL4 sono generici numeri binari a 32 bit. Tutto ciò non sarebbe possibile con i linguaggi di alto livello che simulano via software uno stretto controllo sui tipi di dati; il linguaggio Pascal, ad esempio, impedisce al programmatore di effettuare una somma tra un Single (reale con segno a 32 bit) e un Longint (intero con segno a 32 bit).

In sostanza, possiamo dire che tutti i dati definiti con le direttive di Figura 12.13, possono essere tranquillamente definiti usando le direttive standard di Figura 12.10; queste direttive, essendo appunto standard, sono compatibili con tutti gli assembler disponibili sul mercato. Le direttive di Figura 12.13 influiscono solo sull'aspetto estetico dei programmi; molti programmatori preferiscono le direttive di Figura 12.10 perché rispecchiano meglio la filosofia della programmazione Assembly che non lascia molto spazio alle formalità.

12.2.3 La direttiva TYPEDEF

Gli assembler più recenti mettono a disposizione anche la direttiva TYPEDEF che permette di assegnare nuovi nomi (alias) alle direttive di Figura 12.13; in questo modo, è possibile definire i dati statici di un programma, con una sintassi molto simile (formalmente) a quella utilizzata con i linguaggi di alto livello. I fanatici del Borland Turbo Pascal, ad esempio, possono scrivere:
Integer TYPEDEF SWORD
creando in questo modo un alias Integer per la direttiva SWORD; a questo punto è possibile scrivere definizioni del tipo:
pascalVar Integer +12538

12.2.4 Base predefinita per i valori immediati

In assenza di diverse indicazioni da parte del programmatore, tutti i numeri espliciti (valori immediati, spiazzamenti, etc) presenti in un programma Assembly, si intendono espressi in base 10; ciò significa che nella seguente definizione:
Pressione DW 3800
alla variabile Pressione viene assegnato il valore iniziale 3800 espresso in base 10; se vogliamo indicare in modo esplicito la base numerica di un valore immediato, dobbiamo servirci dei suffissi mostrati in Figura 12.14. I suffissi possono essere espressi con una lettera maiuscola o minuscola; per la base ottale è preferibile utilizzare il suffisso Q in quanto la lettera O può essere confusa con uno zero.

Con questi suffissi, la precedente definizione può essere riscritta come:
Pressione DW 0000111011011000b
oppure:
Pressione DW 7330q
oppure:
Pressione DW 3800d
oppure:
Pressione DW 0ED8h
È anche possibile imporre una diversa base predefinita attraverso la direttiva:
.RADIX n
Il valore n indica la base predefinita che può essere 2, 8, 10 o 16.
Scrivendo, ad esempio, all'inizio di un programma:
.RADIX 16
allora tutti i numeri espliciti privi di suffisso si intendono espressi in base 16; di conseguenza, la precedente definizione:
Pressione DW 3800
verrebbe interpretata da MASM come:
Pressione DW 3800h
Se si impone una base predefinita 16, allora tutti i numeri espliciti espressi in una base diversa da 16 (compresi i numeri in base 10) devono specificare obbligatoriamente il suffisso; come si può facilmente intuire, questa situazione può creare parecchia confusione, per cui si raccomanda vivamente di lasciare 10 come base predefinita.

Nel caso dei numeri espliciti espressi in base 16, si può presentare un piccolo problema; consideriamo a tale proposito la seguente definizione:
VarHex DW F28Ch
Quando l'assembler incontra questa definizione, genera un messaggio di errore; il perché di questo messaggio di errore è abbastanza evidente. Il valore immediato F28Ch inizia con una lettera dell'alfabeto, per cui l'assembler confonde questo valore con un identificatore; se l'identificatore F28Ch non esiste, l'assembler genera appunto un messaggio di errore.
Per evitare questo problema, dobbiamo mettere uno zero prima della cifra più significativa di F28Ch; la precedente definizione diventa quindi:
VarHex DW 0F28Ch

12.3 Definizione dei dati statici complessi dell'Assembly

Dopo aver parlato dei formati elementari dei dati, che l'Assembly ci mette a disposizione, passiamo ad esaminare le direttive che ci permettono di definire i cosiddetti "aggregati" di dati; gli aggregati sono particolari strutture dati che possono essere trattate come un singolo dato complesso.
Cominciamo con le strutture propriamente dette, che possono essere create attraverso le direttive STRUC e UNION disponibili con MASM; si tenga presente che altri assembler (e le versioni più vecchie di MASM) non supportano queste direttive.

12.3.1 La direttiva STRUC

La direttiva STRUC permette di creare strutture dati equivalenti alle struct del C/C++ e ai record del Pascal; con le versioni più recenti di MASM, questa direttiva assume anche il nome STRUCT. Una struttura racchiude un insieme di dati di qualsiasi formato; la Figura 12.15 illustra la sintassi da utilizzare con questa direttiva. Come si può notare, una STRUC viene aperta da un nome simbolico seguito dalla direttiva STRUC; la struttura viene poi chiusa dallo stesso nome simbolico seguito dalla direttiva ENDS, proprio come accade per i segmenti di programma. In effetti, osservando la Figura 12.15 si può constatare che una STRUC è formalmente identica ad un segmento dati; al suo interno, infatti, possiamo inserire dati di qualsiasi formato (comprese le strutture stesse). Attraverso le dichiarazioni, stiamo creando in pratica nuovi formati di dati; i nomi attribuiti a questi nuovi formati di dati, possono essere usati come vere e proprie direttive. Nel caso di Figura 12.15, possiamo utilizzare il nome Struc1 come se fosse una delle direttive di Figura 12.10 o di Figura 12.13; di conseguenza, all'interno di un segmento di programma, possiamo scrivere, ad esempio:
varStruc1 Struc1 < 2AB1h, 3BF1854Dh, 884Ch, 2Fh, 5Ah >
In questo modo stiamo definendo ed inizializzando una struttura varStruc1 di tipo Struc1; come si può notare, la lista degli inizializzatori è racchiusa da una coppia di parentesi angolari. Ricorrendo alla terminologia dei linguaggi di programmazione ad oggetti, si può anche dire che varStruc1 è una istanza di Struc1.

È importante sottolineare ancora la differenza fondamentale che esiste tra una dichiarazione e una definizione; con la dichiarazione di Figura 12.15 stiamo semplicemente illustrando all'assembler le caratteristiche generali di un dato strutturato come Struc1. Attraverso, invece, la definizione di varStruc1, stiamo chiedendo all'assembler di riservare fisicamente una locazione di memoria di dimensioni sufficienti a contenere la stessa struttura varStruc1; quando l'assembler incontra questa definizione, crea in quel preciso punto del segmento di programma, una locazione di memoria da:
2 + 4 + 2 + 1 + 1 = 10 byte
Supponendo che varStruc1 si trovi all'offset 0028h di un segmento di programma, allora questa struttura verrà disposta in memoria secondo lo schema di Figura 12.16. Come accade per il linguaggio C, anche in Assembly gli elementi che fanno parte di una struttura vengono chiamati membri (nel Pascal si utilizza il termine campi); nel nostro caso, la struttura varStruc1 contiene i membri elencati in Figura 12.15 e in Figura 12.16.

Per accedere ai membri di una struttura, si utilizza la stessa sintassi dei linguaggi di alto livello; questa sintassi prevede l'uso dell'operatore '.' (punto) secondo la forma:
NomeStruttura.NomeMembro
NomeStruttura.NomeMembro rappresenta il contenuto della locazione di memoria che si trova all'offset risultante dalla somma:
Offset(NomeStruttura) + Offset(NomeMembro)
Mentre però Offset(NomeStruttura) viene calcolato rispetto al segmento di appartenenza della struttura, Offset(NomeMembro) viene, invece, calcolato rispetto ad Offset(NomeStruttura); in sostanza, è come se NomeMembro fosse un dato definito all'interno di un segmento chiamato NomeStruttura.
Osservando, ad esempio, la Figura 12.15 e la Figura 12.16, si vede subito che: Consideriamo, ad esempio, l'istruzione:
MOV AX, DS:varStruc1.varSt1
Osservando la Figura 12.16, si vede subito che l'operando sorgente di questa istruzione fa riferimento alla WORD che si trova all'offset:
0028h + 0000h = 0028h
del segmento di programma che contiene varStruc1; dopo il trasferimento dati si ottiene quindi AX=2AB1h.

Consideriamo l'istruzione:
MOV EBX, DS:varStruc1.varSt2
Osservando la Figura 12.16, si vede subito che l'operando sorgente di questa istruzione fa riferimento alla DWORD che si trova all'offset:
0028h + 0002h = 002Ah
del segmento di programma che contiene varStruc1; dopo il trasferimento dati si ottiene quindi EBX=3BF1854Dh.

Consideriamo l'istruzione:
MOV DX, WORD PTR DS:varStruc1.varSt2[2]
Osservando la Figura 12.16, si vede subito che l'operando sorgente di questa istruzione fa riferimento alla WORD che si trova all'offset:
0028h + 0002h + 0002h = 002Ch
del segmento di programma che contiene varStruc1; dopo il trasferimento dati si ottiene quindi DX=3BF1h (WORD più significativa di varSt2). In questo caso, l'operatore WORD PTR è necessario in quanto varSt2 è stata dichiarata di tipo DWORD.

Naturalmente, possiamo accedere a varStruc1 anche attraverso i registri puntatori; ponendo allora BX=0028h, SI=0002h e ricordando l'associazione predefinita tra BX e DS, l'istruzione dell'ultimo esempio può essere riscritta come:
mov dx, [bx+si+2]
MASM rende possibile la dichiarazione di strutture che contengono tra i loro membri altre strutture; in un caso del genere si parla di strutture innestate.
In Figura 12.17 vediamo un esempio che mostra una struttura che ha tra i suoi membri un'altra struttura. In questo esempio, dichiariamo innanzi tutto una struttura Point2d che è formata da due membri, x e y, entrambi di tipo WORD; questi due membri rappresentano le coordinate (ascissa e ordinata) di un punto del piano.
Successivamente dichiariamo una struttura Rect che è formata da due membri, p1 e p2, entrambi di tipo Point2d; i due punti p1 e p2 rappresentano i vertici, in alto a sinistra e in basso a destra, di un rettangolo.

Se ora vogliamo creare e inizializzare un'istanza di Rect, possiamo scrivere:
r1 Rect < < 3AB2h, 1C8Fh >, < 284Dh, 6FC5h > >
Come si può notare, abbiamo una coppia più esterna di parentesi angolari (per Rect), che incorpora due coppie interne di parentesi angolari (per i due Point2d).
Supponendo che r1 si trovi all'offset 00F6h di un segmento di programma chiamato DATASEGM, allora questa struttura verrà disposta in memoria secondo lo schema di Figura 12.18. Anche nel caso delle strutture innestate, l'accesso ai membri avviene con la stessa sintassi dei linguaggi di alto livello; questa sintassi assume la forma:
NomeStruttura.NomeStrutturaInnestata.NomeMembro
Come al solito, NomeStruttura.NomeStrutturaInnestata.NomeMembro rappresenta il contenuto della locazione di memoria che si trova all'offset risultante dalla somma:
Offset(NomeStruttura) + Offset(NomeStrutturaInnestata) + Offset(NomeMembro)
Osservando che r1.p1 rappresenta una struttura Point2d che contiene il vertice in alto a sinistra del rettangolo, allora r1.p1.x e r1.p1.y rappresentano le coordinate di questo stesso vertice; l'offset di p1 viene calcolato rispetto a r1, mentre gli offset di x e di y vengono calcolati rispetto a p1.
Analogamente, osservando che r1.p2 rappresenta una struttura Point2d che contiene il vertice in basso a destra del rettangolo, allora r1.p2.x e r1.p2.y rappresentano le coordinate di questo stesso vertice; l'offset di p2 viene calcolato rispetto a r1, mentre gli offset di x e di y vengono calcolati rispetto a p2.

Volendo inizializzare la struttura r1 nel blocco codice del programma, anziché con il metodo visto prima, possiamo scrivere le istruzioni mostrate in Figura 12.19; queste istruzioni rappresentano chiaramente dei trasferimenti di dati da Imm16 a Mem16. Osservando lo schema di Figura 12.18, si può constatare che:

r1.p1.x rappresenta il contenuto 3AB2h della locazione di memoria da 16 bit che si trova all'offset:
00F6h + 0000h + 0000h = 00F6h
r1.p1.y rappresenta il contenuto 1C8Fh della locazione di memoria da 16 bit che si trova all'offset:
00F6h + 0000h + 0002h = 00F8h
r1.p2.x. rappresenta il contenuto 284Dh della locazione di memoria da 16 bit che si trova all'offset:
00F6h + 0004h + 0000h = 00FAh
r1.p2.y rappresenta il contenuto 6FC5h della locazione di memoria da 16 bit che si trova all'offset:
00F6h + 0004h + 0002h = 00FCh
Una struttura innestata può avere tra i suoi membri ulteriori strutture innestate; gli innesti possono andare avanti sino all'esaurimento della memoria disponibile.

In MASM, i nomi dei membri di una struttura sono invisibili all'esterno della struttura stessa e quindi possono essere ridefiniti (ad esempio, come membri di altre strutture); in sostanza, nel caso del MASM l'identificatore r1.p1.x viene considerato distinto da p1 o da x.

12.3.2 La direttiva UNION

Passiamo ora alla direttiva UNION (unione) attraverso la quale possiamo creare aggregati di dati, molto simili alle strutture; le UNION dell'Assembly equivalgono alle union del C/C++ e ai record varianti del Pascal.
La differenza fondamentale che esiste tra una STRUC e una UNION sta nel fatto che tutti i membri di una UNION vengono sovrapposti tra loro in modo che condividano lo stesso offset iniziale in memoria; questo tipo di aggregato quindi è molto utile quando si ha bisogno di una variabile che in fase di esecuzione di un programma, assume formati diversi (BYTE, WORD, etc).
Come si può facilmente intuire, nel caso delle UNION possiamo inizializzare solo un membro per volta; la Figura 12.20 mostra un esempio di dichiarazione di una unione. Osserviamo che Union1 dichiara una UNION formata da un membro di tipo BYTE, uno di tipo WORD e uno di tipo DWORD; se ora definiamo in un segmento di programma una istanza VarUnion1 di Union1, l'assembler riserva a questa istanza uno spazio la cui ampiezza in bit è pari a quella del membro più grande di Union1. Come possiamo notare in Figura 12.20, il membro più grande di Union1 è varUn3 che ha una ampiezza di 32 bit; lo spazio da 32 bit riservato dall'assembler è sufficiente a contenere uno qualunque dei tre membri di Figura 12.20.
Per la definizione di varUnion1 possiamo utilizziare la sintassi generica:
varUnion1 Union1 < >
In questo caso stiamo dicendo all'assembler che per l'inizializzazione di varUnion1 utilizziamo i valori predefiniti di Figura 12.20; a questo punto, nel blocco codice del programma possiamo scrivere, ad esempio:
MOV DS:varUnion1.varUn2, 2BFFh
Abbiamo quindi inizializzato il membro varUn2 di tipo WORD; supponendo che varUnion1 si trovi all'offset 00D4h di un segmento di programma, allora questa UNION assumerà in memoria lo schema illustrato in Figura 12.21. Osserviamo subito che i tre membri di varUnion1 condividono tutti lo stesso offset iniziale 00D4h; siccome abbiamo inizializzato il membro varUn2 a 16 bit, solamente i primi 16 bit di varUnion1 sono significativi. In sostanza, subito dopo l'inizializzazione, solamente il membro varUnion1.varUn2 contiene un dato valido; durante la fase di esecuzione del programma, possiamo alterare in qualunque momento questa situazione scrivendo, ad esempio:
MOV DS:varUnion1.varUn1, 2Fh
Subito dopo l'esecuzione di questa istruzione, la locazione di Figura 12.21 assume l'aspetto mostrato in Figura 12.22. Da questo momento, solamente varUnion1+varUn1 contiene un dato valido; questa situazione permane sino alla prossima modifica di varUnion1.
Possiamo dire quindi che, durante la fase di esecuzione del programma, varUnion1 può essere utilizzata per contenere a scelta, un dato a 8 bit, oppure un dato a 16 bit, oppure un dato a 32 bit; chiaramente, è compito del programmatore tenere traccia del membro che è valido in un determinato momento.

Una UNION può contenere tra i suoi membri anche una o più STRUC; a sua volta, ogni STRUC innestata può contenere altre strutture innestate o anche altre unioni innestate. La gestione di questi innesti è tanto più complessa quanto più sono "contorti" gli innesti stessi; in ogni caso, raramente si ha bisogno di strutture dati così complicate.
La Figura 12.23 illustra un esempio di UNION che contiene tra i suoi membri anche una STRUC. La UNION chiamata Frutta può contenere un BYTE di nome Mela, oppure una WORD di nome Pera, oppure una DWORD di nome Ciliegia, oppure una struttura Agrumi di nome Altro; la struttura Agrumi a sua volta è formata da due DWORD e occupa quindi 64 bit.
In seguito a questa dichiarazione, possiamo utilizzare il nome Frutta per definire le istanze di questa UNION; nel segmento dati del nostro programma possiamo scrivere, ad esempio:
varFrutta1   Frutta < >
Quando l'assembler incontra questa definizione, riserva a varFrutta1 uno spazio sufficiente a contenere il membro più grande dell'unione Frutta; dalla Figura 12.23 si rileva che il membro più grande è Altro che occupa 64 bit.
Nel segmento di codice del nostro programma possiamo ora procedere con l'inizializzazione di uno dei membri di varFrutta1; supponendo di voler inizializzare per primo il membro Pera a 16 bit, possiamo scrivere, ad esempio:
MOV DS:varFrutta1.Pera, 8B21h
Nell'ipotesi che varFrutta1 si trovi all'offset 00C8h di un segmento di programma, allora questa UNION assumerà in memoria lo schema illustrato in Figura 12.24. Da questo momento, solamente varFrutta1.Pera contiene un dato valido; questa situazione permane sino alla prossima modifica di varFrutta1. Ad un certo punto della fase di esecuzione, possiamo decidere di alterare la situazione di Figura 12.24, inizializzando varFrutta1.Altro; possiamo scrivere, ad esempio:
MOV DS:varFrutta1.Altro.Arancio, 3FAB819Ch
e:
MOV DS:varFrutta1.Altro.Limone, 6DF934E1h
Dopo l'esecuzione di queste istruzioni, la locazione di memoria di Figura 12.24 assume l'aspetto mostrato in Figura 12.25. Da questo momento, solamente varFrutta1+Altro contiene un dato valido; questa situazione permane sino alla prossima modifica di varFrutta1.

Tutte le considerazioni appena illustrate per le UNION, sono valide anche per le STRUC; dagli esempi che sono stati presentati si può anche constatare che seguendo la logica, la gestione di questi aggregati complessi non presenta grosse difficoltà. Nei casi più contorti, possiamo semplificarci notevolmente la vita tracciando su un foglio di carta uno schema della locazione di memoria che contiene l'aggregato che vogliamo gestire; gli schemi come quelli di Figura 12.22 o di Figura 12.25, ci permettono anche di capire meglio il modo di lavorare della CPU.

12.3.3 La direttiva RECORD

Attraverso la direttiva RECORD, fornita dagli assembler come MASM, possiamo dichiarare una singola locazione di memoria contenente una sequenza di dati, ciascuno dei quali può avere una qualsiasi ampiezza in bit; questa direttiva si rivela molto utile nel momento in cui abbiamo la necessità di compattare nel più piccolo spazio possibile, una numerosa serie di informazioni.
Un esempio pratico è rappresentato dal registro FLAGS della CPU, dove ogni singolo bit ha un preciso significato; il registro FLAGS può essere visto quindi come un classico esempio di RECORD.

La sintassi da utilizzare per la dichiarazione di un RECORD è la seguente:
NomeRecord RECORD NomeMembro1: ampiezza1, NomeMembro2: ampiezza2, ...
Per analizzare un esempio pratico, supponiamo di voler definire un record varData1, destinato a contenere in forma compatta una data del calendario compresa tra 01/01/0000 e 31/12/2047; a tale proposito, suddividiamo il record in tre membri destinati a contenere, il giorno, il mese e l'anno.
Osserviamo che: Con MASM possiamo gestire il tutto attraverso la seguente dichiarazione:
RecData RECORD Giorno: 5, Mese: 4, Anno: 11
A questo punto, nel blocco dati del nostro programma possiamo creare una istanza di RecData attraverso la seguente definizione riferita alla data del 24/10/2003 (in binario 11000b/1010b/11111010011b):
varData1 RecData < 24, 10, 2003 >
Quando l'assembler incontra questa definizione, riserva uno spazio pari a 32 bit destinato a varData1; come è stato già anticipato, nel caso delle CPU 80286 e inferiori l'ampiezza di un RECORD può essere di 8 o 16 bit, mentre con le CPU 80386 e superiori si può avere anche una ampiezza di 32 bit.
L'assembler, eventualmente, incrementa la dimensione complessiva del RECORD, in modo da portarla a 8 bit, 16 bit o 32 bit; questo incremento viene ottenuto inserendo un numero adeguato di zeri alla sinistra del RECORD stesso. Nel caso di varData1, la dimensione complessiva è:
5 + 4 + 11 = 20 bit
Siccome questa dimensione è maggiore di 16 bit, l'assembler crea una locazione di memoria da 32 bit aggiungendo 12 zeri alla sinistra del RECORD; naturalmente, in questo caso dobbiamo disporre almeno di una CPU 80386.

Nell'ipotesi che varData1 si trovi in memoria all'offset 008Dh di un segmento di programma, si ottiene la situazione illustrata in Figura 12.26. Osserviamo che Anno occupa gli 11 bit meno significativi di varData1, mentre Giorno occupa i 5 bit più significativi; alla sinistra di Giorno troviamo, inoltre, 12 zeri aggiunti dall'assembler per riempire totalmente i 32 bit di questo dato.

Bisogna prestare particolare attenzione al fatto che, a differenza di quanto accade con le STRUC, i vari membri di un RECORD formano un unico numero binario; nel caso quindi del nostro esempio, Anno rappresenta la parte meno significativa di varData1, mentre Giorno rappresenta la parte più significativa. La definizione di varData1 crea quindi una locazione di memoria contenente il numero binario 11000101011111010011b; in pratica, è come se il programmatore avesse scritto:
varData1 DD 00000000000011000101011111010011b
Con i vecchi assembler, per l'accesso ad un record come varData1 si poteva utilizzare una sintassi del tipo varData1.Giorno, varData1.Mese, etc; le versioni più recenti di MASM non permettono però questa possibilità.
Del resto, osservando la Figura 12.26 si può facilmente constatare che non è possibile maneggiare i membri di un RECORD come si fa con le strutture; ciò è dovuto al fatto che la CPU non permette l'accesso a dati che non abbiano una ampiezza in bit multipla intera di 8 e che non siano almeno allineati al BYTE (come accade con i membri di varData1).
In definitiva, la direttiva RECORD si rivela utile solo per la definizione dei dati ma non per la loro gestione; nei capitoli successivi verranno illustrati gli operatori logici e, in particolare, le istruzioni logiche attraverso le quali è possibile accedere ai singoli bit di una locazione di memoria!

12.3.4 La direttiva DUP

Attraverso la direttiva DUP è possibile "replicare" un oggetto che può essere, un dato semplice, un aggregato di dati, o persino un'altra direttiva DUP seguita da un ulteriore oggetto da replicare; a differenza di quanto accade con STRUC, UNION e RECORD, che vengono usate nelle dichiarazioni, la direttiva DUP deve essere inserita in un segmento di programma in quanto comporta la definizione di un aggregato di dati, con conseguente allocazione della memoria.

La sintassi generale per DUP è la seguente:
NomeSimbolico DIRETTIVA Repliche DUP ( Oggetto )
La DIRETTIVA è una di quelle illustrate in Figura 12.10 e in Figura 12.13, oppure il nome simbolico di una STRUC, UNION, RECORD, etc; il valore immediato Repliche indica il numero di oggetti da replicare.

Gli oggetti replicati con DUP vengono disposti in memoria in modo consecutivo e contiguo; indicando con Dimensione(Oggetto) la dimensione in byte di Oggetto, possiamo affermare allora che la memoria complessiva da allocare è pari al prodotto:
Repliche * Dimensione(Oggetto)
Consideriamo il seguente esempio:
VettWord1 DW 4 DUP ( 03FDh )
Quando l'assembler incontra questa definizione, riserva uno spazio pari a 4 word, per un totale di 4*2=8 byte; in questo spazio vengono sistemate 4 WORD, ciascuna delle quali viene inizializzata con il valore 03FDh.
Nell'ipotesi che VettWord1 si trovi all'offset 00F2h di un segmento di programma, si ottiene per questa variabile la disposizione in memoria mostrata in Figura 12.27. Dalla Figura 12.27 si rileva chiaramente che: Caricando in BX l'offset 00F2h e ricordando l'associazione predefinita tra BX e DS, possiamo anche dire che: Una sequenza consecutiva e contigua di oggetti, tutti della stessa natura, viene chiamata vettore; ogni oggetto appartenente ad un vettore prende il nome di elemento del vettore. Passiamo ora ad un esempio più impegnativo; in riferimento alla struttura Rect dichiarata in Figura 12.17, consideriamo la seguente definizione:
VettRect1 Rect 10 DUP ( < > )
Davanti a questa definizione, l'assembler non si spaventa per niente e crea uno spazio sufficiente per contenere 10 strutture Rect; ogni Rect occupa 8 byte, per cui la memoria totale allocata dall'assembler è pari a 10*8=80 byte.
Supponendo che VettRect1 si trovi all'offset 00F6h di un segmento di programma, allora questo vettore di oggetti Rect verrà disposto in memoria secondo lo schema di Figura 12.28; in questa figura, per ovvie ragioni di spazio, vengono mostrati solamente i primi due Rect del vettore. Analizzando la Figura 12.28 rileviamo che il Rect di indice 0 è individuato da VettRect1[0]; di conseguenza:

VettRect1[0].p1 rappresenta il Point2d che si trova all'offset:
00F6h + 0000h + 0000h = 00F6h
VettRect1[0].p1.x rappresenta la WORD che si trova all'offset:
00F6h + 0000h + 0000h + 0000h = 00F6h
VettRect1[0].p1.y rappresenta la WORD che si trova all'offset:
00F6h + 0000h + 0000h + 0002h = 00F8h
Analogamente:

VettRect1[0].p2 rappresenta il Point2d che si trova all'offset:
00F6h + 0000h + 0004h = 00FAh
VettRect1[0].p2.x rappresenta la WORD che si trova all'offset:
00F6h + 0000h + 0004h + 0000h = 00FAh
VettRect1[0].p2.y rappresenta la WORD che si trova all'offset:
00F6h + 0000h + 0004h + 0002h = 00FCh
Sempre dalla Figura 12.28 rileviamo che ogni Rect occupa 8 byte, per cui il Rect di indice 1 (cioè, il secondo elemento del vettore) è individuato da VettRect1[8]; di conseguenza:

VettRect1[8].p1 rappresenta il Point2d che si trova all'offset:
00F6h + 0008h + 0000h = 00FEh
VettRect1[8].p1.x rappresenta la WORD che si trova all'offset:
00F6h + 0008h + 0000h + 0000h = 00FEh
VettRect1[8].p1.y rappresenta la WORD che si trova all'offset:
00F6h + 0008h + 0000h + 0002h = 0100h
Analogamente:

VettRect1[8].p2 rappresenta il Point2d che si trova all'offset:
00F6h + 0008h + 0004h = 0102h
VettRect1[8].p2.x rappresenta la WORD che si trova all'offset:
00F6h + 0008h + 0004h + 0000h = 0102h
VettRect1[8].p2.y rappresenta la WORD che si trova all'offset:
00F6h + 0008h + 0004h + 0002h = 0104h
Per accedere agli altri Rect si utilizza lo stesso meccanismo; infatti, il Rect di indice 2 è individuato da VettRect1[16], il Rect di indice 3 è individuato da VettRect1[24] e così via.

Volendo utilizzare i registri puntatori, possiamo porre BX=00F6h; a questo punto osserviamo che: Volendo trasferire, ad esempio, in AX il contenuto del membro p2.y del Rect di indice 3, dobbiamo scrivere l'istruzione:
mov ax, [bx+(3*8)+4+2]
L'espressione (3*8)+4+2 non comporta alcuna perdita di tempo in quanto viene risolta dall'assembler in fase di assemblaggio del programma; alla fine viene generato un codice macchina contenente un semplice effective address del tipo [BX+Disp16].

Come è stato detto in precedenza, l'oggetto replicato da DUP può essere persino un'altra direttiva DUP seguita da un ulteriore oggetto da replicare; possiamo scrivere, ad esempio:
MatrWord1 DW 5 DUP (8 DUP (72F8h))
In questo modo stiamo chiedendo all'assembler di replicare 5 oggetti, ciascuno dei quali è un vettore di 8 WORD che valgono tutte 72F8h; complessivamente abbiamo bisogno quindi di 5*8=40 word, per un totale di 40*2=80 byte.
Un "vettore di vettori" prende il nome di matrice rettangolare o vettore bidimensionale; ogni oggetto della matrice prende il nome di elemento della matrice.
La matrice MatrWord1 può essere schematizzata simbolicamente come in Figura 12.29. Naturalmente, MatrWord1 viene disposta in memoria, non come in Figura 12.29, ma come una sequenza di 5*8=40 WORD consecutive e contigue; il programmatore Assembly è libero di gestire come meglio crede questa disposizione. Diversi linguaggi di alto livello dispongono in memoria le matrici, come una sequenza consecutiva e contigua di righe; in questo caso si dice che la matrice è row ordered (ordinata per righe). Altri linguaggi, invece, dispongono in memoria le matrici, come una sequenza consecutiva e contigua di colonne; in questo caso si dice che la matrice è column ordered (ordinata per colonne).

Tornando a MatrWord1, possiamo osservare che i vari elementi di questa matrice sono individuati dalla sequenza:
MatrWord1[0], MatrWord1[2], MatrWord1[4], ...
Se abbiamo tracciato su un foglio di carta uno schema come quello di Figura 12.29, si presenta il problema di come risalire all'indirizzo di memoria dell'elemento che si trova all'incrocio tra un certo indice di riga e un certo indice di colonna; in sostanza, dato l'elemento di Figura 12.29 che si trova all'incrocio tra la riga i e la colonna j, vogliamo determinare lo spiazzamento del corrispondente elemento che si trova in memoria.
Indicando con Dimensione(elemento) la dimensione in byte di ciascun elemento della matrice, la formula da utilizzare è la seguente:
Spiazzamento = ((i * numero_colonne) + j) * Dimensione(elemento)
Naturalmente, lo Spiazzamento così calcolato deve essere sommato all'offset da cui inizia MatrWord1 in memoria; ad esempio, l'elemento che si trova all'incrocio tra la riga i=3 e la colonna j=4, corrisponde a:
MatrWord1[((3*8)+4)*2] = MatrWord1[56]
La direttiva DUP può essere utilizzata anche come membro di una STRUC o di una UNION; la Figura 12.30 mostra un esempio pratico che fa riferimento al RECORD RecData definito in precedenza. Questa struttura è formata da 3 word e da un vettore di 10 RecData; ogni RecData occupa 32 bit (4 byte), per cui la dimensione totale di Automobile è pari a:
2 + 2 + 2 + (10 * 4) = 6 + 40 = 46 byte
In un segmento di programma possiamo ora creare delle istanze di Automobile. La seguente definizione:
FerrariF3000 Automobile 20 DUP ( < > )
crea un vettore di 20 strutture Automobile; questo vettore richiede quindi 46*20=920 byte di memoria.
I vari elementi del vettore FerrariF3000 sono: In relazione all'elemento FerrariF3000[46] e tenendo presente che ogni RecData occupa 4 byte, possiamo dire che i vari elementi del vettore Revisioni sono: Volendo inserire in FerrariF3000[46].Revisioni[0] la data 12/03/2008 (in binario 01100b/0011b/11111011000b), possiamo scrivere l'istruzione:
MOV DS:FerrariF3000[46].Revisioni[0], 01100001111111011000b
Osserviamo che ogni elemento Revisioni[i] è una DWORD; di conseguenza, l'assembler è in grado di rilevare che questa istruzione rappresenta un trasferimento dati da Imm32 a Mem32.

Utilizzando numerose direttive DUP innestate, si possono ottenere vettori tridimensionali, quadridimensionali, etc; tutto ciò si applica non solo a oggetti semplici, ma anche a oggetti di tipo struttura, unione, etc. In questo modo si ottengono aggregati di dati particolarmente complessi; la dimensione complessiva di ciascuno di questi aggregati non deve superare 65536 byte.

12.3.5 Creazione diretta di vettori monodimensionali e multidimensionali

Tutte le definizioni effettuate con la direttiva DUP possono essere ottenute anche in modo diretto; ciò è possibile in quanto, in fase di definizione di un dato, l'assembler ci permette di specificare una lista formata da uno o più inizializzatori separati da virgole.
Ad esempio, la definizione:
VettWord1 DW 8 DUP ( 03BCh )
equivale a:
VettWord1 dw 03BCh, 03BCh, 03BCh, 03BCh, 03BCh, 03BCh, 03BCh, 03BCh
Anche in questo caso quindi, l'assembler crea una locazione di memoria da 8 word, nella quale vengono disposte in modo consecutivo e contiguo, 8 WORD inizializzate tutte con il valore fisso 03BCh.
Il vantaggio del metodo diretto sta nel fatto che possiamo specificare una lista di inizializzatori, tutti diversi tra loro; possiamo scrivere, ad esempio:
VettWord1 dw 2800, 3500, 1890, 3961, 8767, 9945, 1998, 6780
Se vogliamo simulare una matrice da 4*10 BYTE, possiamo ricorrere alla direttiva DUP scrivendo:
MatrByte1 DB 4 DUP ( 10 DUP ( 0 ) )
Volendo utilizzare il metodo diretto, possiamo assegnare un valore diverso ad ogni elemento della matrice; in questo modo si ottiene la situazione mostrata in Figura 12.31 (ricordiamo che i nomi simbolici da assegnare ai dati sono facoltativi). Questa matrice è chiaramente ordinata per righe; se vogliamo ordinare la matrice per colonne, dobbiamo definirla come in Figura 12.32 (matrice da 10*4 BYTE). Chiaramente, in entrambi i casi l'assembler crea una locazione di memoria da 40 byte; all'interno di questo spazio vengono sistemati in modo consecutivo e contiguo, 40 elementi di tipo BYTE.

In riferimento alla struttura Rect dichiarata in Figura 12.17, abbiamo visto che è possibile scrivere:
VettRect1 Rect 10 DUP ( < > )
Di conseguenza, possiamo anche scrivere:
VettRect1 Rect < >, < >, < >, < >, < >, < >, < >, < >, < >, < >
Volendo creare un vettore bidimensionale di 4*3 oggetti di tipo Rect, possiamo scrivere:
MatrRect1 Rect 4 DUP ( 3 DUP ( < > ) )
Di conseguenza, possiamo anche utilizzare il metodo diretto illustrato in Figura 12.33. Tutte le considerazioni appena svolte si applicano anche all'oggetto associato ad una direttiva DUP; consideriamo, ad esempio, la seguente definizione:
MatrRect1 Rect 4 DUP ( < >, < >, < > )
Questa definizione replica 4 volte un vettore formato da 3 oggetti di tipo Rect; alla fine si ottiene la stessa situazione di Figura 12.33.

12.3.6 Caratteri e stringhe alfanumeriche

Come già sappiamo, un generico simbolo appartenente al set ASCII, viene codificato attraverso un numero binario a 8 bit; la lettera A, ad esempio, viene codificata come 01000001b (65). Il metodo più banale per definire un dato contenente il codice ASCII della lettera A, consiste allora nello scrivere:
AsciiSymbol db 65
L'Assembly però ci permette di utilizzare una sintassi molto più chiara ed elegante; possiamo scrivere, infatti:
AsciiSymbol db 'A'
Questa istruzione è assolutamente equivalente a quella precedente; l'assembler, infatti, provvede a convertire 'A' nel valore 65 a 8 bit.
Al posto degli apici singoli si possono utilizzare anche i doppi apici ("A").

In base a queste considerazioni, possiamo facilmente definire intere stringhe; si definisce stringa ASCII, un vettore di codici ASCII. Possiamo scrivere allora:
asmString db 'A', 's', 's', 'e', 'm', 'b', 'l', 'y'
Questa istruzione è perfettamente equivalente a:
asmString db 65, 115, 115, 101, 109, 98, 108, 121
Anche in questo caso, l'Assembly ci permette di utilizzare una sintassi molto più chiara ed elegante; possiamo scrivere, infatti:
asmString db 'Assembly'
Quando l'assembler incontra questa definizione, crea un vettore di 8 byte e lo riempie con gli 8 codici ASCII dei simboli specificati da asmString.

Se all'interno della stringa è presente un apostrofo, possiamo servirci dei doppi apici scrivendo:
asmString db "L'Assembly"
Alternativamente, osservando che l'apostrofo corrisponde al codice ASCII 39, possiamo scrivere:
asmString db 'L', 39, 'Assembly'
Osservando che ogni codice ASCII occupa 8 bit, è anche possibile scrivere:
wordString dw 'ab'
oppure:
dwordString dd 'abcd'
e così via.

Grazie alle stringhe, possiamo dichiarare strutture dati come quella mostrata in Figura 12.34. Come è stato spiegato in un precedente capitolo, i codici ASCII sono numeri a 8 bit che permettono di rappresentare solo 28=256 simboli, appartenenti principalmente alla cultura occidentale; con la diffusione planetaria dei computer e con la loro interconnessione attraverso Internet, chiaramente 256 simboli non sono più sufficienti.
Per questo motivo, è stato definito un nuovo set di codici, chiamato UNICODE; in questo caso vengono utilizzati codici a 16 bit che permettono quindi di rappresentare 216=65536 simboli differenti comprendenti, tra l'altro, i simboli dell'alfabeto cirillico, dell'alfabeto cinese, giapponese, arabo, indiano, etc. Per motivi di compatibilità, i primi 128 codici UNICODE coincidono con i primi 128 codici ASCII.
Le stringhe UNICODE quindi, a differenza delle stringhe ASCII, sono vettori di WORD; se si sta usando un editor che supporta i caratteri UNICODE, la stringa:
unicodeStr dw 0910h, 0920h, 0930h, 0940h, 0950h, 0960h
verrebbe visualizzata in caratteri Devanagari come:
ऐ ठ र ी ॐ ॠ
All'indirizzo Unicode Book si può scaricare gratuitamente una applicazione Windows che permette di visualizzare l'elenco più recente di tutti i simboli codificati in formato UNICODE.
In ambiente Linux sono presenti ottime applicazioni come gucharmap e KCharSelect.

12.4 Creazione di un Data Segment

Tutto ciò che è stato esposto nelle precedenti sezioni, può essere utilizzato per creare segmenti di programma e per definire al loro interno i dati statici di cui abbiamo bisogno; nel caso più semplice, possiamo creare segmenti destinati esclusivamente ai dati statici. In questo modo otteniamo dei cosiddetti Data Segments (segmenti di dati); la Figura 12.35 illustra un esempio pratico che fa riferimento anche ai vari aggregati di dati dichiarati in questo capitolo. In questo esempio abbiamo creato un segmento dati individuato dal nome simbolico DATASEGM; il segmento viene indirizzato con offset a 16 bit (USE16), ed è dotato di allineamento PARA, combinazione PUBLIC e classe 'DATA'.

Osserviamo che per inizializzare i dati di tipo floating point, possiamo anche ricorrere a numeri espressi in notazione esponenziale; questo significa che:
-5.12345E-200 = -5.12345 * (10-200)
Osserviamo, inoltre, che l'esempio di Figura 12.35 assume che la base numerica predefinita sia 10; di conseguenza, tutti i numeri espliciti privi di suffisso, si intendono espressi in base 10.

Una volta che abbiamo definito i dati statici del nostro programma, possiamo impiegarli nelle varie istruzioni, riferendoci ad essi tramite il loro nome simbolico, oppure tramite il loro indirizzo; nel primo caso si parla di accesso ai dati per nome, mentre nel secondo caso si parla di accesso ai dati per indirizzo.
In base a quanto abbiamo visto nei precedenti capitoli possiamo dire che, in ogni caso, l'accesso ad un dato statico di un programma avviene sempre tramite il suo indirizzo logico Seg:Offset; per rendercene conto, consideriamo la seguente istruzione che si riferisce all'accesso per nome ad un dato definito in Figura 12.35:
mov cx, ds:varRect.p1.y
Come possiamo notare, l'operando sorgente è una locazione di memoria che si trova all'indirizzo logico Seg:Offset; in questo caso, la componente Seg è contenuta in DS, mentre la componente Offset è un semplice Disp16 rappresentato dalla WORD chiamata simbolicamente varRect.p1.y.
Il vantaggio che deriva dall'uso dei nomi simbolici, sta nel fatto che in questo modo stiamo delegando all'assembler il compito di calcolare il corretto offset del dato al quale vogliamo accedere; in presenza, infatti, del nome varRect.p1.y, l'assembler analizza il DATASEGM di Figura 12.35 per ricavare il corrispondente offset.
Come abbiamo visto nel precedente capitolo, quando utilizziamo in una istruzione il nome simbolico di un dato (cioè, un offset esplicito), dobbiamo indicare anche il registro di segmento contenente la componente Seg dell'indirizzo logico del dato stesso; in questo modo, possiamo far capire all'assembler che il nome simbolico rappresenta una locazione di memoria e non un generico valore numerico.
Appare anche evidente il fatto che il programmatore è tenuto rigorosamente ad inizializzare i registri di segmento che vuole impiegare per referenziare i segmenti di dati dei propri programmi; in caso contrario, l'istruzione del precedente esempio trasferisce in CX un valore privo di senso!

Volendo accedere per indirizzo a varRect.p1.y, possiamo scrivere una istruzione del tipo:
mov cx, ds:[si]
Questa volta la CPU è in grado di associare automaticamente SI a DS, per cui possiamo anche scrivere:
mov cx, [si]
Anche in questo caso notiamo che l'operando sorgente indica una locazione di memoria a 16 bit che si trova all'indirizzo logico Seg:Offset (la presenza di CX indica all'assembler che gli operandi sono a 16 bit); la componente Seg è contenuta in DS, mentre la componente Offset è contenuta in SI.
Questa volta, il programmatore è tenuto rigorosamente ad inizializzare, non solo DS, ma anche SI; infatti, affinché la precedente istruzione fornisca il risultato desiderato, il registro DS deve contenere la componente Seg assegnata a DATASEGM, mentre SI deve contenere la componente Offset assegnata a varRect.p1.y.

In base alle considerazioni appena esposte, è necessario ribadire che il programmatore ha l'importante compito di inizializzare i registri di segmento che intende utilizzare per referenziare i segmenti di dati dei propri programmi; ricordiamo inoltre che il registro di segmento naturale per i dati è DS. In caso di necessità possiamo anche servirci di ES, FS e GS; inoltre, se vogliamo accedere a dati statici definiti nel segmento di stack o nei segmenti di codice, dobbiamo ricordarci di specificare il segment override per SS o CS.
Nel prossimo capitolo analizzeremo in dettaglio tutti gli aspetti relativi al calcolo degli offset dei dati statici di un programma e al loro corretto allineamento in memoria.

12.5 Creazione di uno Stack Segment

Anche in relazione alle variabili temporanee di un programma, la cosa migliore da fare consiste nel creare un apposito segmento da destinare esclusivamente a questo tipo di informazioni; in questo caso si ottiene un cosiddetto Stack Segment (segmento di stack).
Come già sappiamo, nel momento in cui inizia la fase di esecuzione di un programma, la CPU si aspetta che la coppia SS:SP sia già stata predisposta in modo da referenziare lo stack; in particolare, SS deve contenere la componente Seg dell'indirizzo logico normalizzato da cui inizia lo stack, mentre SP deve indicare inizialmente la massima ampiezza in byte dello stesso stack.
A seconda dei casi, il compito di inizializzare la coppia SS:SP può anche ricadere sul programmatore; se vogliamo evitare questa eventualità, possiamo servirci dell'attributo di combinazione STACK illustrato nella sezione 12.1.
Abbiamo visto, infatti, che il linker utilizza automaticamente un eventuale segmento di programma con attributo di combinazione STACK, per inizializzare la coppia SS:SP; supponendo, ad esempio, di avere bisogno di uno stack da 1024 byte (0400h byte), possiamo definire lo stack segment mostrato in Figura 12.36. In questo esempio abbiamo creato un segmento di stack individuato dal nome simbolico STACKSEGM; il segmento viene indirizzato con offset a 16 bit (USE16) ed è dotato di allineamento PARA, combinazione STACK e classe 'STACK'.
Incontrando questo segmento di programma, il linker pone automaticamente:
SS = STACKSEGM
e:
SP = 0400h
Osserviamo che all'interno dello stack segment è presente un vettore da 1024 byte; il nome simbolico per questo vettore non è necessario in quanto la CPU indirizza lo stack attraverso la coppia SS:SP. Non è necessaria nemmeno l'inizializzazione del vettore di stack; quest'area, infatti, viene continuamente modificata dalla CPU durante la fase di esecuzione del programma.

Se lo stack viene creato all'interno di un segmento di programma privo dell'attributo STACK, allora il compito di inizializzare la coppia SS:SP ricade ovviamente sul programmatore; questo caso viene analizzato più avanti.

In rarissime circostanze che verranno illustrate nei capitoli successivi, il programmatore ha la responsabilità di modificare in fase di esecuzione, il contenuto del registro SP; nel caso generale, invece, il contenuto della coppia SS:SP viene gestito dalla CPU e, per ovvie ragioni, non deve essere assolutamente modificato dal programmatore. Se abbiamo la necessità di "esplorare" lo stack, possiamo servirci dell'apposito registro puntatore BP; ricordiamo anche che la CPU associa automaticamente SP e BP al registro di segmento SS.

Nei precedenti capitoli abbiamo visto che se vogliamo spingere al massimo le prestazioni di un programma, dobbiamo affrontare con molta attenzione il problema del corretto allineamento in memoria delle informazioni che fanno parte del programma stesso; questo aspetto assume una importanza ancora maggiore nel caso dello stack segment. Un programma in esecuzione, infatti, può avere l'esigenza di sfruttare lo stack in modo veramente intensivo; nei capitoli successivi vedremo inoltre che anche il SO usa lo stack dei programmi per poter svolgere numerosi compiti.
Tutto ciò che riguarda il corretto allineamento in memoria del segmento di stack e del registro SP, verrà trattato in dettaglio nel prossimo capitolo.

12.6 Creazione di un Code Segment

Dopo aver definito il Data Segment e lo Stack Segment di un programma, non ci resta che definire appositi segmenti destinati a contenere le istruzioni per l'elaborazione dei dati statici e dinamici; nel caso più semplice, possiamo creare un segmento riservato unicamente alle istruzioni, ottenendo in questo modo un cosiddetto Code Segment.
All'interno del segmento di codice vengono inserite, in particolare, un gruppo di istruzioni che nel loro insieme formano il main program (programma principale o codice principale); dal codice principale è possibile saltare ad altre istruzioni che spesso si trovano in sottoprogrammi disposti in uno o più moduli esterni.
Analizziamo ora gli aspetti fondamentali che caratterizzano un segmento di programma destinato a contenere, in particolare, il codice principale.

12.6.1 L'entry point

Un qualunque programma, scritto in qualunque linguaggio, deve fornire alla CPU una informazione fondamentale che prende il nome di entry point (punto di ingresso); l'entry point rappresenta in pratica l'indirizzo Seg:Offset della prima istruzione che verrà eseguita dalla CPU. Nei linguaggi di alto livello, l'entry point viene gestito direttamente dal compilatore o dall'interprete; in Assembly, invece, l'entry point deve essere specificato dal programmatore.
Come si può facilmente intuire, il linker utilizza proprio l'entry point per inizializzare la coppia CS:IP; in sostanza, non appena inizia la fase di esecuzione di un programma, la coppia CS:IP punta alla prima istruzione che verrà eseguita dalla CPU. Il compito di inizializzare la coppia CS:IP spetta quindi rigorosamente al linker; in fase di esecuzione, il contenuto di CS:IP viene gestito dalla CPU e non deve essere assolutamente modificato dal programmatore.

Il metodo più semplice per indicare l'entry point di un programma, consiste nel definire una cosiddetta label (etichetta); una label è formata da un nome simbolico, seguito da un ':' (due punti). All'interno di un qualunque segmento di programma possiamo scrivere, ad esempio:
esempio_di_etichetta:
Una label ci permette di individuare facilmente un indirizzo posto all'interno di un segmento di programma; possiamo paragonare la label ad una sorta di segnalibro che ci aiuta a ritrovare rapidamente una determinata pagina che stavamo leggendo. Da queste considerazioni si deduce che una label ha il semplice scopo di indicare una posizione all'interno di un segmento di programma; il programmatore può quindi definire tutte le label di cui ha bisogno, senza il rischio di sprecare memoria.

Per indicare all'assembler che una determinata label rappresenta l'entry point del nostro programma, dobbiamo utilizzare una apposita direttiva che viene illustrata più avanti; un programma Assembly deve specificare obbligatoriamente un unico entry point.

12.6.2 La direttiva ASSUME

Abbiamo visto che ogni volta che in una istruzione facciamo riferimento al nome simbolico (offset esplicito) di un dato del nostro programma, siamo tenuti a specificare anche il registro di segmento contenente la componente Seg dell'indirizzo logico del dato stesso; infatti, a differenza di quanto accade con i registri puntatori, l'assembler non è in grado di associare automaticamente un nome simbolico ad un registro di segmento. Nei programmi molto grossi, questa situazione può risultare abbastanza fastidiosa; per il programmatore esiste anche il rischio di incappare in una svista che porta ad indicare un registro di segmento sbagliato.
Supponiamo, ad esempio, di voler accedere per nome ai dati statici definiti nel segmento DATASEGM di Figura 12.35; se decidiamo di impiegare DS per gestire questo segmento, ogni volta che in una istruzione è presente il nome di uno dei dati di Figura 12.35, dobbiamo indicare anche lo stesso DS come registro di segmento predefinito. Per delegare all'assembler questo fastidioso compito, possiamo servirci della direttiva ASSUME; la sintassi da utilizzare è la seguente:
ASSUME SegReg: NomeSegmento1, SegReg: NomeSegmento2, ...
Nel nostro caso, all'interno del segmento di codice di un programma possiamo scrivere:
ASSUME DS: DATASEGM
A questo punto possiamo scrivere istruzioni del tipo:
MOV AX, varWord
Quando l'assembler incontra una istruzione di questo genere, grazie alla precedente direttiva ASSUME è in grado di sapere che il dato varWord definito nel segmento DATASEGM, deve essere associato a DS; di conseguenza, nel codice macchina di questa istruzione, l'assembler non inserisce alcun segment override (infatti, DS è il registro di segmento naturale per i dati).
Se, invece, vogliamo gestire DATASEGM attraverso ES, possiamo scrivere:
ASSUME ES: DATASEGM
Anche in questo caso, possiamo scrivere istruzioni del tipo:
MOV AX, varWord
Quando l'assembler incontra una istruzione di questo genere, grazie alla precedente direttiva ASSUME è in grado di sapere che il dato varWord definito nel segmento DATASEGM, deve essere associato a ES; di conseguenza, nel codice macchina di questa istruzione, l'assembler inserisce il segment override 26h relativo al registro ES.

Il problema appena descritto, riguarda anche i nomi simbolici definiti all'interno di un segmento di codice; questi nomi possono appartenere a dati statici, etichette, sottoprogrammi, etc.
In questo caso, siamo obbligati ogni volta ad indicare all'assembler che la componente Seg dell'indirizzo logico di un determinato nome simbolico, è contenuta in CS; per evitare questo fastidio, possiamo servirci anche in questo caso della direttiva ASSUME. All'inizio di un segmento di programma chiamato, ad esempio, CODESEGM, possiamo scrivere:
ASSUME CS: CODESEGM
Nel caso dei segmenti di codice, la direttiva ASSUME svolge anche un altro importante ruolo; per capire in cosa consiste questo ruolo, vediamo quello che succede quando la CPU incontra una istruzione che prevede un salto (jump) ad un determinato indirizzo Seg:Offset.
Per poter effettuare questo salto, la CPU carica in CS:IP l'indirizzo Seg:Offset di destinazione; si possono presentare allora le due possibilità seguenti: Nel caso a, la CPU deve modificare solo IP in quanto CS rimane inalterato; questo salto viene definito NEAR jump (salto vicino).
Nel caso b, la CPU deve modificare, sia CS, sia IP; questo salto viene definito FAR jump (salto lontano).
Questi due tipi di salto corrispondono a due diversi codici macchina; grazie alle direttive ASSUME che associano CS ai vari code segment del nostro programma, l'assembler è in grado di sapere se, per una istruzione di salto, deve generare il codice macchina di un NEAR jump o di un FAR jump.
Proprio per i motivi appena illustrati, si raccomanda vivamente di inserire una direttiva:
ASSUME CS: NomeSegmento
all'inizio di ogni segmento di codice chiamato NomeSegmento.

Nel momento in cui decidiamo di far cessare l'associazione tra un SegReg ed un segmento di programma, possiamo ricorrere ancora alla direttiva ASSUME; per dissociare, ad esempio, DS da DATASEGM, possiamo scrivere:
ASSUME DS: NOTHING
Il termine inglese NOTHING significa niente; da questo momento in poi, l'assembler non associa più DS ai nomi simbolici dei dati definiti in DATASEGM.

La direttiva ASSUME è molto comoda, ma deve essere utilizzata con molta attenzione; se il programmatore non ha le idee chiare su ciò che deve fare, può andare incontro ad errori particolarmente difficili da scovare!

12.6.3 Inizializzazione dei registri di segmento per i dati

Una direttiva come:
ASSUME DS: DATASEGM
dice semplicemente all'assembler che tutti i nomi simbolici dei dati definiti in DATASEGM, rappresentano delle componenti Offset da associare ad una componente Seg contenuta in DS; è chiaro però che questa direttiva da sola non serve a niente. È anche necessario che il programmatore provveda ad inizializzare DS con la componente Seg assegnata a DATASEGM; come già sappiamo, questa informazione è rappresentata dallo stesso nome simbolico DATASEGM.
Prima di accedere a qualunque dato presente in DATASEGM, dobbiamo quindi ricordarci di inizializzare DS; possiamo usare a tale proposito le seguenti istruzioni: È proibito trasferire direttamente un valore immediato come DATASEGM in un SegReg; siamo costretti quindi ad effettuare un passaggio intermedio attraverso il registro AX. Nei limiti del possibile, in casi di questo genere conviene sempre utilizzare l'accumulatore; in questo modo otteniamo un codice macchina più compatto e più veloce.
Naturalmente, lo stesso tipo di inizializzazione deve essere effettuato, se necessario, anche per ES; ricordiamo poi che con le CPU 80386 e superiori, possiamo gestire i data segment anche attraverso FS e GS.

12.6.4 Inizializzazione della coppia SS:SP

Se abbiamo inserito lo stack in un segmento di programma privo dell'attributo STACK, allora il compito di inizializzare la coppia SS:SP spetta a noi; anche in questo caso, il metodo da seguire è abbastanza semplice. Supponiamo, ad esempio, che il segmento di Figura 12.36 sia dotato di attributo PUBLIC e non STACK; per usare questo segmento da 1024 byte come stack segment, possiamo scrivere: Notiamo subito la presenza delle due istruzioni CLI e STI che ancora non conosciamo; queste due istruzioni agiscono sul flag IF (Interrupt Enable Flag) del registro FLAGS. Come già sappiamo, se IF=1, le interruzioni mascherabili possono arrivare liberamente alla CPU; se, invece, IF=0, le interruzioni mascherabili vengono bloccate prima che arrivino alla CPU. Le due istruzioni CLI e STI producono i seguenti effetti: Quando dobbiamo inizializzare la coppia SS:SP, è importante che questo lavoro venga svolto con le interruzioni disabilitate; infatti, quando la CPU riceve una richiesta di interruzione hardware, ferma temporaneamente il programma in esecuzione, salva alcune informazioni nello stack e chiama l'opportuna ISR (Interrupt Service Routine). È chiaro però che se una interruzione arriva quando lo stack non è stato ancora creato, andiamo incontro ad un sicuro disastro!
Ricordiamoci anche del fatto che, per garantire il corretto funzionamento del computer, il flag IF deve restare a zero per il più breve tempo possibile.

12.6.5 Terminazione di un programma DOS

Quando un programma termina, deve restituire il controllo al SO che nel nostro caso è il DOS; con le versioni più recenti del DOS, questo lavoro viene svolto attraverso il servizio n. 4Ch fornito dal vettore di interruzione n. 21h (interrupt dei servizi DOS). Il codice 4Ch deve essere inserito nel registro AH, mentre il registro AL deve contenere un codice chiamato exit code (codice di uscita); tradizionalmente, nei programmi DOS, un codice 0 indica una terminazione senza errori.
L'istruzione che si utilizza per richiedere una interruzione software, ha il mnemonico INT; questo mnemonico è seguito da un codice che rappresenta, come sappiamo, il vettore di interruzione da chiamare. In definitiva, la terminazione del nostro programma è ottenuta con le seguenti istruzioni: Un programma deve avere un unico entry point, ma può avere uno o più exit point; in genere, uno solo di questi exit point indica una terminazione senza errori del programma, mentre gli altri vengono utilizzati con un exit code diverso da zero, per indicare una terminazione anomala.

12.6.6 Struttura generale di un Code Segment

Una volta che abbiamo definito tutti i dettagli relativi ai concetti appena illustrati, possiamo passare alla fase di creazione di un code segment; la Figura 12.37 illustra un code segment destinato a contenere, l'entry point, l'exit point principale e quindi anche il codice principale del programma. In questo esempio abbiamo creato un segmento di codice individuato dal nome simbolico CODESEGM; il segmento viene indirizzato con offset a 16 bit (USE16), ed è dotato di allineamento PARA, combinazione PUBLIC e classe 'CODE'.
L'entry point del programma viene indicato dall'etichetta start; naturalmente, siamo liberi di utilizzare un qualsiasi altro nome simbolico.

Nel caso di Figura 12.37, il linker inizializza la coppia CS:IP ponendo:
CS = CODESEGM
e:
IP = Offset(start)
Subito dopo l'entry point, vengono effettuate le inizializzazioni più importanti; osserviamo, infatti, che viene inizializzato il registro DS e viene associato lo stesso DS a DATASEGM. In questa stessa zona deve essere effettuata, se necessario, anche l'inizializzazione della coppia SS:SP; vista la delicatezza di questa operazione, si consiglia vivamente di inizializzare SS:SP immediatamente dopo l'entry point.
Le direttive come ASSUME, servono per impartire determinate disposizioni all'assembler e non devono essere confuse con le istruzioni; queste direttive quindi non comportano alcuna allocazione di memoria.
Terminate le varie inizializzazioni, incontriamo un'area riservata alle istruzioni che formano il codice principale; subito dopo troviamo le istruzioni per la terminazione del programma.

12.7 Schema generale di un programma Assembly

Raccogliendo tutto ciò che è stato esposto in questo capitolo, siamo finalmente in grado di definire la struttura generale di un semplice programma Assembly; prima di passare ad illustrare questa struttura, analizziamo alcuni strumenti dell'Assembly, che si rivelano molto utili.

12.7.1 Costanti simboliche

Nei precedenti capitoli è stato ampiamente spiegato che in un programma scritto con un qualsiasi linguaggio, bisogna evitare di maneggiare direttamente numeri espliciti; il programmatore, infatti, può facilmente andare incontro ad una svista che porta a scrivere un numero sbagliato.
Per evitare questi rischi, tutti i linguaggi di programmazione permettono di gestire i numeri espliciti attraverso nomi simbolici; nel caso di MASM, ci viene messa a disposizione la direttiva = (uguale). Attraverso questa direttiva possiamo scrivere:
TEMPERATURA = +25
Il nome simbolico TEMPERATURA può essere "infilato" dappertutto; lo possiamo usare, ad esempio, per inizializzare un dato, come:
varTemp1 dw TEMPERATURA
Lo possiamo usare in combinazione con DUP, come:
vettTemp dw TEMPERATURA dup ( ? )
(vettore di 25 elementi di tipo WORD).

Lo possiamo persino usare per indicare uno spiazzamento all'interno di un effective address, come:
mov cx, [bx+si+TEMPERATURA]
Ogni volta che l'assembler incontra il nome simbolico TEMPERATURA, lo sostituisce con il numero esplicito +25; eventualmente, l'assembler provvede anche ad adattare +25 all'ampiezza in bit degli operandi.

Una volta che abbiamo definito la costante TEMPERATURA, possiamo anche scrivere:
CALDO = (TEMPERATURA * 6) + 4 - (100 / 2)
Più in generale, possiamo servirci di tutti gli operatori logico aritmetici che l'Assembly ci mette a disposizione; l'insieme completo degli operatori logico aritmetici, verrà illustrato in un capitolo successivo.

Le costanti simboliche possono essere inizializzate solo con valori immediati di tipo intero, con o senza segno; è proibito, invece, scrivere:
PIGRECO = 3.14
Di conseguenza, la direttiva:
TEMP2 = TEMPERATURA / 2
assegna a TEMP2 il valore +12; infatti, la divisione +25/2 produce il quoziente +12.5 che viene troncato a +12.

Anche la direttiva = non deve essere confusa con una istruzione e non comporta quindi alcuna allocazione di memoria; le costanti simboliche quindi, possono essere create dappertutto, sia all'interno, sia all'esterno dei segmenti di programma.

12.7.2 Commenti nel linguaggio Assembly

In tutti i linguaggi di programmazione, i commenti assumono una grande importanza; attraverso i commenti, il programmatore può facilmente individuare lo scopo di un dato o di una istruzione. In un linguaggio "criptico" come l'Assembly, i commenti diventano a dir poco indispensabili; un programmatore Assembly con un minimo di senso di responsabilità, dovrebbe quindi fare un uso veramente massiccio dei commenti nei propri programmi.
Abbiamo già visto che in Assembly, il "punto e virgola" delimita l'inizio di un commento che si sviluppa su una sola linea, come:
rectBase dw 3F22h ; base del rettangolo
Questo commento quindi termina non appena si va a capo.
Se vogliamo scrivere un commento su più linee, possiamo servirci della direttiva COMMENT; possiamo scrivere, ad esempio: Come possiamo notare, un commento associato alla direttiva COMMENT, deve essere aperto e chiuso da uno stesso simbolo appartenente al set di codici ASCII; nel nostro esempio, è stato utilizzato il simbolo '#' (chiamato in gergo, cancelletto). Naturalmente, all'interno del commento non deve essere presente il simbolo '#'; l'assembler, infatti, lo interpreterebbe come la fine del commento stesso.

12.7.3 Modello di un programma Assembly

La Figura 12.38 illustra il modello generale di un programma Assembly, che riassume tutti i concetti esposti in questo capitolo. Il modello di Figura 12.38 verrà largamente utilizzato nei prossimi capitoli per illustrare diversi programmi Assembly di esempio; nei capitoli finali della sezione Assembly Base, analizzeremo programmi dotati di una struttura molto più complessa.

Come possiamo notare, abbiamo a disposizione tre segmenti di programma che ci permettono di suddividere in modo ordinato, il codice, i dati e lo stack; ciascun segmento, come sappiamo, può crescere sino a raggiungere la dimensione massima di 65536 byte (64 KiB). Con il modello di Figura 12.38, possiamo scrivere quindi programmi che richiedono al massimo una allocazione di memoria statica pari a:
65536 * 3 = 196608 byte = 192 KiB
Per scrivere semplici programmi in Assembly, una simile disponibilità di memoria statica è persino esagerata; in casi di questo genere, si potrebbero infilare tutte le informazioni (codice, dati e stack) all'interno di un unico segmento di programma di dimensioni non superiori a 64 KiB!

Come direttiva processor è stata utilizzata .386, che ci permette di sfruttare il set di istruzioni a 32 bit per scrivere programmi compatibili con tutte le CPU 80386 e superiori; utilizzando, invece, direttive come .686, rendiamo il programma incompatibile con tutti i computer equipaggiati con CPU di classe inferiore. Si tenga anche presente che le direttive come .586 o .686, sono disponibili solo con gli assembler più recenti.

Proseguendo nell'analisi del modello di Figura 12.38, possiamo notare la presenza della costante simbolica STACK_SIZE; l'uso di questo nome all'interno dello stack segment, rende il programma molto più chiaro ed elegante.

In assenza di diverse indicazioni da parte del programmatore, i vari segmenti che formano il programma di Figura 12.38, verranno disposti in memoria nello stesso ordine da noi specificato; se vogliamo alterare questa situazione, possiamo servirci, ad esempio, della tecnica dei dummy segments descritta nella sezione 12.1.

La Figura 12.38 ci permette anche di conoscere la tecnica che viene utilizzata per indicare l'entry point del programma; prima di tutto bisogna dire che un qualunque modulo Assembly, deve essere terminato dalla direttiva END. Tutto ciò che viene inserito a partire dalla riga successiva alla direttiva END, viene completamente ignorato dall'assembler; questa opportunità viene sfruttata da molti programmatori Assembly, per inserire svariati commenti alla fine del programma, senza la necessità di ricorrere a punti e virgola o a direttive COMMENT.
Il modulo Assembly che contiene l'entry point, deve essere chiuso dalla direttiva END seguita dal nome simbolico che abbiamo utilizzato per identificare il punto di ingresso del programma; nel nostro caso, possiamo notare che la direttiva END è seguita dal nome start.
Nel caso di un programma Assembly formato da due o più moduli, solo uno di essi deve specificare l'entry point; tutti gli altri moduli, devono essere chiusi da una semplice direttiva END.

Nel prossimo capitolo, analizzeremo in dettaglio tutto il procedimento svolto dall'assembler e dal linker, per rendere eseguibile un programma scritto in codice sorgente Assembly.