Assembly Avanzato con NASM

Capitolo 1: Introduzione


Servendoci dei concetti fondamentali acquisiti nella sezione Assembly Base, possiamo ora dedicarci allo studio di una numerosa serie di argomenti importanti, che richiedono tecniche di programmazione avanzate; un tale obiettivo può essere raggiunto in modo relativamente semplice grazie al fatto che la conoscenza del linguaggio Assembly, permette al programmatore di affrontare con competenza qualsiasi aspetto legato al mondo dei PC.
In effetti, come vedremo chiaramente nei capitoli successivi, buona parte della documentazione tecnica sui PC richiede al programmatore una adeguata padronanza proprio dell'Assembly; in ogni caso, è consigliabile anche una infarinatura generale sul linguaggio C, notoriamente utilizzato come "linguaggio di sistema".

Gli argomenti trattati in questo tutorial hanno anche l'importante finalità di facilitare la migrazione verso la cosiddetta modalità operativa protetta, largamente utilizzata dalle CPU della famiglia 80x86; a tale proposito, bisogna ribadire che la conoscenza della modalità operativa reale si rivela determinante per capire la particolare struttura interna delle moderne CPU 80x86, le quali mantengono tutte la "compatibilità verso il basso".

L'assembler di riferimento nella sezione Assembly Avanzato è il NASM; in ogni caso, il codice sorgente degli esempi presentati nei vari capitoli sarà disponibile sia per NASM che per MASM (che potete trovare anche nella sezione Codice sorgente di esempio per la sezione assembly avanzato dell’ Area Downloads di questo sito).

Tutti i programmi presentati nella sezione Assembly Avanzato sono rivolti all'ambiente operativo rappresentato dalla modalità reale a 16 bit del DOS; come è stato spiegato nella sezione Assembly Base, il termine generico DOS verrà utilizzato per indicare, indifferentemente, FreeDOS, MS-DOS, il Prompt di MS-DOS e i vari emulatori DOS.
L’Assembly rappresenta il set di istruzioni della CPU ed è quindi del tutto indipendente dal SO che si sta usando; con l’Assembly possiamo scrivere programmi per DOS, Windows, Mac OSX, Linux, FreeBSD, Android, etc, sfruttando le istruzioni delle CPU su cui girano tali SO.

Come abbiamo visto nel tutorial Assembly Base, esistono svariati progetti di macchine virtuali che supportano il DOS, a cui si aggiungono gli emulatori DOS; la Figura 1.1 mostra, ad esempio, FreeDOS eseguito in VirtualBox. Un altro progetto estremamente interessante è 86Box, una macchina virtuale capace di emulare l’hardware dei PC e la relativa famiglia di CPU, a partire dalla vecchissima 8088, sino ad arrivare ai Pentium e Celeron della Intel e alla K6 della AMD; la Figura 1.2 mostra MS-DOS 6.22 in esecuzione sotto 86Box. Le macchine virtuali, come sappiamo, presentano un problema legato alla possibilità di condividere file con il SO "ospitante" (host); in particolare, nel caso in cui il SO "ospitato" (guest) è il DOS, lo scambio di file con l’host comporta procedure spesso piuttosto contorte.
Sotto questo punto di vista, le versioni più recenti di 86Box introducono una interessante novità che consiste nella possibilità di montare una directory come se fosse un CD-ROM virtuale. In sostanza, una volta che nella macchina virtuale abbiamo installato il DOS e un driver per il controller del lettore CD, possiamo richiedere il montaggio di una qualsiasi directory dell’host come se fosse un supporto CD-ROM; in questo modo, possiamo usare tale directory per scambiare agevolmente file.
Anche questa novità presenta però delle limitazioni, in quanto tutte le operazioni per lo scambio o modifica di file condivisi, devono avvenire con la directory/CD-ROM smontata; quindi, ogni volta che dobbiamo spostare, cancellare o modificare un file condiviso, siamo costretti a fare prima lo smontaggio e poi il rimontaggio della directory/CD-ROM.
Le macchine virtuali rappresentano la scelta migliore quando è richiesta una emulazione veramente accurata dell’hardware; se però vogliamo evitare tutte le complicazioni esposte in precedenza, possiamo servirci direttamente di un qualsiasi emulatore DOS. Nel tutorial Assembly Base è stato presentato come esempio il caso di DOSBox; esistono anche dei fork come DOSBox-X e DOSBox-staging che estendono notevolmente le caratteristiche di DOSBox. La Figura 1.3 mostra DOSBox-X in esecuzione. Il vantaggio notevole degli emulatori DOS è dato principalmente dalla totale integrazione con l’host, compresa quindi la condivisione dei file nel modo più semplice possibile; inoltre, a differenza di quanto accade con le macchine virtuali, non è necessario installare alcun driver di periferica (lettore CD, mouse, etc), in quanto tutto il necessario viene reso disponibile dall'emulatore stesso.
Su Internet si può effettuare il download di tutti i software citati in precedenza; sui rispettivi siti, è anche disponibile una abbondante documentazione che fornisce ogni dettaglio sull’installazione e l’utilizzo.

