Assembly Base con MASM

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: 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, offset 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:
MAX_INDEX = 15
Questa semplice modifica si ripercuote automaticamente su tutto il codice di BOUND.ASM; in particolare, notiamo la definizione di vectWord:
vectWord dw MAX_INDEX dup (0)
e il calcolo dell'offset relativo all'ultimo elemento di vectWord:
mov ax, offset 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:
.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. 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. 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 dq 3F2D40312883F890h
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 .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!

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.