Assembly Base con MASM

Capitolo 24: Operatori, direttive e macro


Gli assembler più evoluti come MASM e NASM, mettono a disposizione una serie di potenti strumenti che spesso si rivelano di grande aiuto per il programmatore; questi strumenti possono essere suddivisi in tre grandi categorie chiamate: operatori, direttive e macro.

Gli operatori ci permettono di creare complesse espressioni matematiche da inserire all'interno di un programma Assembly; è possibile costruire espressioni valutabili in fase di assemblaggio o in fase di esecuzione di un programma.

Le direttive hanno principalmente lo scopo di permettere al programmatore di impartire ordini all'assembler modificandone la modalità operativa; in questo capitolo esamineremo, in particolare, le direttive condizionali che ci permettono di creare costrutti sintattici simili a quelli dei linguaggi di alto livello.

Attraverso le macro è possibile rappresentare con un nome simbolico, intere porzioni di un programma Assembly; in questo capitolo vengono anche illustrate le macro predefinite messe a disposizione da MASM.

24.1 Gli operatori per la fase di assemblaggio

Iniziamo con una serie di operatori chiamati assembler time operators (operatori per la fase di assemblaggio); questa definizione è legata al fatto che tali operatori, compaiono in espressioni che devono poter essere valutate dall'assembler in fase di assemblaggio del programma.
In sostanza, l'assembler deve avere la possibilità di analizzare e risolvere queste espressioni, ricavandone alla fine un valore numerico (risultato dell'espressione) che verrà inserito direttamente nel codice macchina; proprio per questo motivo, gli assembler time operators possono comparire solo in espressioni comprendenti operandi di tipo Imm (espressioni costanti).
Non è possibile quindi utilizzare operandi di tipo Reg o Mem; è chiaro, infatti, che l'assembler non può conoscere in anticipo il contenuto che verrà assunto da un registro o da una variabile in fase di esecuzione del programma.

Nei precedenti capitoli abbiamo visto che è vivamente sconsigliabile l'utilizzo di operandi immediati sotto forma di numeri espliciti; la cosa più intelligente da fare consiste allora nel servirsi di nomi simbolici.
Il modo più semplice per creare un operando immediato, gestibile attraverso un nome simbolico, consiste nell'utilizzare la direttiva di assegnamento "uguale":
=
La sintassi generica per questa direttiva è la seguente:
nomesimbolico = espressione
In questo caso con il termine espressione si indica un valore immediato o una qualunque espressione matematica formata da operatori dell'Assembly e da operandi immediati; nel caso più semplice possiamo scrivere, ad esempio:
LUNGHEZZA = 3500
In questo caso LUNGHEZZA è il nome simbolico dell'operando immediato, mentre 3500 è il valore immediato che la direttiva = assegna all'operando stesso.
Il nome simbolico LUNGHEZZA può essere utilizzato in qualunque punto del programma; ogni volta che l'assembler incontra questo nome, provvede a sostituirlo con il valore 3500.

Al posto di = si può anche utilizzare la direttiva EQU; questa direttiva viene descritta più avanti. La differenza sostanziale che esiste tra = e EQU sta nel fatto che = permette solo di assegnare valori numerici ad un nome simbolico; la direttiva EQU, invece, è molto più potente in quanto permette di rappresentare con un nome simbolico, sia valori numerici, sia stringhe alfanumeriche, dalle più semplici alle più complesse!

Un nome simbolico al quale è stato assegnato un valore con = può essere ridichiarato più volte (sempre con =) e in qualunque altro punto del programma; nel caso di EQU, invece, è possibile ridichiarare solo nomi simbolici che rappresentano stringhe.
Nel seguito del capitolo e nei capitoli successivi, per le dichiarazioni di operandi di tipo numerico verrà utilizzata sempre la direttiva =; in ogni caso, ciascun programmatore è libero di scegliere la strada che più si addice ai propri gusti.

Tutti gli operatori dell'Assembly lavorano su operandi immediati di tipo intero; questo significa che non sono permesse dichiarazioni di operandi immediati del tipo:
PI_GRECO = 3.14
Le ampiezze in bit degli operandi immediati non possono superare le capacità della CPU che si sta utilizzando; non è possibile quindi dichiarare operandi immediati a 32 bit con una CPU a 16 bit.

24.1.1 Operatori aritmetici

Analizziamo in dettaglio i vari gruppi di operatori partendo da quelli aritmetici; nelle tabelle che seguono, i termini expr1, expr2, expr3, etc, indicano generiche espressioni costanti che l'assembler deve poter risolvere. Vediamo alcuni esempi pratici che illustrano il funzionamento di questi operatori; supponiamo di avere le seguenti dichiarazioni: Quando l'assembler incontra queste dichiarazioni cerca di analizzarle e di risolverle in modo da assegnare al primo membro di ciascuna di esse, un ben preciso valore numerico; nel seguito della fase di assemblaggio, ogni volta che l'assembler incontra uno di questi nomi simbolici, lo sostituisce con il corrispondente valore numerico.

Innanzi tutto osserviamo che: Utilizzando ora gli operandi immediati OPERANDO1, OPERANDO2 e OPERANDO3 (che devono essere già stati dichiarati), possiamo costruire espressioni più complesse come: EXPR1, EXPR2, EXPR3, EXPR4, EXPR5 e EXPR6 mostrate nella tabella precedente.
Per assegnare un valore numerico ai nomi simbolici che rappresentano queste espressioni, l'assembler deve effettuare una serie di calcoli. A tale proposito, notiamo che è permesso l'uso delle parentesi tonde attraverso le quali possiamo stabilire l'ordine di valutazione delle espressioni; le parentesi tonde permettono anche di alterare l'ordine di precedenza degli operatori (che verrà illustrato più avanti).

Si può facilmente constatare che per EXPR1 l'assembler calcola:
EXPR1 = (12000 + 2) - (-2) = 12002 + 2 = +12004
Per EXPR2 l'assembler calcola:
EXPR2 = (2 - 12000) - (-2) = -11998 + 2 = -11996
Per EXPR3 l'assembler calcola:
EXPR3 = -2 - 12000 = -12002
Per EXPR4 l'assembler calcola:
EXPR4 = (12000 * (-2))  / 2 = -24000 / 2 = -12000
Per EXPR5 l'assembler calcola:
EXPR5 = ((12000 - 2) * (-2)) / 2 = (11998 * (-2)) / 2 = -23996 / 2 = -11998
Per EXPR6 l'assembler calcola:
EXPR6 = -(12000 * (-2)) = -(-24000) = +24000
Tenendo conto del fatto che stiamo operando su numeri interi, appare chiaro che l'operatore / si riferisce alla divisione intera, la quale provoca il troncamento di tutte le cifre che nel risultato (quoziente) figurano dopo la virgola; nel caso, ad esempio, di:
2033 / 2
si ottiene 1016.5 che, dopo il troncamento della parte decimale, diventa 1016!
Analogamente, l'operatori MOD (modulo) restituisce il resto della divisione intera tra due operandi.