1.1 Compatibilità tra NASM e MASM

Nonostante NASM e MASM seguano entrambi le convenzioni introdotte dalla Intel per la sintassi del linguaggio Assembly, tra i due assembler esistono alcune differenze di cui si deve tenere conto; per fortuna, in molti casi si possono adottare degli accorgimenti che permettono di ridurre al minimo il lavoro di conversione del codice sorgente da NASM a MASM o viceversa.

Un primo aspetto positivo riguarda il fatto che, con entrambi gli assembler, la definizione delle variabili semplici (tipo BYTE, WORD, DWORD, etc) segue la stessa sintassi; le uniche differenze si riscontrano per i dati strutturati. Come abbiamo visto, però, nel tutorial Assembly Base, si tratta solo di aspetti formali, per cui si può risolvere questo problema scrivendo il codice sorgente in Assembly "puro"; in sostanza, basta evitare di ricorrere alla sintassi avanzata, che purtroppo presenta notevoli incompatibilità tra i due assembler.

Consideriamo ora la seguente definizione, valida sia in NASM che in MASM:
VarWord dw 3F2Bh
Supponendo che questa definizione si trovi all'offset 0FFAh di un segmento dati, bisogna ricordare che in NASM il nome VarWord rappresenta l'offset 0FFAh della variabile, mentre il simbolo [VarWord] rappresenta il suo contenuto 3F2Bh; in MASM, invece, offset VarWord rappresenta l'offset 0FFAh della variabile, mentre VarWord rappresenta il suo contenuto 3F2Bh.

Queste differenze sono facilmente superabili grazie al fatto che, innanzi tutto, anche il MASM accetta la sintassi [VarWord] per indicare il contenuto 3F2Bh della variabile; per quanto riguarda poi l'operatore OFFSET di MASM, possiamo risolvere il problema in NASM ponendo all'inizio del codice sorgente la seguente definizione:
%idefine offset
La "i" di %idefine significa case insensitive, per cui tra i nomi offset e OFFSET non c'è alcuna differenza. Abbiamo quindi creato una macro di nome offset, priva di corpo; a questo punto, anche con il NASM possiamo scrivere istruzioni del tipo:
mov bx, offset VarWord
In presenza di registri per il segment override, la sintassi MASM permette di scrivere simboli del tipo es:[VarWord] e ds:[di-2], mentre in NASM si deve scrivere [es:VarWord] e [ds:di-2]; per fortuna, anche in questo caso il MASM supporta la sintassi del NASM, ma in presenza di offset+spiazzamento è necessario usare le parentesi (ad esempio, [ds:(di-2)]).
Se l'offset è rappresentato da un registro singolo, MASM richiede la sintassi es:[di] e non accetta la forma [es:di] supportata da NASM; il problema si risolve facilmente scrivendo [es:(di+0)].

Se vogliamo accedere al byte più significativo (MSB) di VarWord, in MASM dobbiamo scrivere:
mov al, byte ptr [VarWord+2]
Con il NASM la sintassi è:
mov al, byte [VarWord+2]
Anche in questo caso, con il NASM risolviamo il problema definendo la seguente macro priva di corpo:
%idefine ptr
Sia in NASM che in MASM, il simbolo $ rappresenta il cosiddetto location counter, che indica l'offset corrente all'interno del segmento di programma che l'assembler sta esaminando; si tratta di un simbolo utilizzabile quindi solo in fase di assemblaggio.
Bisogna ricordare che in NASM, una direttiva del tipo:
ORG 150
sposta il location counter in avanti di 150 byte a partire dall'offset corrente (in cui si trova la stessa direttiva ORG); nel caso di MASM, invece, la precedente direttiva posiziona il location counter all'offset 150 del segmento di programma corrente!
Se si ha la necessità di convertire da NASM a MASM (o viceversa) codice sorgente contenente direttive ORG al suo interno, è necessario quindi tenere conto di questa differenza effettuando gli opportuni calcoli.

In generale, se ci si trova a dover convertire frequentemente codice sorgente Assembly da un assembler all'altro, la cosa migliore da fare consiste nel rinunciare all'uso della sintassi avanzata.

