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.