Le espressioni del tipo appena illustrato, possono essere inserite anche all'interno delle istruzioni Assembly; naturalmente, anche in questo caso l'assembler deve essere in grado di convertire queste espressioni in valori immediati.
Nel precedente capitolo, ad esempio, abbiamo visto come si deve procedere per calcolare l'offset dell'ultimo elemento di un vettore; a tale proposito, supponiamo di aver definito un vettore di nome vectDword formato da MAX_ELEM elementi di tipo DWORD, con:
MAX_ELEM = 30
A questo punto, se vogliamo caricare in BX l'offset dell'ultimo elemento di vectDword possiamo scrivere:
mov bx, offset vectDword + ((MAX_ELEM - 1) * 4)
Notiamo, infatti, che (MAX_ELEM - 1) è l'indice dell'ultimo elemento di vectDword e 4 è la dimensione in byte di ciascun elemento.

Ricordiamo che OFFSET è un operatore dell'Assembly che, applicato al nome di una variabile, ne restituisce il relativo offset (che è un valore immediato); se, ad esempio, vectDword si trova all'offset 00BAh del blocco dati, la precedente istruzione assegna a BX il valore:
00BAh + ((30 - 1)  * 4) = 00BAh + (29 * 4) = 00BAh + 116 = 00BAh +  0074h = 012Eh
L'assembler genera quindi il codice macchina:
BBh 012Eh(r)
Quando la CPU incontra tale codice macchina, carica in BX il valore immediato 012Eh (eventualmente rilocato dal linker).

Un altro esempio può essere rappresentato da:
mov ax, ((OPERANDO1 - OPERANDO2) * OPERANDO3) / OPERANDO2
In base ai valori assegnati in precedenza a questi tre operandi, possiamo dire che l'assembler produrrà il codice macchina dell'istruzione che carica in AX il numero negativo -11998.

Possiamo utilizzare espressioni di qualunque complessità senza il rischio di provocare ripercussioni negative sul programma (in termini di prestazioni o di dimensioni del codice); ricordiamoci, infatti, che tutti i calcoli vengono svolti dall'assembler, che in fase di assemblaggio del programma provvede a risolvere queste espressioni sostituendole con il corrispondente risultato (valore immediato).
Nel codice macchina del programma, quindi, non rimane alcuna traccia di tutti questi calcoli (ad eccezione del risultato finale); come al solito, se vogliamo verificare il lavoro svolto dall'assembler, ci basta richiedere la generazione del listing file.

Nel dichiarare espressioni come quelle presentate nei precedenti esempi, il programmatore deve prestare molta attenzione alla corretta gestione del segno e dell'ampiezza in bit dei vari operandi di tipo Imm; vediamo subito un esempio pratico. Analizzando queste istruzioni appare evidente l'intenzione del programmatore di voler operare sui numeri interi con segno a 16 bit in complemento a 2; in presenza di una CPU a 32 bit, l'assembler assegna a PRODOTTO il numero intero negativo -62000, ma in fase di esecuzione del programma, la procedura writeSdec16 visualizza il numero intero positivo +3536!
L'errore commesso dal programmatore è abbastanza chiaro; il valore -62000 richiede almeno 32 bit per essere rappresentato in modo corretto; in esadecimale, la codifica di -62000 a 32 bit in complemento a 2 è FFFF0DD0h. Per caricare tale valore in AX l'assembler tronca i 16 bit più significativi ottenendo così AX=0DD0; ma per i numeri interi con segno a 16 bit in complemento a 2, 0DD0h codifica proprio il numero positivo +3536!

24.1.2 Operatori logici

La tabella seguente mostra gli operatori logici messi a disposizione da MASM; naturalmente, questi operatori non devono essere confusi con le omonime istruzioni Assembly! Così come accade per le istruzioni omonime, anche gli operatori logici dell'Assembly agiscono sui singoli bit dei loro operandi immediati; questo significa che se abbiamo, ad esempio, le seguenti dichiarazioni: allora:
EXPR1 = NOT VAL1
assegna a EXPR1 il numero binario 01010101b.
EXPR2 = VAL1 OR VAL2
assegna a EXPR2 il numero binario 11111111b.
EXPR3 = VAL1 XOR VAL3
assegna a EXPR3 il numero binario 10100101b.
EXPR4 = VAL2 AND VAL3
assegna a EXPR4 il numero binario 00001010b.
mov ax, VAL1 AND (NOT VAL3)
fa generare all'assembler il codice macchina dell'istruzione che carica in AX il numero binario 10100000b.

24.1.3 Operatori di scorrimento dei bit

La tabella seguente mostra gli operatori di scorrimento dei bit messi a disposizione da MASM; anche in questo caso non bisogna confondere questi operatori con le istruzioni omonime dell'Assembly. Questi due operatori trattano il loro operando di sinistra come un numero senza segno (scorrimento logico dei bit); non esistono quindi operatori per lo scorrimento aritmetico dei bit, che tengono cioè conto del bit di segno dell'operando.
Nello scorrimento verso destra, i bit dell'operando che traboccano da destra vengono persi; nello scorrimento verso sinistra, l'assembler finché è possibile cerca di estendere l'ampiezza in bit del risultato.
Se abbiamo, ad esempio, la seguente dichiarazione:
VAL1 = 10101010b
allora l'espressione:
EXPR1 = VAL1 SHL 1
assegna a EXPR1 il numero binario a 16 bit 0000000101010100b; come si può notare, l'assembler tratta EXPR1 come una costante a 16 bit tale da poter contenere interamente il risultato prodotto da SHL. Appare chiaro che tutto ciò ha un limite rappresentato dall'ampiezza massima in bit gestibile dalla CPU che stiamo utilizzando; se abbiamo, ad esempio, una CPU a 16 bit e scriviamo: verrà caricato in AX il valore zero!
Infatti, EXPR1 non può avere un'ampiezza superiore a 16 bit e quindi non è in grado di contenere il risultato completo a 17 bit dello scorrimento verso sinistra; il bit più significativo del risultato trabocca da sinistra e viene perso, per cui a EXPR1 verrà assegnato il numero binario a 16 bit 0000000000000000b!

Un aspetto curioso degli operatori SHL e SHR è dato dal fatto che il contatore che indica il numero di scorrimenti di bit da effettuare, può essere anche un numero negativo; in questo caso si produce l'effetto di far scorrere i bit nel verso opposto a quello indicato dall'operatore!
Se scriviamo, ad esempio: otteniamo l'assegnamento a EXPR1 del numero binario 01010101b; come si può notare, il contatore negativo ha fatto scorrere i bit di VAL1 verso destra anziché verso sinistra!

24.1.4 Operatori relazionali