1.1.1 Novità di NASM 2.15.5

A partire dalla versione 2.15.5, NASM introduce una serie di novità che contribuiscono a migliorare notevolmente la compatibilità con MASM; a tale proposito, all'inizio del codice sorgente bisogna inserire la direttiva:
%use MASM
Con questa direttiva, una prima importante novità è che la sintassi per i segmenti di programma diventa del tutto simile a quella del MASM; ad esempio, al posto di
SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA
possiamo scrivere
DATASEGM SEGMENT ALIGN=16 PUBLIC USE16 CLASS=DATA
In questo caso, analogamente a quanto prevede il MASM, il segmento DATASEGM va anche chiuso con la direttiva
DATASEGM ENDS
Alla fine del file sorgente si può inserire la direttiva END (o END nome_entry_point per il modulo principale) che viene del tutto ignorata da NASM.

Uno schema simile può essere ora usato per racchiudere il corpo delle procedure; una procedura, ad esempio, printStr, di tipo NEAR, viene aperta dalla direttiva
printStr proc near
e viene chiusa dalla direttiva
printStr endp
Le due nuove parole chiave OFFSET e PTR rendono più chiari gli indirizzamenti. Se varWord è il nome di una variabile a 16 bit, allora l’istruzione
mov ax, offset varWord
copia in AX l'offset di varWord; in pratica, OFFSET non è altro che la macro priva di corpo illustrata in precedenza e viene del tutto ignorata da NASM.
Analogamente, l’istruzione in stile MASM
mov ax, word ptr varWord
copia in AX il contenuto di varWord ed è perfettamente equivalente quindi a
mov ax, word [varWord]
Al posto di TWORD ora anche con NASM si può scrivere TBYTE per definire una variabile da 10 byte; a differenza di quanto accade con MASM, però, una tale variabile può essere inizializzata solo con un numero in virgola mobile.

Indipendentemente dall'uso o meno della direttiva %use masm, ora anche NASM supporta nativamente la sintassi MASM per definire variabili inizializzate o non inizializzate.
Se vogliamo definire una variabile varByte a 8 bit non inizializzata, possiamo scrivere
varByte db ?
anziché
varByte resb 1
Se vogliamo definire un vettore vectWord non inizializzato di 200 elementi di tipo WORD, possiamo scrivere
vectWord dw 200 dup (?)
anziché
vectWord resw 200
Per definire un vettore vectByte inizializzato di 6 elementi di tipo BYTE, possiamo scrivere (notare il '%' all'inizio della lista)
vectByte db %(6, 8, 3, 4, 1, 9)
Analogamente, se vogliamo definire vectByte come vettore di 100 elementi di tipo BYTE, tutti inizializzati con lo stesso valore 40, possiamo scrivere
vectByte db 100 dup (40)
Un indirizzo (effective address) [BASE+INDEX+DISP] può essere scritto ora in stile MASM come DISP[BASE+INDEX]; ad esempio:
mov dx, 03F2h[bx+di]
anziché
mov dx, [bx+di+03F2h]
Per gli indirizzi Seg:Offset che comprendono un registro di segmento, è ora possibile scrivere in stile MASM Seg:[Offset]; ad esempio:
mov bx, es:[0010h]
anziché
mov bx, [es:0010h]
Nel tutorial Assembly Base abbiamo visto che la sintassi NASM per l’istruzione LEA appare poco chiara; ad esempio,
lea bx, [varStr]
carica in BX l’offset (effective address) di varStr e non il contenuto di varStr; a partire da NASM 2.15.5 si può utilizzare anche la sintassi MASM per scrivere
lea bx, varStr
Le funzionalità offerte da NASM sono veramente numerose; si consiglia pertanto di consultare il documento NASMDOC.TXT allegato all’assembler.

1.2 Librerie di I/O IOLIBCM e IOLIBEX

Le librerie COMLIB e EXELIB, utilizzate nella sezione Assembly Base per gestire l’I/O di stringhe e numeri, sono state scritte molti anni fa con il Borland Turbo Assembler (TASM); tali librerie sono state ora interamente riscritte e ottimizzate con il NASM.
Nella sezione Librerie di supporto per il corso assembly dell’ Area Downloads di questo sito) sono presenti le due librerie IOLIBCM e IOLIBEX che risultano totalmente compatibili (e quindi interscambiabili) con COMLIB e EXELIB; per la generazione dei relativi object file è richiesto NASM versione 2.15.5 o superiore.
Nel tutorial Assembly Avanzato verranno utilizzate le librerie IOLIBCM e IOLIBEX al posto di COMLIB e EXELIB.