Assembly Base con NASM
Capitolo 23: Istruzioni varie
In questo capitolo viene brevemente descritto il principio di funzionamento di
varie istruzioni della CPU che, in genere, compaiono meno frequentemente
nei programmi Assembly; per maggiori dettagli, si consiglia di consultare
i manuali dei vari assembler o la documentazione tecnica relativa ai vari
modelli di CPU.
23.1 L'istruzione BOUND
Con il mnemonico BOUND si indica l'istruzione Check Array Index Against
Bounds (verifica dei limiti dell'indice di un vettore); questa istruzione, che
è disponibile solo per le CPU 80186 e superiori, ha lo scopo di stabilire
se l'indice di un vettore è compreso tra due valori che rappresentano il limite
inferiore e il limite superiore dell'indice stesso.
In modalità reale, l'unica forma lecita per l'istruzione BOUND è la seguente:
BOUND Reg16, Mem16:Mem16
Per capire bene il funzionamento dell'istruzione BOUND è necessario tenere
presente che il termine indice in questo caso si riferisce, non alla
posizione di un elemento all'interno di un vettore, bensì all'offset che individua
l'elemento stesso; consideriamo, ad esempio, il seguente vettore di 8 WORD
definito all'offset 00B8h di un segmento dati DATASEGM:
vectWord dw 3FB0h, 2C8Ah, 99F2h, 1DAEh, 3BF2h, 8CA9h, 32B1h, 900Dh
Come possiamo notare, l'offset del primo elemento (3FB0h) è 00B8h;
l'offset dell'ottavo e ultimo elemento (900Dh) è:
00B8h + ((8 - 1) * 2) = 00B8h + 14 = 00B8h + Eh = 00C6h
In sostanza, gli elementi (WORD) di vectWord sono compresi tra
l'offset 00B8h (estremo inferiore) e l'offset 00C6h (estremo
superiore); per accedere correttamente ad uno qualsiasi degli 8 elementi di
vectWord dobbiamo quindi specificare un offset compreso tra questi due
limiti!
Per sapere se stiamo accedendo ad un elemento che appartiene al nostro vettore,
possiamo servirci dell'istruzione BOUND; tale istruzione richiede due
operandi espliciti che, formalmente, ricoprono il ruolo di DEST e
SRC. L'operando DEST è di tipo Reg16 e contiene l'offset
dell'elemento a cui vogliamo accedere; l'operando SRC è di tipo
Mem16:Mem16 e contiene il limite inferiore (nella WORD meno
significativa) e il limite superiore (nella WORD più significativa).
Il codice macchina dell'istruzione BOUND è formato dall'opcode 62h
seguito dal campo mod_reg_r/m; quando la CPU incontra questo codice
macchina, assume il seguente comportamento:
- se (Reg16 ≥ limite inferiore) AND (Reg16 ≤ limite superiore)
l'esecuzione prosegue normalmente
- se (Reg16 < limite inferiore) OR (Reg16 > limite superiore)
viene generata una INT 05h
In pratica, se il contenuto del Reg16 è compreso tra i limiti inferiore
e superiore, l'esecuzione prosegue normalmente; se, invece, il contenuto del
Reg16 è maggiore del limite superiore o minore del limite inferiore,
viene generata una INT 05h.
Nell'eseguire l'istruzione BOUND, la CPU tratta il contenuto del
Reg16 come numero intero con segno a 16 bit in complemento a
2; ciò permette anche la gestione di un eventuale sconfinamento al di
sotto dell'offset 0000h!
Supponiamo, ad esempio, di avere un vettore di WORD che inizia dall'offset
0000h di un segmento dati; se proviamo ad accedere alla WORD che
precede il primo elemento del vettore, ci troviamo ad operare sull'offset:
0000h - 2 = FFFEh = 65534
Nell'insieme degli interi con segno a 16 bit in complemento a 2,
il valore 65534 codifica il numero negativo:
65534 - 65536 = -2
Chiaramente, -2 è strettamente minore di 0000h; di conseguenza, una
eventuale istruzione BOUND genera una INT 05h!
23.1.1 Uso pratico dell'istruzione BOUND
In base alle considerazioni esposte in precedenza, possiamo dire che per gestire
in modo corretto l'accesso ad un vettore, dobbiamo posizionare BOUND
immediatamente prima di una istruzione che accede agli elementi del vettore
stesso; nel caso, ad esempio, del vettore vectWord citato in precedenza,
dobbiamo innanzi tutto definire una variabile del tipo:
vectBounds dd 00C600B8h
Come si può notare, vectBounds contiene il limite inferiore 00B8h
nella WORD meno significativa e il limite superiore 00C6h nella
WORD più significativa; a questo punto, se DS:BX punta a
vectWord, possiamo scrivere istruzioni del tipo:
Se BOUND si accorge che BX contiene un indice fuori limite,
genera una INT 05h che permette alla ISR associata di terminare
il programma in esecuzione; in questo modo, si impedisce che la successiva
istruzione MOV scriva dei dati in un'area della memoria che non
appartiene a vectWord!
Se si decide di trattare l'indice fuori limite come condizione di errore,
il problema fondamentale da affrontare consiste nello scrivere la necessaria
ISR personalizzata; il programma di Figura 23.1 illustra un esempio
pratico, molto simile al programma INT01H.ASM illustrato nella Figura
20.13 del Capitolo 20.
Il programma di Figura 23.1 termina correttamente, senza mostrare alcun
messaggio di errore; ciò accade in quanto non si verifica alcuno sconfinamento
dai limiti di vectWord.
Se vogliamo provocare uno sconfinamento, con conseguente chiamata della
ISR associata alla INT 05h, possiamo procedere in diversi modi;
prima del loop, ad esempio, possiamo modificare l'inizializzazione del contatore
CX, scrivendo:
mov cx, MAX_INDEX + 1
In questo modo, l'esecuzione del programma terminerà con un messaggio di errore
visualizzato dalla procedura new_int05h; ciò accade in quanto il programma
tenta di accedere ad un inesistente undicesimo elemento di vectWord!
Un altro metodo consiste nell'inizializzare BX (prima del loop) con
l'istruzione:
mov bx, vectWord - 2
In questo modo, stiamo cercando di accedere alla WORD che precede il
primo elemento di vectWord; il risultato è che la INT 05h viene
generata alla prima iterazione del loop!
Si noti la comodità offerta dall'uso delle costanti simboliche; se, ad esempio,
vogliamo operare su un vettore vectWord da 15 elementi, non
dobbiamo fare altro che modificare la sola dichiarazione:
%assign MAX_INDEX 15
Questa semplice modifica si ripercuote automaticamente su tutto il codice di
BOUND.ASM; in particolare, notiamo la definizione di vectWord:
vectWord resw MAX_INDEX
e il calcolo dell'offset relativo all'ultimo elemento di vectWord:
mov ax, vectWord + ((MAX_INDEX - 1) * 2)
Quando tutte le costanti numeriche presenti in un programma sono gestibili
attraverso le rispettive dichiarazioni, si dice che il programma è
parametrizzato.
Nell'esempio di Figura 23.1 abbiamo deciso di trattare l'indice fuori limite
come condizione di errore; in una situazione del genere è necessario ricorrere
a tutti gli strumenti (come BOUND) necessari per evitare operazioni di
I/O in aree della memoria riservate ad altre informazioni.
Alcuni linguaggi di alto livello, impediscono al programmatore di sconfinare
(in modo esplicito) da un vettore; un caso emblematico è rappresentato dal
linguaggio Pascal.
Non sempre, però, l'indice fuori limite rappresenta una condizione di errore;
può capitare, ad esempio, di avere la necessità di sconfinare "consapevolmente"
dai limiti di un vettore. Questa situazione è del tutto normale per i
programmatori C/C++ e Assembly; d'altra parte, la filosofia di
tali linguaggi consiste proprio nel lasciare al programmatore la massima libertà
d'azione.
Consideriamo, ad esempio, le seguenti definizioni:
Anche un programmatore Assembly alle prime armi, si accorge subito che:
In questo caso, il programmatore sa benissimo che dopo vectWord, sono
presenti altre tre WORD memorizzate, due in varDword e una in
varWord; nessuno allora ci impedisce di accedere a queste tre WORD
attraverso uno spiazzamento (4, 6, 8) calcolato rispetto
all'offset di vectWord!
Bisogna prestare molta attenzione quando si applica la precedente tecnica in
un programma scritto in C/C++; infatti, in Assembly siamo sicuri
del fatto che vectWord, varDword e varWord sono disposte
in memoria in modo consecutivo e contiguo, mentre non è detto che ciò sia vero
in C/C++!
Lo standard ANSI (American National Standards Institute) per il
C/C++ non impone alcuna regola su tale aspetto; ciò significa che i
compilatori C/C++, sono liberi di disporre i dati in memoria come meglio
credono!
23.1.2 Effetti provocati da BOUND sugli operandi e sui flags
L'esecuzione dell'istruzione BOUND non provoca alcuna modifica sul
contenuto degli operandi SRC e DEST.
L'esecuzione dell'istruzione BOUND non modifica alcun campo del
Flags Register.
23.2 Le istruzioni
BSF e
BSR
Le CPU 80386 e superiori, forniscono le due istruzioni BSF e
BSR, attraverso le quali è possibile scandire il contenuto di un
operando, alla ricerca del primo bit che si trova a livello logico 1;
l'istruzione BSF effettua la scansione in avanti, mentre l'istruzione
BSR effettua la scansione all'indietro.
23.2.1 L'istruzione BSF
Con il mnemonico BSF si indica l'istruzione Bit Scan Forward
(scansione in avanti di un operando, alla ricerca del primo bit a livello
logico 1); le uniche forme lecite per questa istruzione, sono le
seguenti:
L'istruzione BSF richiede esplicitamente due operandi che ricoprono il
ruolo di SRC e DEST; l'operando DEST deve essere di tipo
Reg16 o Reg32, mentre l'operando SRC può essere di tipo
Reg o Mem e deve avere la stessa ampiezza in bit di DEST.
Il codice macchina di BSF è formato dall'opcode 0Fh BCh seguito
dal campo mod_reg_r/m; quando la CPU incontra questo codice
macchina, scandisce in avanti (cioè, a partire dal bit meno significativo) il
contenuto di SRC, alla ricerca del primo bit a livello logico 1.
Se il bit viene trovato, la CPU memorizza in DEST la posizione
che il bit stesso assume in SRC; per segnalare che la ricerca ha avuto
successo, la CPU pone, inoltre, ZF=0.
Se il bit non viene trovato (e ciò implica SRC=0), la CPU lascia
indefinito il contenuto di DEST; per segnalare che la ricerca NON
ha avuto successo, la CPU pone, inoltre, ZF=1.
Supponiamo, ad esempio, di avere la seguente definizione:
varWord dw 0011001100100000b
Si nota subito che partendo dal bit meno significativo (posizione 0) e
scandendo in avanti varWord, in posizione 5 incontriamo il primo
bit che vale 1; in presenza allora dell'istruzione:
bsf cx, [varWord]
la CPU pone CX=5 e ZF=0.
23.2.2 L'istruzione BSR
Con il mnemonico BSR si indica l'istruzione Bit Scan Reverse
(scansione all'indietro di un operando, alla ricerca del primo bit a livello
logico 1); le uniche forme lecite per questa istruzione, sono le
seguenti:
L'istruzione BSR richiede esplicitamente due operandi che ricoprono il
ruolo di SRC e DEST; l'operando DEST deve essere di tipo
Reg16 o Reg32, mentre l'operando SRC può essere di tipo
Reg o Mem e deve avere la stessa ampiezza in bit di DEST.
Il codice macchina di BSR è formato dall'opcode 0Fh BDh seguito
dal campo mod_reg_r/m; quando la CPU incontra questo codice
macchina, scandisce all'indietro (cioè, a partire dal bit più significativo) il
contenuto di SRC, alla ricerca del primo bit a livello logico 1.
Se il bit viene trovato, la CPU memorizza in DEST la posizione
che il bit stesso assume in SRC; per segnalare che la ricerca ha avuto
successo, la CPU pone, inoltre, ZF=0.
Se il bit non viene trovato (e ciò implica SRC=0), la CPU lascia
indefinito il contenuto di DEST; per segnalare che la ricerca NON
ha avuto successo, la CPU pone, inoltre, ZF=1.
Supponiamo, ad esempio, di avere la seguente definizione:
varWord dw 0011001100100000b
Si nota subito che partendo dal bit più significativo (posizione 15) e
scandendo all'indietro varWord, in posizione 13 incontriamo il
primo bit che vale 1; in presenza allora dell'istruzione:
bsr cx, [varWord]
la CPU pone CX=13 e ZF=0.
23.2.3 Effetti provocati da BSF e BSR, sugli operandi e sui flags
L'esecuzione delle istruzioni BSF e BSR, provoca la modifica
del solo operando DEST; il contenuto dell'operando SRC rimane
invariato.
Bisogna prestare particolare attenzione alle istruzioni del tipo:
bsf bx, [es:bx]
Dopo l'esecuzione di questa istruzione, la coppia ES:BX punta ad un
indirizzo indefinito della memoria!
L'esecuzione delle istruzioni BSF e BSR, modifica i campi
ZF, CF, OF, SF, AF e PF del registro
FLAGS; il solo flag ZF ha significato, mentre i flags CF,
OF, SF, AF e PF sono indefiniti.
Se la scansione ha successo, la CPU pone ZF=0; se la scansione non
ha successo (SRC=0), la CPU pone ZF=1.
23.3 L'istruzione BSWAP
Con il mnemonico BSWAP si indica l'istruzione Byte Swap (inversione
dell'ordine dei byte in una DWORD); questa istruzione, che è disponibile solo
per le CPU 80486 e superiori, ha lo scopo di invertire la disposizione dei
quattro BYTE presenti in un operando di tipo DWORD.
L'unica forma lecita per l'istruzione BSWAP è la seguente:
BSWAP Reg32
Il codice macchina di BSWAP è formato dai due opcodes 00001111b e
11001_reg; quando la CPU incontra questo codice macchina, accede al
contenuto dell'operando Reg32, legge i quattro BYTE che occupano le
posizioni 0, 1, 2, 3 e li reinserisce in Reg32
in ordine inverso (cioè, nelle posizioni 3, 2, 1, 0).
Supponiamo, ad esempio, di avere EDX=03F1A23Fh; subito dopo l'esecuzione
dell'istruzione:
bswap edx
otteniamo EDX=3FA2F103h.
L'istruzione BSWAP si rivela molto utile quando si ha la necessità di
convertire un programma in modo da adattarlo ad una diversa piattaforma hardware;
non bisogna dimenticare, infatti, che tra le varie piattaforme hardware possono
esistere diverse convenzioni in relazione alla disposizione dei numeri binari in
memoria.
Sappiamo, ad esempio, che le piattaforme hardware basate sulle CPU 80x86,
dispongono i dati in memoria secondo la convenzione little endian; in base
a tale convenzione, i dati di tipo WORD, DWORD, etc, vengono disposti
in memoria con il BYTE meno significativo che occupa l'indirizzo più basso.
In altre piattaforme hardware (come quelle basate sulle CPU della
Motorola), viene seguita la convenzione opposta, chiamata big endian;
in base a tale convenzione, i dati di tipo WORD, DWORD, etc, vengono
disposti in memoria con il BYTE meno significativo che occupa l'indirizzo
più alto.
Teoricamente è possibile utilizzare BSWAP anche con un operando di tipo
Reg16; in un caso del genere, si ottengono risultati privi di senso!
Se abbiamo la necessità di scambiare i BYTE di una WORD, possiamo
servirci dell'istruzione XCHG; ad esempio, per scambiare di posto i due
BYTE del registro DX, possiamo scrivere:
xchg dh, dl
Quando utilizziamo l'istruzione BSWAP in un programma, dobbiamo ricordarci di
inserire la direttiva:
CPU 486
Se stiamo utilizzando un vecchio assembler che non supporta questa direttiva,
possiamo inserire direttamente il codice macchina dell'istruzione; ad esempio, per
applicare BSWAP al registro EDX, tenendo conto dell'operand size
prefix 01010101b=66h e del fatto che EDX=010b, dobbiamo scrivere:
db 01010101b, 00001111b, 11001010b ; bswap edx
23.3.1 Effetti provocati da BSWAP sugli operandi e sui flags
L'esecuzione dell'istruzione BSWAP inverte l'ordine dei BYTE che
formano l'unico operando presente.
L'esecuzione dell'istruzione BSWAP non modifica alcun campo del registro
FLAGS.
23.4 Le istruzioni
BT,
BTC,
BTR,
BTS
Le CPU 80386 e superiori, forniscono una serie di istruzioni che permettono
di testare lo stato di un qualsiasi bit presente in un operando di tipo Reg
o Mem; tali istruzioni vengono indicate con i mnemonici BT,
BTC, BTR e BTS.
Per chiarire il principio di funzionamento di queste istruzioni è necessario
analizzare le convenzioni adottate dalla Intel per indicare la posizione
di un determinato bit all'interno di un registro o di una locazione di memoria;
partiamo allora dai concetti fondamentali di bitBase e bitOffset.
Con il termine bitBase si indica il bit in posizione 0 di un
operando Reg o Mem.
Con il termine bitOffset si indica un numero con segno che rappresenta
la distanza in bit, tra il bitBase e il bit che vogliamo testare.
Se il bitBase si trova in un Reg a n bit, allora il
bitOffset deve essere un numero intero senza segno compreso tra 0 e
n-1; la Figura 23.2 illustra un esempio che si riferisce al bit in posizione
12 del registro CX.
Nell'esempio di Figura 23.2 notiamo che bitBase è il bit in posizione 0
di CX, mentre bitOffset vale 12; per ovviare al fatto che il
programmatore potrebbe specificare un bitOffset che sconfina dai limiti
dell'operando Reg, la CPU si serve, in realtà, del resto della
divisione intera (MOD) tra il contenuto di bitOffset e n (dove
n è l'ampiezza in bit del Reg che contiene il bitBase).
Nel caso di Figura 23.2, abbiamo specificato un bitOffset compatibile con
l'ampiezza di un Reg a 16 bit; infatti:
12 MOD 16 = Resto di 12 / 16 = 12
Se avessimo specificato, invece, bitOffset=132, la CPU avrebbe
calcolato:
132 MOD 16 = Resto di 132 / 16 = 4
e ci avrebbe restituito lo stato del bit in posizione 4 di CX!
Se il bitBase si trova in un operando di tipo Mem, allora il
bitOffset deve essere un numero intero con segno compreso tra il limite
inferiore -231 e il limite superiore +(231-1);
naturalmente, il bitBase è il bit in posizione 0 della locazione di
memoria specificata da Mem.
La Figura 23.3 illustra un esempio che si riferisce al bit in posizione -8
rispetto al bitBase della locazione di memoria puntata da ES:BX.
Nell'esempio di Figura 23.3 notiamo appunto che bitBase è il bit in posizione
0 della locazione di memoria puntata da ES:BX, mentre bitOffset
vale -8!
Il bitOffset può essere specificato anche attraverso un Imm8; in tal
caso, sono permessi solo offset relativi a operandi a 16 o 32 bit.
Più precisamente, un Imm8 deve specificare un bitOffset compreso tra
0 e 15 per operandi a 16 bit, e tra 0 e 31 per
operandi a 32 bit; a tale proposito, la CPU prende in considerazione
solo i primi 4 bit dell'Imm8 per operandi a 16 bit e i primi
5 bit dell'Imm8 per operandi a 32 bit!
23.4.1 L'istruzione BT
Con il mnemonico BT si indica l'istruzione Bit Test (test di un bit);
le uniche forme lecite per l'istruzione BT sono le seguenti:
L'istruzione BT richiede esplicitamente due operandi, SRC e
DEST; l'operando DEST contiene il bitBase, mentre l'operando
SRC contiene il bitOffset.
Quando la CPU incontra il codice macchina di BT, legge lo stato del bit
che si trova alla distanza bitOffset dal bitBase di DEST; il
valore (0 o 1) letto dalla CPU, viene memorizzato nel flag
CF.
Poniamo, ad esempio, CX=0111001011001101b e consideriamo l'istruzione:
bt cx, 7
Dopo l'esecuzione di questa istruzione, la CPU ci restituisce CF=1;
infatti, il bit in posizione 7 di CX è allo stato logico 1!
Consideriamo ora le seguenti definizioni:
Poniamo AX=-8, facciamo puntare ES:DI a varWord2 ed eseguiamo
l'istruzione:
bt [es:di], ax
Dopo l'esecuzione di questa istruzione, la CPU ci restituisce CF=0;
infatti, tenendo presente che in memoria varWord2 segue immediatamente
varWord1, si vede subito che partendo dal bitBase di varWord2
e tornando indietro di 8 posizioni, incontriamo un bit (di varWord1)
a livello logico 0!
23.4.2 L'istruzione BTC
Con il mnemonico BTC si indica l'istruzione Bit Test and Complement
(test e complemento a 1 di un bit); le uniche forme lecite per l'istruzione
BTC sono le seguenti:
L'istruzione BTC richiede esplicitamente due operandi, SRC e
DEST; l'operando DEST contiene il bitBase, mentre l'operando
SRC contiene il bitOffset.
Quando la CPU incontra il codice macchina di BTC, legge lo stato del
bit che si trova alla distanza bitOffset dal bitBase di DEST
e lo salva nel flag CF; lo stesso bit di DEST viene poi sottoposto
al complemento a 1 (inversione).
Poniamo, ad esempio, CX=0111001011001101b e consideriamo l'istruzione:
btc cx, 7
Dopo l'esecuzione di questa istruzione, la CPU ci restituisce CF=1
e CX=0111001001001101b; infatti, il bit in posizione 7 di CX è
inizialmente allo stato logico 1 e dopo il complemento a 1 diventa
0!
23.4.3 L'istruzione BTR
Con il mnemonico BTR si indica l'istruzione Bit Test and Reset (test
e azzeramento di un bit); le uniche forme lecite per l'istruzione BTR sono
le seguenti:
L'istruzione BTR richiede esplicitamente due operandi, SRC e
DEST; l'operando DEST contiene il bitBase, mentre l'operando
SRC contiene il bitOffset.
Quando la CPU incontra il codice macchina di BTR, legge lo stato del
bit che si trova alla distanza bitOffset dal bitBase di DEST
e lo salva nel flag CF; lo stesso bit di DEST viene poi portato a
livello logico 0.
Poniamo, ad esempio, AX=1111000011110000b, BX=12 e consideriamo
l'istruzione:
btr ax, bx
Dopo l'esecuzione di questa istruzione, la CPU ci restituisce CF=1
e AX=1110000011110000b; infatti, il bit in posizione 12 di AX
è inizialmente allo stato logico 1 e dopo l'azzeramento diventa 0!
23.4.4 L'istruzione BTS
Con il mnemonico BTS si indica l'istruzione Bit Test and Set (test
e attivazione di un bit); le uniche forme lecite per l'istruzione BTS sono
le seguenti:
L'istruzione BTS richiede esplicitamente due operandi, SRC e
DEST; l'operando DEST contiene il bitBase, mentre l'operando
SRC contiene il bitOffset.
Quando la CPU incontra il codice macchina di BTS, legge lo stato del
bit che si trova alla distanza bitOffset dal bitBase di DEST
e lo salva nel flag CF; lo stesso bit di DEST viene poi portato a
livello logico 1.
Poniamo, ad esempio, AX=1111000011110000b, BX=10 e consideriamo
l'istruzione:
bts ax, bx
Dopo l'esecuzione di questa istruzione, la CPU ci restituisce CF=0
e AX=1110010011110000b; infatti, il bit in posizione 10 di AX
è inizialmente allo stato logico 0 e dopo l'attivazione diventa 1!
23.4.5 Effetti provocati da BT, BTC, BTR e BTS,
sugli operandi e sui flags
L'esecuzione dell'istruzione BT non provoca alcuna conseguenza sugli
operandi SRC e DEST; l'esecuzione delle istruzioni BTC,
BTR e BTS, provoca la modifica del bit che si trova alla distanza
bitOffset dal bitBase di DEST.
L'esecuzione delle istruzioni BT, BTC, BTR e BTS, provoca
la modifica dei campi ZF, CF, OF, SF, AF e PF
del registro FLAGS; il solo flag CF ha significato, mentre i flags
ZF, OF, SF, AF e PF sono indefiniti.
Nel flag CF la CPU memorizza lo stato originale del bit che si trova
alla distanza bitOffset dal bitBase di DEST.
23.5 L'istruzione CMPXCHG
Con il mnemonico CMPXCHG si indica l'istruzione Compare and Exchange
(compara e scambia); le uniche forme lecite per CMPXCHG sono le seguenti:
L'istruzione CMPXCHG, che è disponibile solo per le CPU 80486 e superiori,
richiede tre operandi di cui due, SRC e DEST, devono essere specificati in
modo esplicito; la CPU utilizza implicitamente l'accumulatore AL/AX/EAX
in base all'ampiezza in bit di SRC e DEST.
Quando la CPU incontra il codice macchina di CMPXCHG, compara DEST
con l'accumulatore; a seconda dell'ampiezza in bit di DEST, la CPU decide
se utilizzare AL, AX o EAX.
- Se DEST == Accumulatore la CPU copia SRC in DEST
e pone ZF=1
- Se DEST != Accumulatore la CPU copia DEST nell'accumulatore
e pone ZF=0
L'istruzione CMPXCHG può essere utilizzata anche in combinazione con il
prefisso LOCK; tale prefisso viene descritto in fondo al capitolo.
Vediamo un esempio pratico:
In questo esempio, l'istruzione CMPXCHG confronta AX con
BX e, siccome AX è diverso da BX, la CPU pone
ZF=0 e carica BX in AX; alla fine si ottiene
AX=2DA1h, BX=2DA1h, CX=3FF0h e ZF=0.
Se AX fosse stato uguale a BX (2DA1h), la CPU avrebbe
posto ZF=1 e avrebbe caricato CX in BX; alla fine avremmo
ottenuto AX=2DA1h, BX=3FF0h, CX=3FF0h e ZF=1.
23.5.1 Effetti provocati da CMPXCHG sugli operandi e sui flags
L'esecuzione dell'istruzione CMPXCHG può provocare la modifica dell'operando
DEST o dell'accumulatore; se la comparazione fornisce ZF=1 viene
modificato DEST, mentre se la comparazione fornisce ZF=0 viene
modificato l'accumulatore.
Analogamente a quanto accade con CMP, anche l'esecuzione dell'istruzione
CMPXCHG provoca la modifica dei campi OF, SF, ZF,
AF, PF, CF del registro FLAGS; il significato assunto
dal contenuto di tali campi è identico a quello già descritto per CMP.
23.6 L'istruzione
CMPXCHG8B
Con il mnemonico CMPXCHG8B si indica l'istruzione Compare and Exchange
8 Bytes (compara e scambia operandi a 8 byte); l'unica forma lecita per
CMPXCHG8B è la seguente:
CMPXCHG8B Mem64
L'istruzione CMPXCHG8B, che è disponibile solo per le CPU 80586 e superiori,
richiede tre operandi di cui uno solo, di tipo Mem64, deve essere specificato in
modo esplicito; la CPU utilizza implicitamente EDX:EAX e ECX:EBX.
Quando la CPU incontra il codice macchina di CMPXCHG8B, compara l'operando
Mem64 con EDX:EAX.
- Se [Mem64] == EDX:EAX la CPU copia ECX:EBX in [Mem64]
e pone ZF=1
- Se [Mem64] != EDX:EAX la CPU copia [Mem64] in EDX:EAX
e pone ZF=0
Nel rispetto della convenzione little endian, i registri EDX e ECX
contengono sempre la DWORD alta di un valore a 64 bit; analogamente, i
registri EAX e EBX contengono sempre la DWORD bassa di un valore
a 64 bit.
L'istruzione CMPXCHG8B può essere utilizzata anche in combinazione con il
prefisso LOCK; tale prefisso viene descritto in fondo al capitolo.
Vediamo un esempio pratico. Prima di tutto definiamo la seguente variabile a
64 bit:
varQword dd 2883F890h, 3F2D4031h
A questo punto possiamo scrivere:
In questo esempio, l'istruzione CMPXCHG8B confronta EDX:EAX con
varQword e, siccome EDX:EAX è uguale a varQword, la
CPU pone ZF=1 e carica ECX:EBX in varQword; alla
fine si ottiene varQword=22FF44813DF1AAB0h e ZF=1.
Se EDX:EAX fosse stato diverso da varQword, la CPU avrebbe posto
ZF=0 e avrebbe caricato varQword in EDX:EAX; alla fine avremmo
ottenuto quindi EDX:EAX=3F2D40312883F890h e ZF=0.
23.6.1 Effetti provocati da CMPXCHG8B sugli operandi e sui flags
L'esecuzione dell'istruzione CMPXCHG8B può provocare la modifica dell'operando
Mem64 o di EDX:EAX; se la comparazione fornisce ZF=1 viene
modificato Mem64, mentre se la comparazione fornisce ZF=0 viene
modificato EDX:EAX.
Contrariamente a quanto accade con CMP, l'esecuzione dell'istruzione
CMPXCHG8B provoca la modifica del solo campo ZF del registro FLAGS;
i campi OF, SF, AF, PF, CF rimangono
invariati.
23.7 L'istruzione CPUID
Con il mnemonico CPUID si indica l'istruzione CPU Identification
(identificazione del modello di CPU installato sul computer); l'unica forma
lecita per questa istruzione è la seguente:
CPUID
In realtà CPUID si serve di un parametro implicito che deve essere passato
attraverso il registro EAX; le informazioni restituite da CPUID
dipendono proprio dal valore di tale parametro.
L'istruzione CPUID, che è disponibile solo per le CPU di classe
80586 o superiore (e per i modelli più recenti delle 80486), ha
un codice macchina formato dall'opcode 0Fh, A2h; quando la
CPU incontra tale codice macchina (purché, ovviamente, sia in grado di
supportarlo) restituisce nei registri generali a 32 bit una dettagliata
serie di informazioni relative alle caratteristiche del microprocessore installato
sul computer.
Per un utilizzo corretto di CPUID la prima cosa da fare consiste nel
determinare se sul computer è installata una CPU con architettura ad
almeno 32 bit; nella sezione Assembly Avanzato verrà presentato
un esempio pratico.
Supponendo di avere sicuramente a disposizione una CPU a 32 bit
o superiore, dobbiamo stabilire se l'istruzione CPUID è supportata; a
tale proposito, dobbiamo verificare se è possibile modificare in modo permanente
il flag ID che è rappresentato dal bit in posizione 21 del registro
EFLAGS.
Se CPUID è supportata, possiamo procedere con la determinazione delle
caratteristiche fondamentali della CPU; a tale proposito, dobbiamo
chiamare CPUID con EAX=0. In tal modo, l'informazione più importante
che otteniamo viene restituita in EAX e rappresenta il valore massimo del
parametro (EAX) che CPUID può accettare; generalmente, nei modelli
più diffusi di CPU di classe 80586 o superiore, tale valore è pari
a 1.
La Figura 23.4 illustra un esempio pratico che mostra come ricavare le informazioni
principali relative al modello di CPU installato sul proprio computer; per
maggiori dettagli si consiglia di consultare i manuali scaricabili dai siti
ufficiali dei vari produttori di CPU.
Nei modelli più recenti di CPU di classe 80686 o superiore, sono
stati introdotti ulteriori metodi che permettono a CPUID di restituire
informazioni sempre più dettagliate; si tenga presente che tali metodi possono
differire in base al produttore della CPU!
Con alcuni modelli di CPU, ad esempio, l'istruzione CPUID potrebbe
accettare anche il parametro EAX=80000000h; per sapere se ciò è possibile
dobbiamo chiamare CPUID con EAX=80000000h, verificando poi che
nello stesso registro EAX venga restituito un valore maggiore o uguale a
80000001h.
In caso affermativo, possiamo chiamare CPUID con EAX=80000001h,
ottenendo in tal modo una enorme quantità di informazioni aggiuntive; come
al solito, tali informazioni vengono restituite nei registri generali a
32 bit.
Analizzando il listato di Figura 23.4, si nota la necessaria presenza della
direttiva CPU 586; come al solito, se il nostro assembler non supporta tale
direttiva, possiamo sempre servirci del codice macchina di CPUID. Si può
scrivere, ad esempio:
23.7.1 Effetti provocati da CPUID sugli operandi e sui flags
L'esecuzione dell'istruzione CPUID con EAX=0, EAX=1 e
EAX=2, provoca la modifica dei registri generali EAX, EBX,
ECX e EDX; ulteriori valori supportati da alcune CPU in
EAX possono provocare la modifica anche dei registri ESI e
EDI!
L'esecuzione dell'istruzione CPUID non modifica alcun campo del registro
FLAGS; per verificare il supporto dell'istruzione CPUID, il
programmatore deve testare la modificabilità del flag ID che si trova in
posizione 21 nel registro EFLAGS.
23.8 L'istruzione NOP
Con il mnemonico NOP si indica l'istruzione No Operation (nessuna
operazione da eseguire); l'unica forma lecita per questa istruzione è la seguente:
NOP
Il codice macchina di NOP è formato dal solo opcode 90h; quando la
CPU incontra tale codice macchina, si limita semplicemente ad incrementare
di 1 il contenuto di IP in modo che la coppia CS:IP punti
all'istruzione successiva a NOP.
Il compito dell'istruzione NOP consiste proprio nel non fare assolutamente
niente; nonostante le apparenze, però, l'istruzione NOP torna utile in molte
situazioni.
Osservando, ad esempio, la tabella delle istruzioni della CPU, si nota che
una 80486 DX a 33 MHz esegue l'istruzione NOP in un solo ciclo
di clock, pari a:
1 / 33000000 = 0.00000003 secondi
Possiamo utilizzare allora un adeguato numero di istruzioni NOP per generare
dei ritardi di milionesimi di secondo che spesso si rendono necessari quando si
programmano determinate periferiche hardware.
L'istruzione NOP viene anche largamente utilizzata dai debuggers; come
sappiamo, il debugger permette al programmatore di effettuare la ricerca di errori
eventualmente presenti nei propri programmi.
Per svolgere tale lavoro il debugger offre la possibilità di inserire dei punti di
interruzione (breakpoints) all'interno del codice del programma da analizzare;
in un precedente capitolo abbiamo visto che un breakpoint è rappresentato da un byte
il cui valore (11001100b) non è altro che il codice macchina della INT
03h, chiamata proprio trap to debugger o breakpoint.
Quando la CPU incontra tale codice macchina, chiama la ISR associata
alla INT 03h; il debugger intercetta questa chiamata ed è così in grado di
analizzare lo stato assunto in quel preciso istante dai vari registri della CPU.
Se il programmatore vuole eliminare un breakpoint deve comunicare questa richiesta al
debugger che, a sua volta, provvede a sostituire facilmente il codice macchina (da un
byte) della INT 03h con il codice macchina (da un byte) di NOP; in questo
modo, quando la CPU incontra l'istruzione NOP passa direttamente
all'istruzione successiva.
Anche gli assembler in molte circostanze ricorrono all'istruzione NOP; un esempio
pratico è dato dagli effetti prodotti dalla direttiva ALIGN che, come sappiamo,
viene utilizzata per allineare il codice e i dati di un programma.
All'interno di un blocco dati, per ottenere l'allineamento richiesto l'assembler
inserisce un adeguato numero di byte di valore 00h; ciò non è possibile in
un blocco codice in quanto la CPU scambierebbe il valore 00h per una
porzione di un codice macchina!
La soluzione a questo problema consiste nel servirsi di uno o più codici macchina
relativi ad istruzioni che non producono alcun effetto sul funzionamento di un
programma; una situazione del genere si presenta proprio con il codice macchina
90h dell'istruzione NOP.
Il mnemonico NOP è un alias per l'istruzione:
xchg ax, ax
Infatti, tale istruzione ha lo stesso codice macchina 90h di NOP!
23.8.1 Effetti provocati da NOP sugli operandi e sui flags
L'istruzione NOP non ha operandi.
L'esecuzione dell'istruzione NOP non modifica alcun campo del registro
FLAGS.
23.9 L'istruzione SETcond
Con il mnemonico SETcond si indicano le istruzioni Set Byte on Condition
(attivazione di un operando da 1 byte se la condizione cond è verificata);
per questo numeroso gruppo di istruzioni, disponibili solo per le CPU di classe
80386 e superiori, le uniche forme lecite sono le seguenti:
Le istruzioni SETcond richiedono quindi un unico operando che può essere di tipo
Reg8 o Mem8; tale operando ricopre il ruolo di DEST.
Il codice macchina di SETcond è formato dall'opcode 0Fh, da un secondo
opcode che individua la condizione cond da verificare e dal campo
mod_reg_r/m; la componente reg di mod_reg_r/m vale sempre
000b.
Quando la CPU incontra tale codice macchina, controlla se la condizione
cond è verificata; tale controllo si basa, come al solito, sullo stato
assunto dai vari flags in seguito ad una operazione logico aritmetica appena
eseguita.
Se cond è verificata, la CPU assegna il valore 1 all'operando
DEST; se cond non è verificata, la CPU assegna il valore
0 all'operando DEST.
Possiamo dire quindi che le istruzioni SETcond sono molto simili formalmente
alle istruzioni Jcond; la differenza sostanziale sta nel fatto che in base al
risultato prodotto dalla valutazione di cond, l'istruzione Jcond decide
se effettuare o meno un trasferimento del controllo, mentre l'istruzione SETcond
decide se assegnare il valore 0 o 1 all'operando DEST.
In base alle analogie appena descritte, anche le istruzioni SETcond possono
essere suddivise in due grandi categorie in quanto la condizione cond può fare
riferimento, esplicitamente o implicitamente, al valore assunto da determinati flags;
analizziamo in dettaglio queste due categorie.
23.9.1 Istruzioni SETcond riferite esplicitamente ai flags
In questa particolare categoria di istruzioni SETcond, la condizione
cond è legata esplicitamente al valore (0 o 1) assunto da un
determinato flag come conseguenza del risultato prodotto da una operazione logico
aritmetica appena eseguita; la Figura 23.5 illustra l'insieme completo di queste
istruzioni.
23.9.2 Istruzioni SETcond riferite implicitamente ai flags
L'altra categoria di istruzioni SETcond fa esplicito riferimento, invece,
alla relazione d'ordine che esiste tra due operandi, DEST e SRC;
la condizione cond rappresenta quindi una comparazione tra due operandi.
Tornando alle istruzioni Jcond, abbiamo anche visto che nella comparazione
tra due operandi è importantissimo distinguere tra numeri interi con o senza segno;
la Figura 23.6 illustra le istruzioni SETcond riferite, esplicitamente, ai
numeri interi senza segno (i due operandi da comparare sono indicati con op1
e op2).
La Figura 23.7 illustra le istruzioni SETcond riferite, esplicitamente, ai
numeri interi con segno; i due operandi da comparare sono indicati con op1
e op2.
In relazione al doppio nome utilizzato da alcuni mnemonici delle istruzioni
SETcond, valgono tutte le considerazioni già esposte per le istruzioni
Jcond; ad esempio, la condizione:
set DEST if not parity (SETNP)
può essere espressa anche come:
set DEST if parity is odd (SETPO)
L'utilizzo delle istruzioni SETcond è estremamente semplice; possiamo scrivere,
ad esempio:
Se AX=0, l'istruzione TEST produce un risultato nullo (ZF=1) e
si ottiene quindi BL=1; se, invece, AX è diverso da zero, l'istruzione
TEST produce un risultato non nullo (ZF=0) e si ottiene quindi
BL=0.
23.9.3 Effetti provocati da SETcond sugli operandi e sui flags
L'esecuzione delle istruzioni SETcond provoca la modifica dell'operando
DEST; se cond è verificata viene assegnato il valore 1 a
DEST, mentre se cond non è verificata viene assegnato il
valore 0 a DEST.
L'esecuzione delle istruzioni SETcond non modifica alcun campo del registro
FLAGS.
23.10 L'istruzione XADD
Con il mnemonico XADD si indica l'istruzione Exchange and Add (scambia
e somma due operandi); questa istruzione, che è disponibile solo per le CPU di
classe 80486 o superiore, può essere utilizzata nelle seguenti forme:
Il codice macchina di XADD è formato dall'opcode 00001111b,
1100000w e dal campo mod_reg_r/m; quando la CPU incontra
tale codice macchina, esegue le seguenti operazioni:
In sostanza, XADD calcola la somma (in un registro temporaneo Temp)
tra SRC e DEST, poi copia DEST in SRC e Temp
in DEST.
L'operando SRC può essere solo di tipo Reg; l'istruzione XADD
può essere utilizzata anche in combinazione con il prefisso LOCK.
Vediamo un esempio pratico:
In questo esempio, XADD calcola:
Temp16 = BX + AX = 8000 + 3500 = 11500
Il contenuto (3500) di AX viene copiato in BX, mentre
Temp16=11500 viene copiato in AX; alla fine si ottiene
AX=11500 e BX=3500.
23.10.1 Effetti provocati da XADD sugli operandi e sui flags
L'esecuzione dell'istruzione XADD provoca la modifica di entrambi gli
operandi SRC e DEST.
Come accade per ADD, anche l'esecuzione dell'istruzione XADD modifica
i campi OF, SF, ZF, AF, PF, CF del
Flags Register; il significato di tali campi è lo stesso già descritto per
ADD.
23.11 Le istruzioni
XLAT,
XLATB
Con il mnemonico XLAT si indica l'istruzione Table Look-up Translation
(conversione attraverso una tabella di look-up); in modalità reale, le uniche
forme lecite per questa istruzione sono le seguenti:
Il codice macchina di XLAT è formato dal solo opcode D7h; quando la
CPU incontra tale codice macchina, esegue la seguente operazione:
AL = [DS:BX+AL]
che equivale alla pseudo istruzione (AL non può essere usato come registro
indice):
mov al, [ds:bx+al]
In sostanza, il contenuto di AL viene trattato come numero intero senza
segno, compreso quindi tra 0 e 255; tale numero rappresenta un indice
(spiazzamento) all'interno di un vettore di BYTE che si trova in memoria a
partire dall'indirizzo logico DS:BX (base address).
Il BYTE che si trova all'indirizzo logico DS:(BX+AL) viene copiato
nello stesso registro AL; possiamo dire quindi che l'istruzione XLAT
utilizza AL come operando implicito DEST e DS:(BX+AL)
come operando implicito SRC.
Nella forma implicita, il mnemonico di questa istruzione viene rappresentato
da XLATB; con tale mnemonico, entrambi gli operandi sono impliciti e la
CPU utilizza obbligatoriamente AL come DEST e la coppia
DS:(BX+AL) come SRC.
La forma esplicita è rappresentata da XLAT che permette di specificare
un SegReg diverso da DS (segment override); possiamo scrivere, ad
esempio:
xlat [es:si+8]
Si tenga presente però che, così come accade per i mnemonici del tipo STOS,
LODS, etc, anche il mnemonico XLAT viene reso disponibile solo per
permettere al programmatore di specificare un segment override; in ogni caso,
la componente base+index è sempre rappresentata da BX+AL e ciò
significa che nell'esempio appena presentato, il simbolo SI+8 viene
ignorato dall'assembler!
Con NASM possiamo specificare il segment override attraverso la sintassi
già illustrata nel precedente capitolo; si può scrivere, ad esempio:
fs xlatb
Il nome del mnemonico XLAT deriva dal fatto che con il termine look-up
table si indica una tabella contenente, in genere, una matrice di m * n
elementi; come sappiamo, per accedere ad uno qualunque di tali elementi, dobbiamo
specificare le corrispondenti coordinate (riga, colonna).
Una matrice formata da una sola riga, assume la struttura di un vettore
monodimensionale; l'istruzione XLAT presuppone che la look-up table
sia un vettore monodimensionale formato da un numero di elementi di tipo BYTE
compreso tra 0 e 255.
Come si può facilmente intuire, le caratteristiche dell'istruzione XLAT si
prestano molto bene per gestire una look-up table contenente i 256
simboli del codice ASCII;
infatti, l'istruzione XLAT è stata creata dalla Intel proprio per
effettuare conversioni tra codice ASCII e codici standard utilizzati su
altre piattaforme hardware.
In particolare, l'istruzione XLAT viene impiegata in modo massiccio per
effettuare conversioni tra codice ASCII e codice EBCDIC (Extended
Binary Coded Decimal Interchange Code); il codice EBCDIC viene utilizzato
dai mainframe della IBM.
Per permettere lo scambio di informazioni (ad esempio, un listato Assembly)
tra un computer che utilizza il codice
ASCII e un computer che
utilizza il codice EBCDIC, dobbiamo scrivere quindi un apposito programma di
conversione da ASCII a EBCDIC e viceversa; a titolo di curiosità vediamo
in Figura 23.8 i codici EBCDIC delle lettere minuscole dell'alfabeto.
Come si può notare, a differenza di quanto accade con il codice
ASCII, i codici EBCDIC
delle lettere minuscole sono consecutivi ma non contigui; questo significa che non è
possibile applicare al codice EBCDIC gli algoritmi che vengono utilizzati
usualmente per le stringhe in formato
ASCII!
Per illustrare il funzionamento dell'istruzione XLAT vediamo proprio un esempio
relativo alla conversione da ASCII a EBCDIC; in questo esempio creiamo una
look-up table che riceve in input un codice ASCII di una lettera minuscola
e restituisce in output il corrispondente codice EBCDIC.
Nel blocco dati del programma definiamo la seguente tabella:
Supponendo che questo blocco dati venga referenziato da DS, la tabella
tabEBCDIC dovrà essere puntata da DS:BX; la conversione da ASCII
a EBCDIC viene naturalmente effettuata da XLAT che si aspetta di trovare
in AL l'indice di tabEBCDIC a cui accedere.
Osserviamo ora che gli indici di tabEBCDIC partono da zero, mentre i codici
ASCII delle 26 lettere
minuscole sono compresi tra 97 e 122 (e sono consecutivi e contigui);
ciò significa che se vogliamo conoscere, ad esempio, il codice EBCDIC della
lettera 'f' dobbiamo caricare in AL il codice
ASCII 'f'
diminuito di 97. Notiamo, infatti, che:
ASCII('f') - 97 = 102 - 97 = 5
L'elemento in posizione 5 all'interno di tabEBCDIC vale 134 che è
proprio il codice EBCDIC della lettera 'f'!
In base alle considerazioni appena svolte, possiamo scrivere istruzioni del tipo:
L'istruzione XLAT accede all'indice 5 della tabella tabEBCDIC,
legge il valore 134 e lo carica in AL; in modo analogo, caricando in
AL il valore:
ASCII('z') - 97 = 122 - 97 = 25
ed eseguendo XLAT otteniamo in AL il valore 169 che occupa,
infatti, la posizione 25 in tabEBCDIC e rappresenta proprio il codice
EBCDIC della lettera 'z'.
Se vogliamo effettuare conversioni da EBCDIC a ASCII dobbiamo seguire
lo stesso procedimento; naturalmente, in questo caso bisogna ricordarsi di tenere
conto degli indici che in EBCDIC non vengono utilizzati come, ad esempio, tutti
gli indici compresi tra 138 e 144!
L'esempio mostrato in precedenza è piuttosto semplice e non evidenzia le potenzialità
di XLAT; nei programmi reali in genere si utilizza un loop all'interno del quale
si sviluppa un flusso di byte che vengono letti in sequenza da un buffer, convertiti con
XLAT e inviati via modem a un altro computer.
23.11.1 Effetti provocati da XLAT/XLATB sugli operandi e sui flags
L'esecuzione dell'istruzione XLAT/XLATB provoca la modifica dell'operando
implicito AL.
L'esecuzione dell'istruzione XLAT/XLATB non altera alcun campo del Flags
Register.
23.12 Il prefisso LOCK
Con il mnemonico LOCK si indica il prefisso Assert LOCK# Signal Prefix;
tale prefisso può precedere esclusivamente le istruzioni: ADD, ADC,
AND, BTC, BTR, BTS, CMPXCHG, DEC, INC,
NEG, NOT, OR, SBB, SUB, XOR, XADD e
XCHG.
Prima di eseguire una istruzione preceduta dal prefisso LOCK, la CPU
invia un segnale di attivazione al corrispondente piedino LOCK# (vedi Figura
9.3 e Figura 9.6 del capitolo 9); tale segnale rimane attivo per tutto il tempo
necessario alla CPU per l'esecuzione dell'istruzione stessa.
In un sistema multiprocessore la situazione appena descritta permette alla sola
CPU che ha ricevuto il segnale di LOCK, di avere il controllo
esclusivo sulla memoria condivisa (tra i vari microprocessori) a cui accede
l'istruzione in esecuzione.