La tabella seguente mostra gli operatori relazionali messi a disposizione da MASM; attraverso questi operatori è possibile confrontare tra loro operandi immediati o espressioni complesse contenenti operandi immediati. Combinando tra loro due o più espressioni costanti attraverso gli operatori relazionali, si ottiene una cosiddetta espressione booleana; come già sappiamo, il risultato di una espressione booleana può essere vero o falso (TRUE o FALSE).
Convenzionalmente, in tutti i linguaggi di programmazione (compreso l'Assembly), un risultato FALSE viene associato al valore zero; un risultato TRUE, invece, viene associato ad un valore diverso da zero (valore non nullo) stabilito dalle convenzioni standard del linguaggio che si sta usando.
Il programmatore deve solo ricordarsi di questa regola e non ha la necessità di conoscere il valore numerico esatto associato a TRUE (a titolo di curiosità, MASM assegna a TRUE il valore simbolico -1); si può dire anzi che è vivamente sconsigliabile la scrittura di programmi che fanno esplicito riferimento al valore numerico assegnato a TRUE!

Supponiamo di avere: In questo caso, tenendo conto del fatto che VAL1 è chiaramente minore (LT) di VAL2, si ottiene l'assegnamento di FALSE (zero) a EXPR1 e EXPR3, mentre a EXPR2 verrà assegnato TRUE (non zero).

Gli operatori relazionali esprimono tutta la loro potenza quando vengono usati in combinazione con le direttive condizionali IF, ESLSEIF, etc; tali direttive vengono illustrate più avanti.

In questo paragrafo sono stati illustrati solo alcuni dei numerosissimi operatori messi a disposizione da MASM; nei precedenti capitoli abbiamo già conosciuto diversi operatori dell'Assembly come SEG, OFFSET, BYTE PTR, WORD PTR, etc. Altri operatori importanti verranno illustrati nei prossimi capitoli; per ulteriori dettagli si consiglia di consultare i manuali di MASM reperibili anche attraverso Internet.

24.1.5 Ordine di precedenza degli operatori

In conclusione di questo paragrafo analizziamo la tabella relativa all'ordine di precedenza dei vari operatori dell'Assembly; gli operatori appartenenti alla stessa riga hanno uguale precedenza, mentre le varie righe indicano i diversi ordini di precedenza, dal più alto (riga 1) al più basso (riga 13). I diversi ordini di precedenza dei vari operatori, permettono all'assembler di valutare in modo corretto istruzioni del tipo (esempio del paragrafo 24.1.1):
mov bx, offset vectDword + ((MAX_ELEM - 1) * 4)
Siccome il calcolo dell'offset ha precedenza maggiore dell'operatore + binario, l'assembler determina innanzi tutto l'offset di vectDword, poi calcola:
(MAX_ELEM - 1) * 4 = (30 - 1) * 4 = 29 * 4 = 116
e infine somma il valore 116 all'offset di vectDword; se + avesse avuto precedenza maggiore del calcolo dell'offset avremmo ottenuto un'istruzione senza senso indicante all'assembler di sommare 116 a vectDword e calcolare poi l'offset del risultato della somma!

L'ordine di precedenza dei vari operatori è una caratteristica importantissima di tutti i linguaggi di programmazione, i quali forniscono sempre un'apposita tabella relativa a questo aspetto; in ogni caso, un programmatore avveduto e responsabile non dovrebbe fidarsi ciecamente di tali tabelle!
Un compilatore (o un assemblatore) che non gestisce correttamente gli ordini di precedenza dei vari operatori, può generare programmi contenenti dei bug difficilissimi da scovare; in caso di dubbio, si consiglia vivamente di ricorrere alle parentesi tonde che permettono di specificare in modo esplicito l'ordine di valutazione di un'espressione e, soprattutto, rendono molto più chiaro il codice.

24.2 Gli operatori per la fase di esecuzione

Le versioni più recenti di MASM forniscono una serie di potenti operatori utilizzabili in fase di esecuzione di un programma; per questo motivo si parla anche di run time operators (operatori per la fase di esecuzione).
Tutti questi operatori risultano molto familiari ai programmatori C/C++ in quanto utilizzano la classica sintassi degli operatori relazionali e logici di tali linguaggi; la tabella seguente mostra i run time operators più importanti (i simboli op1 e op2 indicano generici operandi di tipo Reg, Mem o anche Imm). Anche in questo caso, quindi, abbiamo a che fare con una serie di operatori relazionali, booleani e logici; la differenza sostanziale rispetto agli assembler time operators è data dal fatto che con i run time operators possiamo utilizzare, non solo operandi di tipo Imm, ma anche operandi di tipo Reg e Mem!

I run time operators possono essere utilizzati esclusivamente in combinazione con una serie di apposite direttive per il controllo del flusso; queste direttive vengono illustrate più avanti.

L'uso dei run time operators di tipo relazionale è abbastanza intuitivo; ad esempio, l'espressione:
(ax == bx)
restituisce TRUE se e solo se il contenuto di AX coincide con il contenuto di BX, mentre in caso contrario si ottiene FALSE.
Analogamente, l'espressione:
(ax < bx)
restituisce TRUE se e solo se il contenuto di AX è minore del contenuto di BX; in caso contrario si ottiene FALSE.

Ricordiamoci della importante differenza che esiste tra gli operatori logici bit a bit e gli operatori booleani; a tale proposito, si veda quanto spiegato al paragrafo 19.15 del Capitolo 19.
Per maggiore chiarezza, vediamo un semplice esempio pratico; consideriamo le seguenti istruzioni: Si può facilmente constatare che:
al & bl = 00000000b ; AND logico bit a bit)
mentre:
al && bl = TRUE ; AND booleano
Una espressione come:
!cx
è di tipo booleano e non logico e quindi restituisce TRUE se e solo se il contenuto di CX vale zero; se, invece, il contenuto di CX è diverso da zero, si ottiene FALSE.

Un'ultima considerazione da fare riguarda gli assembler time operators + binario, - binario e * binario; come abbiamo già visto nei precedenti capitoli, questi tre operatori possono essere utilizzati anche a run time per gestire gli indirizzamenti!
Con tutte le CPU della famiglia Intel e compatibili si può scrivere, ad esempio:
mov ax, [di+2]
Questa istruzione trasferisce in AX un valore a 16 bit che si trova all'offset DI+2 del segmento di programma referenziato da DS.
Analogamente, l'istruzione:
mov ax, es:[di-2]
trasferisce in AX un valore a 16 bit che si trova all'offset DI-2 del segmento di programma referenziato da ES.
Nel Capitolo 11 abbiamo anche visto che con le CPU 80386 e superiori è inoltre possibile utilizzare l'operatore * per gestire il nuovo formato di indirizzamento S.I.B. (Scale Index Base); usando, ad esempio, EBP come registro base, EDI come registro indice, 4 come fattore di scala e la costante simbolica DISP32 come spiazzamento, possiamo scrivere l'istruzione:
mov eax, [ebp + (edi * 4) + DISP32]
Tutti questi calcoli verranno eseguiti dalla CPU in fase di esecuzione del programma; si noti che le parentesi tonde sono superflue in quanto * ha precedenza maggiore di + binario!

24.3 Le direttive

Nei precedenti capitoli abbiamo già incontrato numerose direttive dell'Assembly; possiamo citare, ad esempio, ASSUME, SEGMENT, ALIGN, END, INCLUDE, .8086, .386, DB, DW, etc. Altre direttive verranno illustrate al momento opportuno nei capitoli successivi; in questo capitolo, invece, verranno analizzate principalmente le direttive condizionali che si utilizzano in combinazione con gli operatori a run time e ad assembler time.

24.3.1 Le direttive condizionali per la fase di assemblaggio

Il primo gruppo di assembler time directives che andiamo ad esaminare comprende le direttive IF, ELSEIF, ELSE e ENDIF; queste direttive ci permettono di effettuare, in fase di assemblaggio, una scelta tra più opzioni possibili.
Vediamo un esempio pratico che illustra l'utilizzo abbastanza intuitivo di queste direttive; supponiamo di avere le seguenti dichiarazioni: A questo punto, nel blocco codice del nostro programma possiamo scrivere: Questa sequenza dice all'assembler che: In fase di assemblaggio del programma, l'assembler rileva che VAL1 è minore di VAL2, per cui la prima espressione booleana è TRUE; la conseguenza pratica è data dal fatto che verrà assemblata esclusivamente l'istruzione:
mov ax, 10
Per verificarlo possiamo consultare il listing file prodotto dall'assembler.

In sostanza, attraverso queste direttive possiamo indicare all'assembler quali porzioni del nostro programma devono essere assemblate e quali no!
In effetti, le direttive appena illustrate vengono utilizzate principalmente per tale scopo; si parla allora di assemblaggio condizionale.

Come si può notare, il tipo di blocco condizionale appena illustrato deve iniziare con un IF e deve terminare con un ENDIF; questa caratteristica rende possibile la creazione di blocchi condizionali innestati. Possiamo scrivere, ad esempio: All'interno di un blocco IF ENDIF le direttive ELSEIF e ELSE sono facoltative; se necessario si può inserire un numero arbitrario di ELSEIF, mentre la direttiva ELSE deve essere unica in quanto rappresenta la scelta predefinita nel caso in cui tutte le precedenti espressioni siano risultate FALSE!
L'assembler valuta solo le istruzioni associate alla prima espressione che vale TRUE, dopo di che salta alla direttiva ENDIF (cioè, esce dal blocco condizionale); anche se più espressioni valgono TRUE, viene presa in considerazione solo la prima di esse che viene incontrata.
In assenza di espressioni che valgono TRUE, vengono valutate le istruzioni associate al caso ELSE; se ELSE non è presente, l'assembler salta direttamente alla direttiva ENDIF.

Tradizionalmente (anche per motivi di chiarezza), le direttive vengono scritte sempre in maiuscolo; in ogni caso è possibile anche l'uso delle lettere minuscole.

Come è stato già spiegato, possiamo creare espressioni di qualunque complessità; possiamo scrivere, ad esempio:
IF (expr1 GT (expr2 AND (NOT expr3)))
Se l'espressione è verificata (TRUE), l'assembler effettua l'assemblaggio di tutte le istruzioni che seguono la direttiva IF; in caso contrario vengono esaminate le espressioni associate ad eventuali direttive ELSEIF e ELSE.

Naturalmente è anche possibile scrivere:
IF expr1
Questo costrutto significa: se expr1 è TRUE, cioè se il risultato prodotto dalla valutazione di expr1 è diverso da zero.
Attenzione, invece, al fatto che il costrutto:
IF (NOT expr1)
non significa: se expr1 è FALSE; infatti, l'operatore NOT inverte i bit di expr1 (NOT logico bit a bit) per cui questo costrutto significa: se ~expr1 è TRUE!
Per sapere se expr1 è uguale a zero, la forma corretta da utilizzare è:
IF (expr1 EQ 0)
Passiamo ora ad un altro importante gruppo di assembler time directives che ci permette di prendere delle decisioni in base al fatto che un determinato operando sia stato definito o meno; un tipico blocco condizionale di questo genere comprende le direttive: IFDEF, ELSEIFDEF, ELSE, ENDIF.
Possiamo scrivere, ad esempio: Il costrutto:
IFDEF OPERANDO1
significa: se OPERANDO1 esiste, cioè se OPERANDO1 è stato definito da qualche parte del programma; in caso affermativo viene assemblato il primo blocco di istruzioni.
Se, invece, esiste OPERANDO2, viene assemblato il secondo blocco di istruzioni; altrimenti (se non esiste nessuno dei due operandi), viene presa la decisione predefinita che consiste nell'assemblaggio del terzo blocco di istruzioni.
In queste valutazioni non ha alcuna importanza il valore assegnato ad un operando; infatti, anche se scriviamo:
OPERANDO1 = 0
il test:
IFDEF OPERANDO1
restituisce ugualmente TRUE in quanto stiamo testando se OPERANDO1 esiste e non se è diverso da zero!

Vediamo un esempio pratico che dimostra la grande utilità di queste direttive. Supponiamo di voler scrivere un programma che, se eseguito su una CPU 80386 o superiore utilizza le istruzioni a 32 bit, mentre in caso contrario utilizza le istruzioni a 16 bit; possiamo scrivere allora porzioni di codice del tipo: Se vogliamo assemblare il programma con una CPU 80386 o superiore, ci basta dichiarare il nome simbolico CPU386 che provocherà l'assemblaggio dell'istruzione CWDE; infatti, in questo caso il costrutto:
IFDEF CPU386
restituisce TRUE in quanto il nome CPU386 esiste. Se, invece, vogliamo assemblare il programma con una CPU a 16 bit, ci basta eliminare (o commentare) la dichiarazione del nome simbolico CPU386; in questo caso verrà assemblata l'istruzione CWD!

In alternativa, è anche possibile scrivere: In questo modo, assegnando al nome simbolico CPU386 un valore diverso da zero, otteniamo l'assemblaggio della sola prima istruzione; assegnando, invece, a CPU386 il valore zero, otteniamo l'assemblaggio della sola seconda istruzione.

Sono disponibili anche le direttive IFNDEF, ELSEIFNDEF le quali seguono una logica inversa rispetto a IFDEF, ELSEIFDEF; ad esempio, il costrutto:
IFNDEF OPERANDO1
significa: se OPERANDO1 non è stato definito (not defined).
Possiamo dire quindi che se OPERANDO1 non esiste, il costrutto precedente restituisce TRUE; se, invece, OPERANDO1 esiste, si ottiene FALSE.
Più avanti verranno illustrate una serie di altre importanti assembler time directives che vengono prevalentemente usate per creare le macro.

24.3.2 Le direttive condizionali per la fase di esecuzione

Come è stato accennato in precedenza, le versioni più recenti di MASM (6.x o superiore) forniscono una serie di direttive e operatori utilizzabili anche a run time; attraverso questi strumenti è possibile scrivere programmi Assembly con una sintassi molto simile a quella dei linguaggi di alto livello come il C/C++ o il Pascal.
Tutte le run time directives devono essere precedute da un punto (.) che le distingue dalle assembler time directives; partiamo allora dalle direttive condizionali che sono: .IF, .ELSEIF, .ELSE e .ENDIF.
I run time operators possono essere utilizzati esclusivamente in combinazione con le run time directives; possiamo scrivere, ad esempio: Un programmatore Assembly con un minimo di esperienza non si fa certo impressionare da questa "meraviglia" e intuisce subito il trucco; per svelare il mistero ci basta consultare il listing file del nostro programma!
Nel caso, ad esempio, del Borland Turbo Assembler, scopriamo che il codice precedente viene tradotto (com'era prevedibile) nel seguente modo: Come si può notare, le run time directives non hanno niente di magico; queste direttive, infatti, vengono interpretate dall'assembler che le traduce poi nella corrispondente sequenza di istruzioni Assembly. A rigore quindi non si potrebbe neanche parlare di direttive a run time; infatti, è sempre l'assembler che traduce tutto in codice macchina (ad assembler time)!

Le run time directives presentano una importante limitazione legata al fatto che l'espressione booleana associata ad esse deve essere costituita da un confronto tra due operandi semplici; non è possibile quindi utilizzare espressioni complesse del tipo:
.IF (ax < (bx & !cx))
Nel caso generale, una direttiva condizionale viene tradotta in un confronto (CMP o TEST) tra l'operando di sinistra e quello di destra; ne consegue che tali due operandi debbono soddisfare i requisiti richiesti dalle stesse istruzioni CMP e TEST!
In sostanza, anche in questo caso possiamo parlare di operando sorgente e operando destinazione; proprio come accade allora per CMP e TEST, i due operandi SRC e DEST devono avere la stessa ampiezza in bit e possono combinarsi tra loro solamente nel modo seguente: Sono proibiti, invece, i confronti del tipo: Oltre alle direttive condizionali sono disponibili anche una serie di direttive che permettono di implementare le iterazioni; anche in questo caso si utilizza la classica sintassi delle analoghe direttive offerte dai linguaggi di alto livello.
Il ciclo WHILE DO viene implementato con le direttive .WHILE, .ENDW; il ciclo REPEAT UNTIL viene implementato con le direttive .REPEAT, .UNTIL e .UNTILCXZ.

Supponiamo di voler inizializzare con il valore 3BF2h tutti i 10 elementi di un vettore di WORD chiamato Vector1; utilizzando allora un ciclo WHILE DO, possiamo scrivere: Come accade con i linguaggi di alto livello, la direttiva .WHILE valuta l'espressione tra parentesi tonde e se ottiene TRUE esegue le istruzioni successive; dopo l'ultima istruzione, la direttiva .ENDW salta nuovamente a .WHILE per cui il test viene nuovamente ripetuto. Le iterazioni proseguono purché l'espressione tra parentesi tonde continui ad essere TRUE; quando l'espressione diventa FALSE l'iterazione termina e l'esecuzione riprende dall'istruzione successiva alla direttiva .ENDW.
Appare evidente che se l'espressione booleana risulta FALSE sin dall'inizio, il ciclo .WHILE non viene mai eseguito; inoltre, se l'espressione booleana non diventa mai FALSE, si innesca un loop infinito!

Nel caso del Borland Turbo Assembler il codice precedente viene tradotto nel seguente modo: Se abbiamo bisogno di un ciclo che venga sicuramente eseguito almeno una volta, possiamo utilizzare le direttive .REPEAT e .UNTIL; in questo caso, infatti, la condizione di uscita dal ciclo viene testata alla fine del ciclo stesso.
La struttura generale del ciclo è la seguente: Come si può notare, la sequenza istruzioni viene eseguita almeno una volta; alla fine viene valutata l'espressione associata alla direttiva .UNTIL. Se espressione risulta FALSE viene ripetuto il ciclo; le iterazioni continuano finché espressione non diventa TRUE.

Al posto di .UNTIL si può utilizzare anche la direttiva .UNTILCXZ; come si può facilmente intuire, questa direttiva termina le iterazioni quando espressione diventa TRUE e/o quando CX diventa zero.

Per creare dei loop più complessi sono disponibili anche le direttive .BREAK e .CONTINUE; queste due direttive sono analoghe a quelle fornite dai linguaggi di alto livello.
All'interno di un loop, la direttiva .BREAK provoca l'immediata uscita dal loop stesso; si può scrivere, ad esempio:
.BREAK .IF (ax == cx)
Sempre all'interno di un loop, la direttiva .CONTINUE determina un salto alla direttiva che controlla il loop stesso (.WHILE, .UNTIL o UNTILCXZ), provocando quindi una nuova valutazione della condizione di uscita; si può scrivere, ad esempio:
.CONTINUE .IF (ax < cx)
In definitiva, il comportamento delle run time directives è assolutamente analogo a quello delle istruzioni per il controllo di flusso dei linguaggi di alto livello; pertanto, se si vogliono avere maggiori dettagli sull'uso di tutte queste direttive si consiglia di consultare un qualsiasi manuale di programmazione sul linguaggio C/C++, Pascal, etc.

Non ci vuole molto a capire che le run time directives fanno storcere il naso a parecchi programmatori Assembly; in effetti, non si capisce bene che senso abbia programmare in questo modo!
L'aspetto più discutibile poi è dato dal fatto che queste direttive vengono tradotte in istruzioni Assembly in base ai gusti di chi ha progettato l'assembler; e non è detto che tali gusti coincidano con quelli dei programmatori!
Con le run time directives stiamo chiedendo all'assembler di scrivere il codice al nostro posto; tutto ciò è chiaramente in contrasto con la filosofia di quei programmatori che utilizzano l'Assembly proprio perché vogliono avere il controllo totale sul programma che stanno scrivendo.
Le run time directives presentano la caratteristica di snaturare l'Assembly avvicinandolo ai inguaggi di alto livello; ma, a quel punto, tanto vale passare direttamente ad un vero linguaggio di alto livello che mette a disposizione tutte queste comodità. Una valida alternativa è rappresentata dal linguaggio C che può essere considerato un vero e proprio Assembly di alto livello.

24.3.3 La direttiva LABEL

La direttiva LABEL permette di creare etichette personalizzate; la sintassi generale per questa direttiva è la seguente:
nome_etichetta LABEL tipo
Il campo nome_etichetta rappresenta il nome simbolico che vogliamo assegnare alla etichetta; il campo tipo rappresenta le caratteristiche dell'etichetta che vogliamo creare.
Per definire le caratteristiche di una etichetta possiamo servirci, in particolare, dei size operators e dei distance operators; vediamo subito alcuni esempi.

Se vogliamo creare una etichetta che delimita l'inizio di una procedura raggiungibile con una FAR CALL possiamo scrivere:
nome_procedura LABEL FAR
In questo modo, una istruzione del tipo:
call nome_procedura
verrà tradotta dall'assembler nel codice macchina di una FAR CALL; nel capitolo successivo verrà illustrato un metodo molto più elegante per creare procedure NEAR o FAR.

Consideriamo ora la seguente porzione di un blocco dati: Il fatto che wordData sia stata definita come LABEL di tipo WORD, ci permette di utilizzare tale nome per accedere ai dati successivi a blocchi di 16 bit; infatti, possiamo dire che: In sostanza, attraverso wordData possiamo accedere alle informazioni successive come se si trattasse di un vettore di WORD.

24.4 Le macro

Molti linguaggi di programmazione, compreso l'Assembly, supportano le macro; si tratta di un potentissimo strumento che permette di rappresentare con un nome simbolico, intere porzioni di codice, se non addirittura interi programmi!

Si può tracciare un parallelo tra la struttura di una macro e una dichiarazione del tipo:
NOME_SIMBOLICO = 3500
In tale dichiarazione possiamo notare la presenza di un nome simbolico, una direttiva (=) e un valore numerico (3500) da assegnare al nome simbolico stesso.
Nel caso della macro, la differenza sostanziale sta nel fatto che al posto del valore numerico inizializzante ci può essere praticamente qualsiasi cosa; una macro, infatti, può rappresentare un semplice valore numerico o anche una sequenza di caratteri alfanumerici che nel loro insieme formano intere porzioni di codice.
In fase di assemblaggio di un programma, ogni volta che l'assembler incontra il nome di una macro, lo sostituisce con tutto ciò che la macro stessa rappresenta; tale operazione di sostituzione prende il nome di espansione della macro.

Una macro è quindi una semplice dichiarazione a disposizione dell'assembler; come accade per tutte le dichiarazioni, la macro non ha un indirizzo di memoria. Ciò significa che la dichiarazione di una macro può essere collocata dappertutto, sia all'interno, sia all'esterno dei segmenti di programma.

Il caso più semplice in assoluto è rappresentato da un operando immediato dichiarato attraverso la direttiva di assegnamento =; possiamo creare quindi una semplicissima macro scrivendo, ad esempio:
PESO_KG = 300
In fase di assemblaggio del programma, ogni volta che l'assembler incontra il nome PESO_KG lo sostituisce con il valore numerico 300; possiamo dire quindi che la macro PESO_KG viene espansa nel numero esplicito 300.
In precedenza è stata più volte sottolineata l'importanza dell'uso dei nomi simbolici al posto dei numeri espliciti; in questo modo si rende il programma più comprensibile e si riduce notevolmente il rischio di commettere errori. Se si sbaglia nello scrivere un numero esplicito, l'assembler non è in grado di intervenire; se, invece, si sbaglia nello scrivere un nome simbolico, si ottiene subito un messaggio di errore da parte dell'assembler stesso!

24.4.1 La direttiva EQU

Con l'operatore di assegnamento = possiamo dichiarare nomi simbolici che rappresentano esclusivamente valori numerici; se ci serve qualcosa di più complesso, dobbiamo ricorrere alla direttiva EQU (equivalent).
Questa direttiva permette di dichiarare nomi simbolici che possono rappresentare, sia valori numerici immediati, sia stringhe alfanumeriche; l'unica limitazione è data dal fatto che una dichiarazione creata con la direttiva EQU deve svilupparsi su un'unica linea del programma (non è permesso cioè andare a capo).
Con la direttiva EQU la dichiarazione precedente può essere riscritta come:
PESO_KG EQU 300
A differenza di quanto accade con la direttiva =, se abbiamo dichiarato PESO_KG con la direttiva EQU non è possibile ridichiarare questo nome simbolico in nessun'altra parte del programma; non è possibile cioè assegnare a PESO_KG un altro valore o un altro significato.
Questa limitazione riguarda solo i nomi simbolici dichiarati con EQU che rappresentano valori numerici; la ridichiarazione, invece, è permessa per i nomi simbolici dichiarati con EQU che rappresentano stringhe alfanumeriche.

Una macro dichiarata con EQU è formata da un nome simbolico, dalla direttiva EQU e dal cosiddetto corpo della macro; in fase di assemblaggio di un programma, ogni volta che l'assembler incontra il nome di una macro, lo sostituisce con il relativo corpo.
Il corpo di una macro può rappresentare qualsiasi cosa che abbia un senso logico nel contesto del programma che si sta scrivendo; in sostanza, dopo la fase di espansione delle macro si deve ottenere codice Assembly sensato. In caso contrario l'assembler genera gli opportuni messaggi di errore!

Supponiamo di voler rappresentare una stringa C mediante una macro; possiamo scrivere:
TEXT_MACRO EQU Stringa da stampare, 0
A questo punto, nel blocco dati del nostro programma possiamo scrivere:
Stringa1 db TEXT_MACRO
In fase di assemblaggio del programma, l'assembler esamina il blocco dati e incontrando il nome simbolico TEXT_MACRO, lo sostituisce con il suo corpo ottenendo:
Stringa1 db Stringa da stampare, 0
Chiaramente questo codice è privo di senso per cui l'assembler genera un messaggio di errore; il modo corretto di scrivere la macro precedente è:
TEXT_MACRO EQU 'Stringa da stampare', 0
Una volta capito il meccanismo possiamo anche scrivere, ad esempio:
MACRO_DB EQU db 'Stringa da stampare', 0
o direttamente:
MACRO_STRING_DB EQU Stringa1 db 'Stringa da stampare', 0
Utilizzando quest'ultima macro, nel blocco dati del nostro programma possiamo scrivere semplicemente:
MACRO_STRING_DB
Quando l'assembler incontra questo nome simbolico lo espande in:
Stringa1 db 'Stringa da stampare', 0
Se, subito dopo la dichiarazione della macro MACRO_STRING_DB, scriviamo:
MACRO_2 EQU MACRO_STRING_DB
l'assembler si accorge che il nome simbolico MACRO_STRING_DB appartiene ad una macro già dichiarata in precedenza e quindi assegna il corpo di MACRO_STRING_DB a MACRO_2; in sostanza, dopo queste dichiarazioni, anche il corpo di MACRO_2 vale:
Stringa1 db 'Stringa da stampare', 0
La direttiva EQU si rivela utilissima in molte circostanze; in particolare, la possiamo utilizzare per implementare nuove istruzioni che non sono supportate dall'assembler in nostro possesso.
Supponiamo, ad esempio, di avere un vecchio assembler a 32 bit che non supporta l'istruzione CPUID; come già sappiamo, il codice macchina di questa istruzione è formato dai due opcodes 0Fh e A2h. Con l'ausilio di EQU ci basta scrivere:
CPUID EQU db 00001111b, 10100010b
oppure:
CPUID EQU db 0Fh, 0A2h
In questo modo, anche il nostro vecchio assembler sarà in grado di supportare l'istruzione CPUID!

24.4.2 La direttiva MACRO

Se nemmeno la direttiva EQU soddisfa le nostre esigenze, allora non ci resta che ricorrere alle multiline macro (macro multilinea); con le macro multilinea si può fare veramente di tutto ed è difficile che un programmatore possa pretendere di più.

La struttura generale di una macro multilinea comprende: La differenza sostanziale rispetto a EQU sta nel fatto che le macro dichiarate con la direttiva MACRO possono svilupparsi eventualmente su più linee; inoltre, come è stato appena detto, una macro multilinea può essere dotata anche di una lista opzionale di parametri.
Vediamo alcuni esempi pratici che illustrano le potenzialità di questo strumento.

L'esempio mostrato in precedenza, relativo alla creazione di stringhe con EQU, appare piuttosto banale; vediamo allora cosa può fare la direttiva MACRO. A tale proposito, scriviamo la seguente dichiarazione: Non ci vuole molto a capire che la macro MAKE_STRING ci permette di definire, non una sola stringa predefinita, ma qualunque numero di stringhe, ciascuna con il proprio nome e con il proprio corpo!
All'interno di un segmento di programma possiamo ora scrivere: Quando l'assembler incontra le precedenti linee, le espande in: Proviamo ora a definire una stringa C con MAKE_STRING; possiamo scrivere, ad esempio:
MAKE_STRING StringaC1, 'corpo stringa C1', 0
Il problema evidente riguarda lo zero finale dopo la virgola; l'assembler lo scambia per un inesistente terzo argomento e o ignora!
Per ovviare a questo inconveniente, MASM permette di racchiudere un qualsiasi argomento tra parentesi angolari <>; la precedente definizione diventa quindi:
MAKE_STRING StringaC1, <'corpo stringa C1', 0>
Nei precedenti capitoli abbiamo visto che con le CPU della famiglia 80x86 non è permesso il trasferimento dati da memoria a memoria; possiamo sopperire a questa carenza attraverso la seguente macro multilinea: Come si può notare, questa macro è formata da un nome MEM2MEM che simboleggia una istruzione MOV tra due generici operandi di tipo Mem; abbiamo poi la direttiva MACRO seguita da una lista formata dai due parametri, Destinazione e Sorgente, separati da virgole.
Il corpo della macro è formato da una istruzione PUSH che inserisce nello stack il contenuto del Sorgente e da una istruzione POP che estrae tale contenuto dallo stack e lo salva in Destinazione realizzando così il trasferimento dati richiesto; infine, al termine del corpo della macro è presente la direttiva ENDM.

A questo punto, all'interno del blocco codice del nostro programma possiamo scrivere istruzioni del tipo:
MEM2MEM Var1, Var2
(dove Var1 e Var2 sono, in questo caso, due variabili a 16 bit).

In fase di assemblaggio del programma, ogni volta che l'assembler incontra la precedente istruzione, effettua l'espansione della macro MEM2MEM; tale espansione consiste anche nel sostituire al parametro DEST l'argomento Var1 e al parametro SRC l'argomento Var2. Alla fine, come risulta anche dal listing file, si ottiene: In questo modo abbiamo l'impressione di copiare Var2 in Var1 con un'unica istruzione; si può anche notare che la macro MEM2MEM, senza alcuna modifica, gestisce anche operandi a 32 bit.
Come al solito, è fondamentale il fatto che l'espansione della macro porti alla generazione di codice che abbia un senso logico; se, ad esempio, Var2 è un operando di tipo Mem8, allora l'istruzione:
MEM2MEM Var1, Var2
provoca un messaggio di errore dell'assembler in quanto stiamo tentando di inserire nello stack un operando a 8 bit!
Bisogna anche prestare molta attenzione al fatto che l'uso disinvolto delle macro può portare a degli errori piuttosto subdoli e quindi difficili da individuare; nel caso, ad esempio, della macro MEM2MEM, nessuno ci impedisce di scrivere:
MEM2MEM ax, bx
In questo caso l'assembler utilizza BX come sorgente e AX come destinazione, generando codice Assembly del tutto lecito; se, però, scriviamo:
MEM2MEM ax, ebx
otteniamo una istruzione PUSH che inserisce nello stack i 32 bit di EBX e una istruzione POP che estrae dallo stack 16 bit e li copia in AX. L'assembler non rileva, ovviamente, alcun errore in quanto il codice appena espanso è sintatticamente legale; alla fine ci ritroviamo con 16 bit di dati in eccesso nello stack (stack disallineato)!

Come è stato detto in precedenza, il corpo di una macro multilinea può contenere praticamente qualsiasi cosa che abbia un senso logico; vediamo un altro esempio di una macro chiamata INIT_VECTOR che inizializza con il valore InitVal i NumElem elementi, da ElemSize byte ciascuno, di un generico vettore che si trova all'offset VectOffset. Come si può notare, si tratta del classico loop gestito da CX per inizializzare gli elementi di un vettore. Supponiamo ora di voler inizializzare con il valore 3500 i 10 elementi da 2 byte ciascuno di un vettore Vector1; possiamo scrivere allora: L'espansione della macro porterà alla generazione del seguente codice: Appare chiaro che il programmatore è tenuto a chiamare la macro passandole il corretto numero di argomenti; tali argomenti, inoltre, debbono avere determinate caratteristiche in modo che l'espansione della macro porti alla generazione di codice legale.

La struttura di INIT_VECTOR ci permette di analizzare un problema che si verifica quando nel corpo di una macro è presente un'etichetta (nel nostro caso init_loop); se, ad esempio, nel nostro programma sono presenti due chiamate alla macro INIT_VECTOR, verranno effettuate due espansioni che determineranno la presenza di due etichette (init_loop) aventi lo stesso nome!
Naturalmente, questa è una situazione illegale in quanto è proibito ridefinire un nome già definito in precedenza; una possibile soluzione a questo problema è rappresentata dall'uso della direttiva LOCAL. La direttiva LOCAL, se presente, deve essere la prima istruzione del corpo della macro; questa direttiva elenca una serie di nomi simbolici che verranno espansi dall'assembler sempre con nomi diversi in modo da evitare le ridefinizioni. Nel caso, quindi, della macro INIT_VECTOR, possiamo scrivere: Se questa macro viene espansa, ad esempio, 10 volte nel blocco codice del nostro programma, otterremo 10 nomi differenti per l'etichetta init_loop; questi nomi vengono scelti direttamente dall'assembler!

Tornando al caso generale, bisogna dire che il corpo di una macro multilinea può contenere chiamate ad altre macro le quali, naturalmente, devono essere già state dichiarate in precedenza; è anche permesso l'utilizzo di tutte le assembler time directives e degli assembler time operators. In questo modo è possibile indicare all'assembler quali parti di una macro devono essere espanse e quali no; è consigliabile comunque non abusare troppo di queste caratteristiche per evitare di scrivere programmi troppo contorti.
Possiamo dichiarare persino macro ricorsive; una macro è ricorsiva quando presenta nel proprio corpo una chiamata a se stessa. Le macro ricorsive verranno illustrate in un apposito capitolo dedicato all'implementazione della ricorsione in Assembly.

24.4.3 Le macro predefinite di MASM

Nel precedente paragrafo sono stati analizzati gli strumenti con i quali si possono creare macro personalizzate; il MASM dispone anche di altri strumenti con i quali si possono creare macro aventi una struttura predefinita. Esaminiamo, in particolare, le macro che si possono creare con le direttive REPT e WHILE.

La direttiva REPT (repeat) ci permette di creare macro il cui corpo viene ripetuto un certo numero di volte; il numero di ripetizioni da effettuare viene indicato da un valore immediato che deve essere specificato subito dopo la direttiva REPT. La struttura generale di questa macro predefinita è la seguente: Utilizziamo, ad esempio, REPT per sopperire ad una carenza delle CPU 8086 che non supportano istruzioni del tipo:
shl ax, 4
Invece di inserire 4 istruzioni ciascuna delle quali fa scorrere i bit di AX di una posizione verso sinistra, possiamo scrivere la seguente macro: A questo punto, nel blocco codice del nostro programma, possiamo scrivere istruzioni del tipo:
MULTISHL ax, 4
Subito dopo la fase di assemblaggio, si ottiene la seguente espansione della macro: La direttiva WHILE ci permette di creare macro il cui corpo viene ripetuto un numero di volte che dipende dal risultato della valutazione di un'espressione booleana; l'espressione booleana deve essere specificata subito dopo la direttiva WHILE. La struttura generale di questa macro predefinita è la seguente: In pratica, viene valutata espressione e se si ottiene TRUE viene espanso il corpo della macro; successivamente viene nuovamente valutata espressione e se si ottiene ancora TRUE si verifica una nuova espansione del corpo della macro. Il processo continua finché espressione non diventa FALSE; in casi di questo genere è importante evitare, come al solito, la creazione di un loop infinito.
Molto spesso la direttiva WHILE viene usata in combinazione con la direttiva EXITM (exit macro) che permette di uscire prematuramente da una qualsiasi macro multilinea; nel corpo di numerose macro è facile incontrare codice del tipo: Bisogna ribadire che i vari assembler forniscono numerose altre direttive che spesso creano problemi di compatibilità; maggiori dettagli possono essere reperiti sui manuali di MASM.

24.5 Analogie tra macro e sottoprogrammi

Un aspetto molto importante da chiarire riguarda il fatto che qualcuno, osservando la struttura e le caratteristiche di una macro multilinea, potrebbe pensare di trovarsi davanti ad un vero e proprio sottoprogramma; in effetti, l'analogia esiste perché, come vedremo nel prossimo capitolo, un sottoprogramma è formato da un nome simbolico, da una direttiva e da un corpo.
Questa analogia però è solamente formale in quanto, in realtà, tra le macro e i sottoprogrammi esiste una notevole differenza sostanziale; ovviamente, si tratta della stessa differenza che esiste tra una dichiarazione e una definizione!
Una macro rappresenta una semplice dichiarazione attraverso la quale diamo all'assembler una serie di informazioni sulla struttura della macro stessa; nella fase di assemblaggio l'assembler utilizza queste informazioni per effettuare l'espansione della macro.
Una procedura, invece, necessita di una definizione e ciò comporta, come sappiamo, l'allocazione della memoria necessaria per contenere il corpo della procedura stessa; di conseguenza, una procedura ha anche un suo preciso indirizzo di memoria, che coincide con l'indirizzo della sua prima istruzione.

Un'altra questione molto importante riguarda il fatto che su alcuni libri si afferma che le macro multilinea, in determinate circostanze, possono rappresentare una valida alternativa ai sottoprogrammi; questa affermazione è corretta purché si tenga sempre presente la differenza sostanziale che esiste tra macro e sottoprogrammi, cioè tra dichiarazioni e definizioni.
Come vedremo nel prossimo capitolo, un sottoprogramma viene caricato in memoria assieme al programma principale; il programma principale può chiamare un sottoprogramma quando vuole e dove vuole. Ciascuna di queste chiamate corrisponde ad un salto (trasferimento del controllo) all'indirizzo di memoria da cui inizia il corpo del sottoprogramma; al termine del sottoprogramma, il controllo torna al programma principale (o, in generale, a chi ha effettuato la chiamata).
Questa tecnica di programmazione presenta il vantaggio di farci risparmiare parecchi byte di codice; infatti, se il programma principale deve chiamare 100 volte un determinato sottoprogramma, non vengono effettuati 100 salti a 100 copie diverse del sottoprogramma, ma 100 salti sempre allo stesso sottoprogramma.
Lo svantaggio evidente di questa tecnica di programmazione è rappresentato, invece, dal sensibile calo delle prestazioni; è chiaro, infatti, che i continui salti dal programma principale ai sottoprogrammi e viceversa, determinano una certa diminuzione della velocità di esecuzione.

Se, al posto di un sottoprogramma si utilizza una macro, si ottengono vantaggi e svantaggi di segno opposto; se il programma principale deve chiamare 100 volte una stessa macro, si ottengono 100 espansioni del corpo della macro, che fanno aumentare notevolmente le dimensioni del codice. Il vantaggio delle macro sta nel fatto che il loro uso elimina parecchi salti favorendo quindi uno stile di programmazione di tipo sequenziale; tanto minore è il numero di salti, tanto maggiore sarà la velocità del programma.

In definitiva, se il nostro programma ha bisogno, ad esempio, di utilizzare più volte uno stesso algoritmo, possiamo inserire tale algoritmo in un sottoprogramma o in una macro; per decidere quale sia la scelta migliore basta fare un semplice ragionamento. Se l'algoritmo è molto grosso e viene chiamato parecchie volte dal programma principale, conviene inserirlo in un sottoprogramma; in questo modo si ottiene un programma molto compatto e sufficientemente veloce. Se l'algoritmo è relativamente piccolo e viene chiamato poche volte dal programma principale, conviene inserirlo in una macro multilinea; in questo modo si ottiene un programma sufficientemente compatto e molto veloce.

Le potenzialità delle macro multilinea vanno ben oltre gli aspetti appena illustrati; per descrivere in modo esauriente questo argomento non basterebbe un intero libro. Per maggiori dettagli si può fare riferimento al manuale di MASM; si tenga presente, comunque, che certe caratteristiche delle macro possono variare sintatticamente da assembler ad assembler, con conseguenti problemi di compatibilità.