Assembly Base con MASM

Capitolo 17: Istruzioni aritmetiche


In questo capitolo vengono illustrate le principali istruzioni aritmetiche messe a disposizione dalla famiglia delle CPU 80x86; queste istruzioni permettono di effettuare svariate operazioni come, addizioni, sottrazioni, moltiplicazioni, divisioni, comparazioni numeriche, conversioni numeriche, cambiamenti di segno, etc.
Come è stato spiegato nel capitolo sulla matematica del computer, una CPU con architettura a n bit gestisce via hardware qualsiasi operazione che coinvolga numeri interi aventi una ampiezza massima di n bit; tutte le operazioni su numeri interi aventi una ampiezza in bit superiore a n, devono essere gestite via software dal programmatore. Nel seguito del capitolo vengono mostrati semplici esempi pratici che illustrano il procedimento da seguire per operare su numeri di grosse dimensioni; questi esempi verranno poi ampliati e ottimizzati nei capitoli successivi.
Tutte le istruzioni aritmetiche lavorano su operandi di tipo Reg, Mem e Imm; per ovvi motivi, è proibito l'utilizzo di operandi di tipo SegReg.
Le istruzioni aritmetiche relative ai numeri in formato BCD o Binary Coded Decimal (decimale codificato in binario), verranno illustrate nel prossimo capitolo.

17.1 L'istruzione ADD

Con il mnemonico ADD si indica l'istruzione Addition (addizione tra SRC e DEST); incontrando questa istruzione, la CPU somma il contenuto di SRC con il contenuto di DEST e mette il risultato nell'operando DEST.
Entrambi gli operandi devono essere specificati esplicitamente dal programmatore e devono avere la stessa ampiezza in bit; le uniche forme lecite per l'istruzione ADD, sono le seguenti: Per poter analizzare degli esempi pratici, consideriamo le seguenti definizioni presenti in un blocco dati chiamato DATASEGM: Ovviamente, agli operandi di tipo Mem si applicano tutti i concetti esposti nel precedente capitolo in relazione agli indirizzamenti; salvo casi particolari, tali concetti non verranno più ripetuti.

Supponiamo di avere BX=14980, DX=20800 e consideriamo la seguente istruzione:
add bx, dx
Quando la CPU incontra questa istruzione, calcola:
14980 + 20800 = 35780
Il risultato 35780, viene poi sistemato nei 16 bit del registro BX.

Supponiamo di avere CH=38 e consideriamo l'istruzione:
add ch, VarByte
Quando la CPU incontra questa istruzione, calcola:
38 + 210 = 248
Il risultato 248, viene poi sistemato negli 8 bit del registro CH.

Se SI=20000 e DS:(BX+DI+0002h) punta a VarWord, possiamo scrivere l'istruzione:
add [bx+di+0002h], si
La presenza del registro SI indica all'assembler che gli operandi sono a 16 bit; quando la CPU incontra questa istruzione, calcola:
12890 + 20000 = 32890
Il risultato 32890, viene poi sistemato nella locazione di memoria (VarWord) che si trova all'indirizzo DS:(BX+DI+0002h).

In presenza della dichiarazione:
PRESSIONE = 450000
e con ES:DI che punta a VarDword, possiamo scrivere la seguente istruzione:
add dword ptr es:[di], PRESSIONE
L'operatore DWORD PTR è necessario per indicare all'assembler che gli operandi sono a 32 bit; il segment override è necessario perché ES non è il registro di segmento naturale per i dati.
Quando la CPU incontra questa istruzione, calcola:
3896580 + 450000 = 4346580
Il risultato 4346580, viene poi sistemato nella locazione di memoria (VarDword) che si trova all'indirizzo ES:DI.

17.1.1 Istruzione ADD con operando sorgente di tipo Imm

Nel campo Opcode del codice macchina di una qualsiasi istruzione aritmetica che coinvolge un eventuale operando sorgente di tipo Imm, è sempre presente il bit s o sign bit; come sappiamo, questo bit indica alla CPU se è necessario procedere all'estensione del bit di segno dello stesso Imm. In sostanza, se s=0, l'Imm non viene modificato dalla CPU; se, invece, s=1, l'Imm viene esteso attraverso il suo bit di segno, sino a raggiungere l'ampiezza in bit dell'operando DEST.
Se necessario, l'estensione dell'ampiezza dell'operando di tipo Imm viene effettuata direttamente dall'assembler, che pone quindi s=0; in alternativa, l'assembler può decidere di delegare questo compito alla CPU e in tal caso, l'assembler stesso pone s=1. Considerando il fatto che questo aspetto vale per tutte le istruzioni aritmetiche, analizziamo alcuni esempi dettagliati.
È importante ricordare che il programmatore ha il compito di assegnare ad un Imm un valore compatibile con l'ampiezza in bit degli operandi delle istruzioni che coinvolgono lo stesso Imm; in caso contrario, si ottengono risultati privi di senso.

Dai manuali della CPU, si ricava che il codice macchina generico per una somma tra sorgente Imm e destinazione Reg/Mem, è formato dall'Opcode 100000sw, seguito dal campo mod_000_r/m e da un Imm.

Supponiamo, ad esempio, di dichiarare un Imm che vogliamo trattare come intero senza segno a 8 bit; in tal caso, allo stesso Imm dobbiamo assegnare un valore compreso tra 0 e 255.
Come esempio pratico, consideriamo la seguente dichiarazione:
SUPERFICIE = 32
In assenza del segno esplicito, l'assembler tratta SUPERFICIE come un intero positivo (+32) che richiede almeno 8 bit; la rappresentazione binaria a 8 bit di +32, è 00100000b (20h).
Nell'ipotesi che EBX=0171D86Eh (24238190), consideriamo ora la seguente istruzione:
add ebx, SUPERFICIE
Questa istruzione deve produrre il risultato:
24238190 + 32 = 24238222
Gli operandi devono essere a 32 bit (w=1), per cui si rende necessaria l'estensione dell'ampiezza in bit di SUPERFICIE; l'assembler osserva che il bit di segno di SUPERFICIE è 0, compatibile quindi con il segno positivo. L'estensione dell'ampiezza di SUPERFICIE può essere delegata allora alla CPU; l'assembler pone s=1, per cui:
Opcode = 10000011b = 83h
Il registro destinazione è EBX, per cui r/m=011b; per indicare che r/m codifica un registro, si pone, come sappiamo, mod=11b. Si ottiene quindi:
mod_000_r/m = 11000011b = C3h
L'operando sorgente è:
Imm = 00100000b = 20h
Gli operandi sono a 32 bit, per cui si rende necessario il prefisso 66h; l'assembler genera quindi il codice macchina:
01100110b 10000011b 11000011b 00100000b = 66h 83h C3h 20h
Quando la CPU incontra questa istruzione, estende a 32 bit il valore 00100000b (attraverso il suo bit di segno 0) e ottiene:
00000000000000000000000000100000b = 00000020h = +32
Successivamente, la CPU esegue la somma:
0171D86Eh + 00000020h = 0171D88Eh = 24238222
La CPU ha quindi eseguito correttamente la somma:
24238190 + 32 = 24238222
Il risultato 24238222 viene sistemato nel registro EBX; se vogliamo verificare in pratica l'esempio appena esposto, possiamo scrivere le seguenti istruzioni, che si servono della procedura writeUdec32 per la visualizzazione di un intero senza segno a 32 bit: Supponiamo, ad esempio, di dichiarare un Imm che vogliamo trattare come intero con segno a 8 bit; in tal caso, allo stesso Imm dobbiamo assegnare un valore compreso tra -128 e +127.
Come esempio pratico, consideriamo la seguente dichiarazione:
DISLIVELLO = -49
La rappresentazione binaria a 8 bit in complemento a 2 del numero negativo -49, è:
256 - 49 = 207 = 11001111b = CFh
Nell'ipotesi che EBX=0171D86Eh (24238190), consideriamo ora la seguente istruzione:
add ebx, DISLIVELLO
Questa istruzione deve produrre il risultato:
24238190 + (-49) = 24238141
Gli operandi devono essere a 32 bit (w=1), per cui si rende necessaria l'estensione dell'ampiezza in bit di DISLIVELLO; l'assembler osserva che il bit di segno di DISLIVELLO è 1, compatibile quindi con il segno negativo. L'estensione dell'ampiezza di DISLIVELLO può essere delegata allora alla CPU; l'assembler pone s=1, per cui:
Opcode = 10000011b = 83h
Il registro destinazione è EBX, per cui r/m=011b; per indicare che r/m codifica un registro, si pone mod=11b. Si ottiene quindi:
mod_000_r/m = 11000011b = C3h
L'operando sorgente è:
Imm = 11001111b = CFh
Gli operandi sono a 32 bit, per cui si rende necessario il prefisso 66h; l'assembler genera quindi il codice macchina:
01100110b 10000011b 11000011b 11001111b = 66h 83h C3h CFh
Quando la CPU incontra questa istruzione, estende a 32 bit il valore 11001111b (attraverso il suo bit di segno 1) e ottiene:
11111111111111111111111111001111b = FFFFFFCFh = -49
Successivamente, la CPU esegue la somma:
0171D86Eh + FFFFFFCFh = 0171D83Dh = 24238141
La CPU ha quindi eseguito correttamente la somma:
24238190 + (-49) = 24238141
Il risultato 24238141 viene sistemato nel registro EBX; se vogliamo verificare in pratica l'esempio appena esposto, possiamo scrivere le seguenti istruzioni, che si servono della procedura writeSdec32 per la visualizzazione di un intero con segno a 32 bit:

17.1.2 Effetti provocati da ADD sugli operandi e sui flags

L'esecuzione dell'istruzione ADD, modifica il contenuto del solo operando DEST, che viene sovrascritto dal risultato della somma appena effettuata; il contenuto dell'operando SRC rimane inalterato.
Anche per ADD, bisogna prestare attenzione ai soliti casi del tipo:
add bx, [bx+di+002Ah]
Dopo l'esecuzione di questa istruzione, il contenuto del registro puntatore BX viene modificato.

La possibilità di effettuare una somma tra Reg e Reg ci permette di scrivere istruzioni del tipo:
add ax, ax
Come si può facilmente constatare, l'esecuzione di questa istruzione fa raddoppiare il contenuto originale di AX; se, ad esempio, AX=2500, dopo l'esecuzione di ADD si ottiene:
AX = 2500 + 2500 = 5000
L'esecuzione dell'istruzione ADD e di qualsiasi altra istruzione aritmetica, modifica i campi OF, SF, ZF, AF, PF e CF del Flags Register; come si può facilmente intuire, le modifiche apportate a questi flags assumono una importanza enorme, in quanto forniscono, sia alla CPU, sia al programmatore, informazioni dettagliate sul risultato scaturito dalla operazione appena eseguita.
Questi aspetti sono già stati esposti nel capitolo dedicato alla matematica del computer; vediamo alcuni esempi, che si riferiscono ad operandi a 16 bit e che possono essere verificati attraverso le seguenti istruzioni (valore1 e valore2 indicano due numeri interi espliciti): Come sappiamo, grazie all'aritmetica modulare, la CPU utilizza la sola istruzione ADD per operare, indifferentemente, su numeri interi con o senza segno; attraverso la consultazione degli opportuni flags, il programmatore ha la possibilità di conoscere il risultato di una somma, sia nell'insieme degli interi con segno, sia nell'insieme degli interi senza segno.

Poniamo AX=12850, BX=4700 ed eseguiamo l'istruzione:
add ax, bx
In binario si ha:
AX = 12850 = 0011001000110010b
e:
BX = 4700 = 0001001001011100b
La CPU esegue quindi la somma: Sulla base di questo risultato, la CPU pone:
CF = 0, PF = 1, AF = 0, ZF = 0, SF = 0, OF = 0
Per giustificare questa situazione, osserviamo innanzi tutto che il risultato è diverso da 0, per cui ZF=0; l'esecuzione della somma non ha prodotto alcun riporto dal primo nibble al secondo nibble, per cui AF=0. Negli 8 bit meno significativi del risultato, è presente un numero pari (4) di 1; di conseguenza, PF=1.
Nell'insieme degli interi senza segno, 0011001000110010b rappresenta 12850, mentre 0001001001011100b rappresenta 4700; la somma di questi due numeri, produce un risultato (17550) non superiore a 65535, per cui CF=0 (nessun riporto).
Nell'insieme degli interi con segno, 0011001000110010b rappresenta +12850, mentre 0001001001011100b rappresenta +4700; la somma di questi due numeri, produce un risultato (+17550) non superiore a +32767, per cui OF=0. Il bit più significativo del risultato è 0, per cui SF=0 (numero positivo).

Poniamo AX=12850, BX=-4700, ed eseguiamo l'istruzione:
add ax, bx
In binario si ha:
AX = 12850 = 0011001000110010b
e:
BX = 216 - 4700 = 60836 = 1110110110100100b
La CPU esegue quindi la somma: Sulla base di questo risultato, la CPU pone:
CF = 1, PF = 0, AF = 0, ZF = 0, SF = 0, OF = 0
Per giustificare questa situazione, osserviamo innanzi tutto che il risultato è diverso da 0, per cui ZF=0; l'esecuzione della somma non ha prodotto alcun riporto dal primo nibble al secondo nibble, per cui AF=0. Negli 8 bit meno significativi del risultato, è presente un numero dispari (5) di 1; di conseguenza, PF=0.
Nell'insieme degli interi senza segno, 0011001000110010b rappresenta 12850, mentre 1110110110100100b rappresenta 60836; la somma di questi due numeri, produce un risultato (73686) superiore a 65535, per cui CF=1 (riporto). Osserviamo che in binario, 73686 si scrive 10001111111010110b, cioè 0001111111010110b (8150) con riporto di 1.
Nell'insieme degli interi con segno, 0011001000110010b rappresenta +12850, mentre 1110110110100100b rappresenta -4700; la somma di questi due numeri, produce un risultato (+8150) non superiore a +32767, per cui OF=0. Il bit più significativo del risultato è 0, per cui SF=0 (numero positivo).

Poniamo AX=-12812, BX=-18004, ed eseguiamo l'istruzione:
add ax, bx
In binario si ha:
AX = 216 - 12812 = 52724 = 1100110111110100b
e:
BX = 216 - 18004 = 47532 = 1011100110101100b
La CPU esegue quindi la somma: Sulla base di questo risultato, la CPU pone:
CF = 1, PF = 1, AF = 1, ZF = 0, SF = 1, OF = 0
Per giustificare questa situazione, osserviamo innanzi tutto che il risultato è diverso da 0, per cui ZF=0; l'esecuzione della somma ha prodotto un riporto dal primo nibble al secondo nibble, per cui AF=1. Negli 8 bit meno significativi del risultato è presente un numero pari (2) di 1; di conseguenza, PF=1.
Nell'insieme degli interi senza segno, 1100110111110100b rappresenta 52724, mentre 1011100110101100b rappresenta 47532; la somma di questi due numeri produce un risultato (100256) superiore a 65535, per cui CF=1 (riporto). Osserviamo che in binario, 100256 si scrive 11000011110100000b, cioè 1000011110100000b (34720) con riporto di 1.
Nell'insieme degli interi con segno, 1100110111110100b rappresenta -12812, mentre 1011100110101100b rappresenta -18004; la somma di questi due numeri, produce un risultato (-30816) non inferiore a -32768, per cui OF=0. Il bit più significativo del risultato è 1, per cui SF=1 (numero negativo); osserviamo che per i numeri con segno a 16 bit in complemento a 2, -30816 si scrive:
216 - 30816 = 34720 = 1000011110100000b
Poniamo AX=28500, BX=21370, ed eseguiamo l'istruzione:
add ax, bx
In binario si ha:
AX = 28500 = 0110111101010100b
e:
BX = 21370 = 0101001101111010b
La CPU esegue quindi la somma: Sulla base di questo risultato, la CPU pone:
CF = 0, PF = 0, AF = 0, ZF = 0, SF = 1, OF = 1
Per giustificare questa situazione, osserviamo innanzi tutto che il risultato è diverso da 0, per cui ZF=0; l'esecuzione della somma non ha prodotto alcun riporto dal primo nibble al secondo nibble, per cui AF=0. Negli 8 bit meno significativi del risultato è presente un numero dispari (5) di 1; di conseguenza, PF=0.
Nell'insieme degli interi senza segno, 0110111101010100b rappresenta 28500, mentre 0101001101111010b rappresenta 21370; la somma di questi due numeri, produce un risultato (49870) non superiore a 65535, per cui CF=0 (nessun riporto).
Nell'insieme degli interi con segno, 0110111101010100b rappresenta +28500, mentre 0101001101111010b rappresenta +21370; la somma di questi due numeri produce un risultato (+49870) superiore a +32767, per cui OF=1. Il bit più significativo del risultato è 1, per cui SF=1 (numero negativo); infatti, per i numeri con segno a 16 bit in complemento a 2, 49870 rappresenta:
49870 - 216 = -15666
Sommando quindi due numeri positivi, abbiamo ottenuto un numero negativo; come sappiamo, la CPU si basa proprio su questo evento per individuare un overflow.

17.2 L'istruzione ADC

Con il mnemonico ADC si indica l'istruzione Add with Carry (somma tra SRC, DEST e CF); incontrando questa istruzione, la CPU calcola:
DEST = DEST + SRC + CF
Gli operandi SRC e DEST devono essere specificati esplicitamente dal programmatore e devono avere la stessa ampiezza in bit; il risultato della somma viene disposto in DEST.
In analogia con ADD, le uniche forme lecite per l'istruzione ADC sono le seguenti: Come si può facilmente immaginare, l'istruzione ADC è stata concepita, principalmente, per permettere di eseguire in modo semplice, somme tra operandi di ampiezza arbitraria; a tale proposito, si devono applicare tutti i concetti esposti nel capitolo sulla matematica del computer. In particolare, è necessario ricordare che la somma:
DEST + SRC + CF
non può mai provocare due riporti consecutivi.

Tutte le considerazioni esposte per l'istruzione ADD in relazione agli effetti provocati sugli operandi e sui flags, restano valide anche per l'istruzione ADC.

Come accade per ADD, anche ADC, grazie all'aritmetica modulare, opera, indifferentemente, sui numeri interi con o senza segno; analizziamo il procedimento che bisogna seguire nei due casi che si possono presentare.

17.2.1 Somma tra numeri interi senza segno di ampiezza arbitraria

Supponiamo di voler eseguire la seguente somma tra interi senza segno a 96 bit: Procediamo innanzi tutto con la definizione delle due variabili; utilizzando, ad esempio, la direttiva DD, possiamo scrivere: A questo punto possiamo procedere con l'esecuzione della somma; la somma tra le due DWORD meno significative viene effettuata con ADD, mentre le due somme successive vengono effettuate con ADC in quanto bisogna tenere conto dell'eventuale riporto proveniente dalle somme precedenti.
Il codice che esegue la somma assume il seguente aspetto: Dopo l'esecuzione dell'ultima somma (tra le due DWORD più significative), consultiamo CF per sapere se c'è stato un riporto finale; nel nostro caso, CF=0, per cui il risultato è valido così com'è.
Se vogliamo visualizzare il risultato con writeHex32, possiamo procedere nel solito modo; possiamo scrivere, ad esempio: Come è stato già spiegato nel capitolo precedente, per mostrare sullo schermo, con writeHex32, un numero esadecimale con ampiezza maggiore di 32 bit, è necessario partire dalla DWORD più significativa; in caso contrario, writeHex32 stamperebbe delle H sul numero da visualizzare.

17.2.2 Somma tra numeri interi con segno di ampiezza arbitraria

Naturalmente, l'esempio appena illustrato è perfettamente valido anche quando i due addendi codificano numeri interi con segno a 96 bit; in tal caso, dobbiamo tenere presente che i numeri stessi risultano rappresentati in complemento a 2, modulo 296.
In base al concetto di estensione del bit di segno, possiamo dire che, tutti i numeri esadecimali a 96 bit, la cui cifra più significativa è compresa tra 0h e 7h (cioè, tra 0000b e 0111b), sono positivi; tutti i numeri esadecimali a 96 bit, la cui cifra più significativa è compresa tra 8h e Fh (cioè, tra 1000b e 1111b), sono negativi.

Per sommare tra loro numeri interi con segno di ampiezza arbitraria, si procede esattamente come mostrato nel precedente esempio; la prima somma viene eseguita con ADD, mentre le somme successive vengono eseguite con ADC. Dopo l'esecuzione dell'ultima somma (tra le due DWORD più significative), si deve consultare, non CF, bensì OF e SF; è chiaro, infatti, che il segno del risultato si trova codificato nel suo bit più significativo.
Se, dopo l'ultima somma, si ha OF=0, allora il risultato è valido e il relativo segno si trova codificato in SF; se, invece, dopo l'ultima somma, si ha OF=1, il risultato è andato in overflow (in tal caso, il contenuto di SF non ha, ovviamente, alcun significato).

17.2.3 Effetti provocati da ADC sugli operandi e sui flags

L'esecuzione dell'istruzione ADC, modifica il contenuto del solo operando DEST, che viene sovrascritto dal risultato della somma appena effettuata; il contenuto dell'operando SRC rimane inalterato.
Anche per ADC, bisogna prestare attenzione ai soliti casi del tipo:
adc bx, [bx+di+002Ah]
Dopo l'esecuzione di questa istruzione, il contenuto del registro puntatore BX viene modificato.

L'esecuzione dell'istruzione ADC modifica i campi OF, SF, ZF, AF, PF e CF del Flags Register; il contenuto di questi campi, ha l'identico significato già illustrato per l'istruzione ADD.

17.3 L'istruzione SUB

Con il mnemonico SUB si indica l'istruzione Subtraction (sottrazione tra SRC e DEST); incontrando questa istruzione, la CPU calcola:
DEST = DEST - SRC
Gli operandi SRC e DEST devono essere specificati esplicitamente dal programmatore e devono avere la stessa ampiezza in bit; il risultato della sottrazione viene disposto in DEST.
Le uniche forme lecite per l'istruzione SUB, sono le seguenti: Appare evidente il fatto che ADD e SUB sono istruzioni tra loro complementari; infatti, dati i due numeri interi A e B, possiamo scrivere:
A + B = A - (-B)
e:
A - B = A + (-B)
Non bisogna però dimenticare che, nel caso di SUB, la CPU si serve del flag CF per segnalare, non un eventuale riporto, bensì un eventuale prestito; analogamente, la CPU si serve del flag AF, per segnalare che nel corso della sottrazione, si è verificato un eventuale prestito dal secondo nibble al primo nibble.
Tutti gli esempi presentati in precedenza per l'istruzione ADD, possono essere quindi ripetuti per l'istruzione SUB, sostituendo semplicemente il mnemonico ADD con il mnemonico SUB.

Grazie alla aritmetica modulare, la CPU può eseguire le sottrazioni senza dover distinguere tra interi con o senza segno; vediamo un esempio pratico.

Poniamo AX=45200, BX=38260, ed eseguiamo l'istruzione:
sub ax, bx
In binario si ha:
AX = 45200 = 1011000010010000b
e:
BX = 38260 = 1001010101110100b
La CPU esegue quindi la sottrazione: Sulla base di questo risultato, la CPU pone:
CF = 0, PF = 0, AF = 1, ZF = 0, SF = 0, OF = 0
Per giustificare questa situazione, osserviamo innanzi tutto che il risultato è diverso da 0, per cui ZF=0; l'esecuzione della sottrazione ha prodotto un prestito dal secondo nibble al primo nibble, per cui AF=1. Negli 8 bit meno significativi del risultato, è presente un numero dispari (3) di 1; di conseguenza, PF=0.

Nell'insieme degli interi senza segno, 1011000010010000b rappresenta 45200, mentre 1001010101110100b rappresenta 38260; la sottrazione:
45200 - 38260 = 6940
produce un risultato positivo (minuendo maggiore del sottraendo), per cui CF=0 (nessun prestito).

Nell'insieme degli interi con segno, 1011000010010000b rappresenta:
45200 - 216 = -20336
mentre 1001010101110100b rappresenta:
38260 - 216 = -27276
La sottrazione:
-20336 - (-27276) = -20336 + 27276 = +6940
produce un risultato non superiore a +32767, per cui OF=0; il bit più significativo del risultato è 0, per cui SF=0 (numero positivo).

17.3.1 Effetti provocati da SUB sugli operandi e sui flags

L'esecuzione dell'istruzione SUB, modifica il contenuto del solo operando DEST, che viene sovrascritto dal risultato della sottrazione appena effettuata; il contenuto dell'operando SRC rimane inalterato.
Anche per SUB, bisogna prestare attenzione ai soliti casi del tipo:
sub di, [di]
Dopo l'esecuzione di questa istruzione, il contenuto del registro puntatore DI viene modificato.

Il metodo più semplice e intuitivo per azzerare il contenuto di un registro, consiste nel servirsi dell'istruzione MOV; se, ad esempio, vogliamo azzerare il contenuto del registro ECX, possiamo scrivere:
mov ecx, 0
La possibilità di effettuare una sottrazione tra Reg e Reg, ci permette di scrivere anche istruzioni del tipo:
sub ecx, ecx
Anche in questo caso, l'effetto prodotto consiste nell'azzeramento del contenuto di ECX.

L'esecuzione dell'istruzione SUB, modifica i campi OF, SF, ZF, AF, PF e CF del Flags Register; il contenuto di questi campi, ha lo stesso significato già illustrato per le istruzioni ADD e ADC. Questa volta però, AF indica se, nell'esecuzione della sottrazione, si è verificato un eventuale prestito dal secondo nibble al primo nibble; analogamente, CF indica se la sottrazione si è conclusa con un eventuale prestito (minuendo minore del sottraendo).

17.4 L'istruzione SBB

Con il mnemonico SBB si indica l'istruzione Integer Subtraction with Borrow (sottrazione tra SRC, DEST e CF); incontrando questa istruzione, la CPU calcola:
DEST = DEST - SRC - CF = DEST - (SRC + CF)
Gli operandi SRC e DEST devono essere specificati esplicitamente dal programmatore e devono avere la stessa ampiezza in bit; il risultato della sottrazione viene disposto in DEST.
In analogia con SUB, le uniche forme lecite per l'istruzione SBB, sono le seguenti: Come si può facilmente immaginare, l'istruzione SBB è stata concepita, principalmente, per permettere di eseguire in modo semplice, sottrazioni tra operandi di ampiezza arbitraria; a tale proposito, si devono applicare tutti i concetti esposti nel capitolo sulla matematica del computer. In particolare, è necessario ricordare che la sottrazione:
DEST - SRC - CF
non può mai provocare due prestiti consecutivi.
Tutte le considerazioni esposte per l'istruzione SUB in relazione agli effetti provocati sugli operandi e sui flags, restano valide anche per l'istruzione SBB.

Come accade con SUB, anche SBB, grazie all'aritmetica modulare, opera, indifferentemente, sui numeri interi con o senza segno; analizziamo il procedimento che bisogna seguire nei due casi che si possono presentare.

17.4.1 Sottrazione tra numeri interi senza segno di ampiezza arbitraria

Supponiamo di voler eseguire la seguente sottrazione tra interi senza segno a 96 bit: Procediamo innanzi tutto con la definizione delle due variabili; utilizzando, ad esempio, la direttiva DD, possiamo scrivere: A questo punto possiamo procedere con l'esecuzione della sottrazione; la sottrazione tra le due DWORD meno significative viene effettuata con SUB, mentre le due sottrazioni successive, vengono effettuate con SBB in quanto bisogna tenere conto dell'eventuale prestito richiesto dalle sottrazioni eseguite in precedenza.
Il codice che esegue la sottrazione assume il seguente aspetto: Dopo l'esecuzione dell'ultima sottrazione (tra le due DWORD più significative), consultiamo CF per sapere se c'è stato un prestito finale; nel nostro caso, CF=0, per cui il risultato è valido così com'è.
Se vogliamo visualizzare il risultato con writeHex32, possiamo procedere esattamente come per l'esempio riferito all'istruzione ADC.

17.4.2 Sottrazione tra numeri interi con segno di ampiezza arbitraria

Naturalmente, l'esempio appena illustrato è perfettamente valido anche quando il minuendo e il sottraendo codificano numeri interi con segno a 96 bit; in tal caso, dobbiamo tenere presente che i numeri stessi risultano rappresentati in complemento a 2, modulo 296. In base al concetto di estensione del bit di segno, possiamo dire che, tutti i numeri esadecimali a 96 bit, la cui cifra più significativa è compresa tra 0 e 7 (cioè, tra 0000b e 0111b), sono positivi; tutti i numeri esadecimali a 96 bit, la cui cifra più significativa è compresa tra 8 e F (cioè, tra 1000b e 1111b), sono negativi.

Per sottrarre tra loro numeri interi con segno di ampiezza arbitraria, si procede esattamente come mostrato nel precedente esempio; la prima sottrazione viene eseguita con SUB, mentre le sottrazioni successive vengono eseguite con SBB. Dopo l'esecuzione dell'ultima sottrazione (tra le due DWORD più significative), si deve consultare, non CF, bensì OF e SF; è chiaro, infatti, che il segno del risultato si trova codificato nel suo bit più significativo.
Se, dopo l'ultima sottrazione, si ha OF=0, allora il risultato è valido e il relativo segno si trova codificato in SF; se, invece, dopo l'ultima sottrazione, si ha OF=1, il risultato è andato in overflow (in tal caso, il contenuto di SF non ha, ovviamente, alcun significato).

17.4.3 Effetti provocati da SBB sugli operandi e sui flags

L'esecuzione dell'istruzione SBB, modifica il contenuto del solo operando DEST, che viene sovrascritto dal risultato della somma appena effettuata; il contenuto dell'operando SRC rimane inalterato.
Anche per SBB, bisogna prestare attenzione ai soliti casi del tipo:
sbb si, [si+03F8h]
Dopo l'esecuzione di questa istruzione, il contenuto del registro puntatore SI viene modificato.

L'esecuzione dell'istruzione SBB, modifica i campi OF, SF, ZF, AF, PF e CF del Flags Register; il contenuto di questi campi, ha l'identico significato già illustrato per l'istruzione SUB.

17.5 Le istruzioni INC e DEC

Con il mnemonico INC si indica l'istruzione Increment by 1 (incremento di uno); questa istruzione richiede un unico operando (DEST), il cui contenuto viene incrementato di 1. Si ottiene quindi:
DEST = DEST + 1
Si può dire quindi che l'istruzione INC è una sorta di ADD che viene usata quando l'operando sorgente vale 1; in un caso del genere, l'utilizzo di INC può portare alla generazione di un codice macchina più compatto ed efficiente rispetto a quello prodotto da ADD.
Non essendo possibile, ovviamente, incrementare un valore immediato, le uniche forme lecite per l'istruzione INC sono le seguenti: Il codice macchina generale per l'istruzione INC è rappresentato dall'Opcode 1111111w, seguito dal campo mod_000_r/m; se l'operando è un registro, viene utilizzato un codice macchina formato dal solo Opcode 01000_reg.
È chiaro quindi che, possibilmente, conviene utilizzare INC con un operando registro; in questo modo, infatti, viene generato un codice macchina da 1 byte, che verrà eseguito più velocemente dalla CPU.

Con il mnemonico DEC si indica l'istruzione Decrement by 1 (decremento di uno); questa istruzione richiede un unico operando (DEST), il cui contenuto viene decrementato di 1. Si ottiene quindi:
DEST = DEST - 1
Si può dire quindi che l'istruzione DEC è una sorta di SUB che viene usata quando l'operando sorgente (sottraendo) vale 1; in un caso del genere, l'utilizzo di DEC può portare alla generazione di un codice macchina più compatto ed efficiente rispetto a quello prodotto da SUB.
Non essendo possibile, ovviamente, decrementare un valore immediato, le uniche forme lecite per l'istruzione DEC sono le seguenti: Il codice macchina generale per l'istruzione DEC è rappresentato dall'Opcode 1111111w, seguito dal campo mod_001_r/m; se l'operando è un registro, viene utilizzato un codice macchina formato dal solo Opcode 01001_reg.
È chiaro quindi che, possibilmente, conviene utilizzare DEC con un operando registro; in questo modo, infatti, viene generato un codice macchina da 1 byte, che verrà eseguito più velocemente dalla CPU.

17.5.1 Effetti provocati da INC e DEC sugli operandi e sui flags

L'esecuzione delle istruzioni INC e DEC, modifica il contenuto dell'unico operando DEST, che viene sovrascritto dal risultato della operazione appena effettuata.

L'esecuzione dell'istruzione INC provoca la modifica dei flags PF, AF, ZF, SF, OF; come si può notare, a differenza di quanto accade con ADD, il flag CF non viene modificato!
Questo significa che se, ad esempio, AX contiene il valore FFFFh e CF=0, allora l'istruzione:
inc ax
produce il seguente effetto:
AX = FFFFh + 0001h = 0000h, con CF = 0
Dopo l'esecuzione di questa istruzione, il registro AX conterrà quindi il valore 0000h, mentre CF resterà inalterato in quanto il riporto provocato da INC viene ignorato.
Il fatto che INC non modifichi CF, è frutto di una precisa scelta fatta dai progettisti delle CPU; infatti, in questo modo è possibile utilizzare INC per incrementare il contatore di un loop (iterazione) senza interferire sul contenuto di CF.
Può capitare, però, che il programmatore abbia la necessità di provocare la modifica di CF in seguito ad una addizione il cui secondo addendo vale 1; in un caso del genere, si deve per forza usare ADD scrivendo, ad esempio:
add ax, 1
Per quanto riguarda il significato degli altri flags modificati da INC, la situazione è del tutto identica al caso dell'istruzione ADD con operando SRC che vale 1.

L'esecuzione dell'istruzione DEC provoca la modifica dei flags PF, AF, ZF, SF, OF; come si può notare, a differenza di quanto accade con SUB, il flag CF non viene modificato!
Questo significa che se, ad esempio, AX contiene il valore 0000h e CF=0, allora l'istruzione:
dec ax
produce il seguente effetto:
AX = 0000h - 0001h = FFFFh, con CF = 0
Dopo l'esecuzione di questa istruzione, il registro AX conterrà quindi il valore FFFFh, mentre CF resterà inalterato in quanto il prestito richiesto da DEC viene ignorato.
Il fatto che DEC non modifichi CF, è frutto di una precisa scelta fatta dai progettisti delle CPU; infatti, in questo modo è possibile utilizzare DEC per decrementare il contatore di un loop (iterazione) senza interferire sul contenuto di CF.
Può capitare, però, che il programmatore abbia la necessità di provocare la modifica di CF in seguito ad una sottrazione il cui sottraendo vale 1; in un caso del genere, si deve per forza usare SUB scrivendo, ad esempio:
sub ax, 1
Per quanto riguarda il significato degli altri flags modificati da DEC, la situazione è del tutto identica al caso dell'istruzione SUB con operando SRC che vale 1.

17.6 L'istruzione NEG

Con il mnemonico NEG si indica l'istruzione Two's complement negation (negazione di un numero intero con segno, espresso in complemento a 2); questa istruzione richiede un unico operando (DEST), il cui contenuto viene trattato come numero intero con segno. Dopo l'esecuzione dell'istruzione NEG, l'operando DEST contiene il valore iniziale, cambiato però di segno; ad esempio, negando il numero negativo -2500, si ottiene il suo opposto e cioè, il numero positivo +2500.
Non essendo possibile, ovviamente, negare un valore immediato, le uniche forme lecite per l'istruzione NEG sono le seguenti:

17.6.1 Complemento a 1 e complemento a 2

Nei capitoli dedicati alla matematica del computer, abbiamo visto come si deve procedere per negare un numero intero con segno; supponiamo di lavorare sugli interi con segno a 16 bit (compresi quindi tra i limiti -32768 e +32767) e vediamo come si ottiene, ad esempio, la negazione del numero +30541.
In matematica, il metodo più ovvio che si può applicare per negare un numero n, consiste nell'eseguire la sottrazione:
0 - n
Nel nostro caso, possiamo scrivere quindi:
0 - 30541 = -30541
Nell'aritmetica modulare del computer, per gli interi con segno a 16 bit in complemento a 2, il valore 0 equivale a 65536 (216); possiamo scrivere quindi:
0 - 30541 = 216 - 30541 = 34995 = 88B3h
Possiamo dire quindi che 34995 (cioè, 88B3h) è la codifica a 16 bit in complemento a 2, del numero negativo -30541.
Per poter eseguire la precedente sottrazione, possiamo procedere in questo modo:
216 - 30541 = 65536 - 30541 = (65535 + 1) - 30541 = (65535 - 30541) + 1
Traducendo tutto in binario, e svolgendo le varie operazioni otteniamo: Osserviamo subito che la prima sottrazione, non fa altro che invertire tutti i bit del numero da negare; infatti:
1111111111111111b - 0111011101001101b = 1000100010110010b
Come sappiamo, questa fase prende il nome di complemento a 1; per effettuare il complemento a 1 di un numero n (cioè, l'inversione di tutti i bit di n), la CPU mette a disposizione l'istruzione NOT, che verrà analizzata in un prossimo capitolo.
La seconda fase, che consiste nel sommare 1 al risultato della sottrazione, prende il nome di complemento a 2; possiamo dire allora che:
NEG(n) = NOT(n) + 1
Questo è proprio il procedimento che la CPU utilizza per negare un numero intero con segno.

Applicando ora questo stesso metodo per negare -30541 (cioè, 1000100010110011b), dovremmo ottenere:
-(-30541) = +30541
Infatti: Traducendo 77D4h in base 10, otteniamo proprio +30541!

17.6.2 Negazione di numeri interi con segno di ampiezza arbitraria

Per negare un numero intero con segno di ampiezza arbitraria, possiamo utilizzare il metodo appena illustrato; a tale proposito, supponiamo di voler negare un numero formato da 96 bit (3 DWORD). Prima di tutto, invertiamo tutti bit delle 3 DWORD; successivamente, sommiamo 1 al risultato appena ottenuto.
Per effettuare la somma, ci serviamo dello stesso procedimento illustrato per l'istruzione ADC; quando avremo a disposizione l'istruzione NOT, vedremo un esempio pratico.

17.6.3 Effetti provocati da NEG sugli operandi e sui flags

L'esecuzione dell'istruzione NEG, modifica il contenuto dell'unico operando DEST, che viene sovrascritto dal risultato della negazione.

L'esecuzione dell'istruzione NEG provoca la modifica dei flags CF, PF, AF, ZF, SF, OF; per capire il significato di questi flags, bisogna ricordare che la negazione di un numero n, equivale allo svolgimento della sottrazione:
0 - n
Si deduce allora che se n=0, otteniamo:
0 - 0 = 0
Questa sottrazione non richiede, ovviamente, alcun prestito, per cui CF=0
Se, invece, il numero n da negare è diverso da zero, si ottiene sempre CF=1; infatti, per negare, ad esempio, +15800, dobbiamo scrivere:
0 - 15800 = -15800
Questa sottrazione richiede, ovviamente, un prestito.

Anche per quanto riguarda gli altri flags, la situazione è abbastanza chiara; è sufficiente applicare tutte le considerazioni già svolte per l'istruzione SUB (con minuendo uguale a 0).
Analizziamo, in particolare, il significato del flag OF; a tale proposito, supponiamo di voler operare sugli interi con segno a 16 bit, in complemento a 2, compresi quindi tra -32768 e +32767.
Negando un numero strettamente positivo, compreso quindi tra +1 e +32767, otteniamo, come previsto, un numero negativo compreso tra -1 e -32767; la CPU pone quindi OF=0. Viceversa, negando un numero negativo, compreso quindi tra -1 e -32767, otteniamo, come previsto, un numero positivo compreso tra +1 e +32767; la CPU pone quindi OF=0
Il numero negativo più piccolo e cioè, -32768, rappresenta un caso particolare; infatti:
0 - (-32768) = 0 + 32768 = 32768
Ma per gli interi con segno a 16 bit in complemento a 2, 32768 è la codifica dello stesso numero negativo -32768; in sostanza, abbiamo ottenuto un numero "troppo" positivo che sconfina nell'insieme dei numeri negativi, per cui la CPU pone OF=1.

17.7 L'istruzione CMP

Con il mnemonico CMP si indica l'istruzione Compare two operands (comparazione tra due operandi); questa istruzione ha lo scopo di confrontare il contenuto dell'operando SRC con il contenuto dell'operando DEST, in modo da stabilire la relazione d'ordine che esiste tra i due operandi. In sostanza, grazie a CMP possiamo sapere se DEST è maggiore di, minore di o uguale a SRC.
Il confronto viene effettuato attraverso la sottrazione:
Temp = DEST - SRC
In base al risultato della sottrazione, appare evidente che: Subito dopo aver eseguito la sottrazione, la CPU modifica gli stessi flags coinvolti dall'istruzione SUB (con gli stessi operandi DEST e SRC); come viene spiegato più avanti, attraverso la consultazione di questi flags possiamo conoscere il risultato della comparazione.
Osserviamo che il risultato della sottrazione viene disposto in un registro temporaneo della CPU e questo significa che, a differenza di quanto accade con SUB, l'istruzione CMP preserva il contenuto, non solo di SRC, ma anche di DEST; è chiaro, infatti, che lo scopo di CMP è solo quello di confrontare due operandi, senza provocare su di essi alcuna modifica.

Le uniche forme lecite per l'istruzione CMP sono le seguenti: Gli operandi SRC e DEST devono essere specificati esplicitamente dal programmatore e devono avere la stessa ampiezza in bit; un eventuale operando SRC di tipo Imm, viene esteso attraverso il suo bit di segno, sino a raggiungere l'ampiezza in bit dell'operando DEST.

L'istruzione CMP, essendo formalmente identica a SUB, opera indifferentemente sui numeri interi con o senza segno; analizziamo in dettaglio il procedimento che bisogna seguire per determinare, nei due casi, il risultato della comparazione.

17.7.1 Comparazione tra numeri interi senza segno

Come è stato detto in precedenza, l'esecuzione dell'istruzione CMP provoca la modifica degli stessi flags coinvolti da SUB e cioè, CF, PF, AF, ZF, SF, OF; il contenuto di questi flags ci fornisce il risultato della comparazione.

Poniamo AX=45200, BX=38260, ed eseguiamo l'istruzione:
cmp ax, bx
In binario si ha:
AX = 45200 = 1011000010010000b
e:
BX = 38260 = 1001010101110100b
La CPU esegue quindi la sottrazione: Sulla base di questo risultato, la CPU pone:
CF = 0, PF = 0, AF = 1, ZF = 0, SF = 0, OF = 0
Per giustificare questa situazione, osserviamo innanzi tutto che il risultato è diverso da 0, per cui ZF=0; ciò significa che, indipendentemente dal contenuto degli altri flags, DEST è sicuramente diverso da SRC (altrimenti avremmo ottenuto ZF=1).
Nell'insieme degli interi senza segno, 1011000010010000b rappresenta 45200, mentre 1001010101110100b rappresenta 38260; la sottrazione:
45200 - 38260 = 6940
produce un risultato positivo (minuendo maggiore o uguale al sottraendo), per cui CF=0 (nessun prestito). Ciò significa che, indipendentemente dal contenuto degli altri flags, DEST è sicuramente non minore di SRC; se il minuendo fosse stato minore del sottraendo, avremmo ottenuto CF=1 (prestito) e quindi, indipendentemente dal contenuto degli altri flags, DEST sarebbe stato sicuramente non maggiore di SRC.

A questo punto, è perfettamente inutile mostrare altri esempi in quanto abbiamo già capito che i due soli flags, ZF e CF, ci forniscono in modo inequivocabile il risultato di una qualsiasi comparazione tra numeri interi senza segno; per rappresentare i vari risultati possibili, ci serviamo dei seguenti simboli (presi in prestito dal linguaggio C): Utilizzando questi simboli, possiamo affermare che: Dalle considerazioni appena esposte, si può notare che il metodo per ricavare il risultato della comparazione tra numeri interi senza segno è piuttosto semplice; in ogni caso, è importante capire bene il significato di concetti come, minore o uguale, maggiore o uguale, non minore, non maggiore, etc.
Se ZF=1, allora DEST è sicuramente UGUALE a SRC, indipendentemente dal contenuto di CF; possiamo anche dire che DEST è NON DIVERSO da SRC, oppure che DEST è MAGGIORE O UGUALE a SRC, oppure che DEST è MINORE O UGUALE a SRC, etc.
Analogamente, se ZF=0, allora DEST è sicuramente DIVERSO da SRC, indipendentemente dal contenuto di CF; possiamo anche dire che DEST è NON UGUALE a SRC.
Se ZF=0 e CF=0, allora DEST è sicuramente MAGGIORE di SRC; possiamo anche dire che DEST è NON MINORE NE' UGUALE a SRC.
Se ZF=0 e CF=1, allora DEST è sicuramente MINORE di SRC; possiamo anche dire che DEST è NON MAGGIORE NE' UGUALE a SRC.

Tutti questi aspetti verranno ulteriormente chiariti in un apposito capitolo.

17.7.2 Comparazione tra numeri interi con segno

Per quanto riguarda la comparazione tra numeri interi con segno, la situazione diventa più impegnativa; in ogni caso, basta seguire la logica per rendersi conto che non c'è niente di complesso.
Facciamo riferimento ai numeri interi con segno a 16 bit in complemento a 2, compresi quindi tra i limiti -32768 e +32767; come si può facilmente intuire, questa volta i flags che ci interessano sono OF, SF e ZF.
Complessivamente, si possono presentare quattro casi distinti, che vengono analizzati nel seguito; appare ovvio il fatto che, anche per i numeri interi con segno, il flag ZF indica se i due operandi sono uguali (ZF=1) o diversi (ZF=0).

1) La sottrazione DEST - SRC produce un risultato compreso tra 0 e +32767; ciò accade quando DEST è "leggermente" maggiore o uguale a SRC. Vediamo alcuni esempi: Come si può notare, in questo caso, non essendo possibile l'overflow, il flag OF vale sempre 0; anche SF vale sempre 0 in quanto DEST è maggiore o uguale a SRC e quindi la sottrazione produce sempre un risultato non negativo. In definitiva, possiamo dire che in questo caso, OF e SF sono sempre uguali tra loro e valgono entrambi 0.

2) La sottrazione DEST - SRC produce un risultato compreso tra +32768 e +65535; ciò accade quando DEST è "eccessivamente" maggiore di SRC. Vediamo alcuni esempi: In pratica, DEST è strettamente maggiore di SRC, ma la loro differenza produce un numero "troppo" positivo che sconfina nell'insieme dei numeri negativi; il risultato va quindi in overflow, per cui si avrà sempre OF=1. Anche SF vale sempre 1, in quanto la sottrazione produce sempre un risultato che sconfina nell'insieme dei numeri negativi; in definitiva, possiamo dire che in questo caso, OF e SF sono sempre uguali tra loro e valgono entrambi 1.

3) La sottrazione DEST - SRC produce un risultato compreso tra -32768 e -1; ciò accade quando DEST è "leggermente" minore di SRC. Vediamo alcuni esempi: Come si può notare, in questo caso, non essendo possibile l'overflow, il flag OF vale sempre 0; il flag SF, invece, vale sempre 1 in quanto DEST è minore di SRC e quindi la sottrazione produce sempre un risultato negativo. In definitiva, possiamo dire che in questo caso, OF e SF sono sempre diversi tra loro, in quanto assumono, rispettivamente, i valori 0 e 1.

4) La sottrazione DEST - SRC produce un risultato compreso tra -65535 e -32769; ciò accade quando DEST è "eccessivamente" minore di SRC. Vediamo alcuni esempi: In pratica, DEST è strettamente minore di SRC, ma la loro differenza produce un numero "troppo" negativo, che sconfina nell'insieme dei numeri positivi; il risultato va quindi in overflow, per cui si avrà sempre OF=1. Il flag SF, invece, vale sempre 0, in quanto la sottrazione produce sempre un risultato che sconfina nell'insieme dei numeri positivi; in definitiva, possiamo dire che in questo caso, OF e SF sono sempre diversi tra loro, in quanto assumono, rispettivamente, i valori 1 e 0.

Sulla base delle considerazioni appena esposte, appare chiaro il metodo che bisogna seguire per ricavare dai flags il risultato della comparazione tra due numeri interi con segno; in pratica, possiamo affermare che: Questa tabella può essere ulteriormente semplificata osservando che, se ZF=1, allora DEST è sicuramente uguale a SRC e ciò vale indipendentemente dal contenuto di OF e SF; se, invece, ZF=0, allora DEST è sicuramente diverso da SRC e ciò vale indipendentemente dal contenuto di OF e SF. In particolare, se ZF=0 e OF è uguale a SF, allora DEST è sicuramente maggiore di SRC; se, invece, ZF=0 e OF è diverso da SF, allora DEST è sicuramente minore di SRC.

17.7.3 Istruzioni per il controllo del flusso

Le considerazioni appena esposte sono molto semplici da un punto di vista teorico, ma piuttosto scomode da applicare in pratica; infatti, sarebbe una follia pensare di svolgere tutti questi controlli ogni volta che dobbiamo effettuare una banale comparazione numerica.
Fortunatamente, però, la CPU ci viene incontro mettendoci a disposizione una miriade di istruzioni che eseguono tutto questo lavoro al nostro posto; si tratta delle cosiddette istruzioni per il controllo del flusso (o istruzioni per il trasferimento del controllo) che, come è stato spiegato nel capitolo dedicato alle reti combinatorie, hanno una importanza enorme nella programmazione. Attraverso queste istruzioni, è possibile scrivere programmi "intelligenti", capaci di prendere decisioni in base al risultato di una comparazione numerica (o di altri tipi di test); le istruzioni per il trasferimento del controllo, verranno illustrate in un apposito capitolo.
Utilizzando queste stesse istruzioni, i linguaggi di alto livello implementano particolari costrutti sintattici, come le istruzioni condizionali IF, THEN, ELSE, i cicli FOR, i cicli WHILE DO, i cicli REPEAT UNTIL, etc; considerando, ad esempio, due variabili numeriche A e B, possiamo scrivere la seguente sequenza di istruzioni in pseudo-linguaggio C: Come si può notare, questo programma è in grado di compiere tre scelte distinte, in base al risultato della comparazione tra A e B; il codice macchina di questo programma, generato dal compilatore C, utilizza proprio l'istruzione CMP per effettuare le varie comparazioni.

17.7.4 Comparazione tra numeri interi di ampiezza arbitraria

Un'ultima considerazione sull'istruzione CMP, riguarda il caso in cui si abbia la necessità di dover comparare tra loro numeri interi di ampiezza arbitraria; in tal caso, il metodo da seguire è molto semplice e consiste nel mettere assieme le cose dette per le due istruzioni SBB e CMP.
In pratica, prima di tutto si sottraggono i due operandi attraverso il metodo illustrato per l'istruzione SBB; si può notare che tale metodo, preserva il contenuto di entrambi gli operandi. Terminata la sottrazione, se si sta operando sui numeri interi senza segno, si consultano i flags CF e ZF; se, invece, si sta operando sui numeri interi con segno, si consultano i flags OF, SF e ZF.

17.7.5 Effetti provocati da CMP sugli operandi e sui flags

L'esecuzione dell'istruzione CMP preserva il contenuto, sia dell'operando SRC, sia dell'operando DEST; infatti, il risultato della sottrazione tra DEST e SRC viene ignorato.

L'esecuzione dell'istruzione CMP, modifica i campi OF, SF, ZF, AF, PF e CF del Flags Register; il contenuto di questi campi, ha l'identico significato già illustrato per l'istruzione SUB.

17.8 Le istruzioni CBW, CWD, CWDE e CDQ

Nel precedente capitolo abbiamo visto che con le CPU 80386 e superiori, attraverso le due istruzioni MOVSX e MOVZX è possibile effettuare un trasferimento dati da un operando sorgente avente una certa ampiezza in bit, ad un operando destinazione avente ampiezza maggiore; l'istruzione MOVZX opera sui numeri interi senza segno, mentre l'istruzione MOVSX opera sui numeri interi con segno.
Se si utilizza MOVZX, il valore da trasferire viene sempre trattato come numero intero senza segno (unsigned) e viene "allungato" a sinistra con degli zeri (zero extension); se si utilizza MOVSX, il valore da trasferire viene sempre trattato come numero intero con segno (signed) in complemento a 2 e viene "allungato" a sinistra attraverso il suo bit di segno (sign extension).

Tutte le CPU della famiglia 80x86, dispongono anche di una serie di istruzioni aritmetiche che permettono di estendere l'ampiezza in bit del valore contenuto in un operando; tale operando, viene sempre trattato come numero intero con segno in complemento a 2 e viene quindi sottoposto all'estensione del suo bit di segno.

17.8.1 L'istruzione CBW

Con il mnemonico CBW si indica l'istruzione Convert Byte to Word (conversione di un BYTE in una WORD); l'unica forma lecita per questa istruzione è:
CBW
Il valore da convertire deve trovarsi obbligatoriamente nel registro (implicito) AL; tale valore, viene trattato sempre come numero intero con segno a 8 bit in complemento a 2.
Quando la CPU incontra l'istruzione CBW, legge il bit più significativo di AL e lo copia in tutti gli 8 bit di AH; il risultato ottenuto dalla CPU si trova quindi nel registro AX.

Supponiamo, ad esempio, di avere AL=01001110b (+78); in presenza dell'istruzione:
cbw
la CPU legge il bit più significativo di AL (0) e lo copia in tutti gli 8 bit di AH. Si ottiene quindi:
AX = 0000000001001110b = 78
Come si può notare, abbiamo ottenuto la rappresentazione a 16 bit in complemento a 2, del numero intero con segno +78.

Supponiamo, ad esempio, di avere AL=11101110b (-18); in presenza dell'istruzione:
cbw
la CPU legge il bit più significativo di AL (1) e lo copia in tutti gli 8 bit di AH. Si ottiene quindi:
AX = 1111111111101110b = 65518
Come si può notare, abbiamo ottenuto la rappresentazione a 16 bit in complemento a 2, del numero intero con segno -18; infatti:
65518 - 216 = -18

17.8.2 Le istruzioni CWD e CWDE

Con il mnemonico CWD si indica l'istruzione Convert Word to Doubleword (conversione di una WORD in una DWORD); l'unica forma lecita per questa istruzione è:
CWD
Il valore da convertire deve trovarsi obbligatoriamente nel registro (implicito) AX; tale valore, viene trattato sempre come numero intero con segno a 16 bit in complemento a 2.
Quando la CPU incontra l'istruzione CWD, legge il bit più significativo di AX e lo copia in tutti i 16 bit di DX; il risultato ottenuto dalla CPU, si trova quindi nella coppia di registri DX:AX, con DX che, nel rispetto della convenzione little endian, contiene la WORD più significativa del risultato.

Supponiamo, ad esempio, di avere AX=0111111111111111b (+32767); in presenza dell'istruzione:
cwd
la CPU legge il bit più significativo di AX (0) e lo copia in tutti i 16 bit di DX. Si ottiene quindi:
DX:AX = 00000000000000000111111111111111b = 32767
Come si può notare, abbiamo ottenuto la rappresentazione a 32 bit in complemento a 2, del numero intero con segno +32767.

Supponiamo, ad esempio, di avere AX=1000000000000000b (-32768); in presenza dell'istruzione:
cwd
la CPU legge il bit più significativo di AX (1) e lo copia in tutti i 16 bit di DX. Si ottiene quindi:
DX:AX = 11111111111111111000000000000000b = 4294934528
Come si può notare, abbiamo ottenuto la rappresentazione a 32 bit in complemento a 2, del numero intero con segno -32768; infatti:
4294934528 - 232 = -32768
L'istruzione CWD è stata concepita per poter lavorare nel modo 16 bit delle CPU 8086; se vogliamo lavorare, esplicitamente, nel modo 32 bit delle CPU 80386 e superiori, possiamo servirci dell'istruzione CWDE.
Con il mnemonico CWDE si indica l'istruzione Convert Word to Doubleword - Extended form (conversione di una WORD in una DWORD); l'unica forma lecita per questa istruzione è:
CWDE
Il valore da convertire deve trovarsi obbligatoriamente nel registro (implicito) AX; tale valore, viene trattato sempre come numero intero con segno a 16 bit in complemento a 2.
Quando la CPU incontra l'istruzione CWDE, legge il bit più significativo di AX e lo copia in tutti i 16 bit più significativi di EAX; il risultato ottenuto dalla CPU si trova quindi nel registro EAX.
Ripetendo allora con CWDE i due esempi appena illustrati per CWD, otteniamo direttamente in EAX l'intero risultato finale a 32 bit.

17.8.3 L'istruzione CDQ

Con il mnemonico CDQ si indica l'istruzione Convert Doubleword to Quadword (conversione di una DWORD in una QWORD); l'unica forma lecita per questa istruzione è:
CDQ
Il valore da convertire deve trovarsi obbligatoriamente nel registro (implicito) EAX; tale valore, viene trattato sempre come numero intero con segno a 32 bit in complemento a 2.
Quando la CPU incontra l'istruzione CDQ, legge il bit più significativo di EAX e lo copia in tutti i 32 bit di EDX; il risultato ottenuto dalla CPU, si trova quindi nella coppia di registri EDX:EAX, con EDX che, nel rispetto della convenzione little endian, contiene la DWORD più significativa del risultato.

Supponiamo, ad esempio, di avere EAX=01110111001100110000110111001010b (+1999834570); in presenza dell'istruzione:
cdq
la CPU legge il bit più significativo di EAX (0) e lo copia in tutti i 32 bit di EDX. Si ottiene quindi:
EDX:EAX = 0000000000000000000000000000000001110111001100110000110111001010b = 1999834570
Come si può notare, abbiamo ottenuto la rappresentazione a 64 bit in complemento a 2, del numero intero con segno +1999834570.

Supponiamo, ad esempio, di avere EAX=11000111111011111110100111110110b (-940578314); in presenza dell'istruzione:
cdq
la CPU legge il bit più significativo di EAX (1) e lo copia in tutti i 32 bit di EDX. Si ottiene quindi:
EDX:EAX = 1111111111111111111111111111111111000111111011111110100111110110b = 18446744072768973302
Come si può notare, abbiamo ottenuto la rappresentazione a 64 bit in complemento a 2, del numero intero con segno -940578314; infatti:
18446744072768973302 - 264 = -940578314

17.8.4 Estensione di numeri interi con segno di ampiezza arbitraria

Se vogliamo operare su numeri interi con segno di ampiezza arbitraria, non dobbiamo fare altro che applicare i concetti appena esposti; supponiamo, ad esempio, di voler estendere a 128 bit il seguente numero intero con segno a 64 bit:
BigNum64 dq 93B148F6C839ABF3h
Prima di tutto, creiamo una locazione di memoria da 128 bit con la seguente definizione:
BigNum128 dd 4 dup (0) ; 4 locazioni da 4 byte ciascuna
Nei 64 bit meno significativi di BigNum128 dobbiamo disporre, ovviamente, il contenuto di BigNum64; in tutti i 64 bit più significativi di BigNum128, dobbiamo copiare il bit di segno di BigNum64.
A tale proposito, possiamo applicare un trucco molto semplice, che consiste nell'utilizzare CDQ per estendere solo la DWORD più significativa (cioè, quella contenente il bit di segno) di BigNum64; come sappiamo, il risultato ottenuto da CDQ viene disposto nella coppia di registri EDX:EAX. Il registro EDX contiene quindi l'estensione del bit di segno di BigNum64; infatti, se BigNum64 è positivo (cifra più significativa compresa tra 0h e 7h), si ottiene EDX=00000000h, mentre se BigNum64 è negativo (cifra più significativa compresa tra 8h e Fh), si ottiene EDX=FFFFFFFFh.
Per svolgere tutto questo lavoro, possiamo utilizzare il seguente codice: Osserviamo che, in relazione all'esecuzione di CDQ, il registro EAX contiene già il valore che ci interessa e cioè, BigNum64[4].
Se ora vogliamo visualizzare il risultato con writeHex32, possiamo utilizzare lo stesso metodo illustrato per l'istruzione ADC.

17.8.5 Effetti provocati da CBW, CWD, CWDE e CDQ, sugli operandi e sui flags

In base alle considerazioni appena svolte, risulta che l'esecuzione dell'istruzione CBW, modifica il contenuto del solo registro AX; l'esecuzione dell'istruzione CWD, modifica il contenuto dei registri AX e DX. L'esecuzione dell'istruzione CWDE, modifica il contenuto del solo registro EAX; l'esecuzione dell'istruzione CDQ, modifica il contenuto dei registri EAX e EDX.

Le istruzioni CBW, CWD, CWDE e CDQ, hanno il solo scopo di effettuare l'estensione del bit di segno del loro operando; di conseguenza, l'esecuzione di queste istruzioni non modifica alcun campo del Flags Register.

17.9 L'istruzione MUL

Con il mnemonico MUL si indica l'istruzione Unsigned Multiply (moltiplicazione di un numero intero senza segno per AL/AX/EAX); le uniche forme lecite per l'istruzione MUL, sono le seguenti: Come si può notare, il programmatore deve specificare, esplicitamente, uno solo dei due fattori (SRC); l'altro fattore è, implicitamente, AL, AX o EAX. A seconda dell'ampiezza in bit (8, 16 o 32) dell'operando SRC, la CPU decide se moltiplicare, rispettivamente, per AL, per AX o per EAX; è proibito l'uso di un operando SRC di tipo Imm.

Come abbiamo visto nel capitolo dedicato alla matematica del computer, moltiplicando tra loro due numeri interi senza segno da n bit ciascuno, otteniamo un risultato che occupa, al massimo, 2n bit; inoltre, il prodotto massimo che si può ottenere con due fattori da n bit, è nettamente inferiore al valore massimo rappresentabile con 2n bit.
Considerando, ad esempio, numeri interi senza segno a 8 bit, il prodotto massimo che possiamo ottenere è:
255 * 255 = 65025
Il valore 65025 è nettamente inferiore al valore massimo (65535) rappresentabile con 16 bit.

Le considerazioni appena svolte dimostrano, inoltre, che la moltiplicazione provoca un cambiamento di modulo; proprio per questo motivo, la CPU dispone di una istruzione che moltiplica numeri interi senza segno (MUL) e di una istruzione che moltiplica numeri interi con segno (IMUL).

Se vogliamo conoscere le convenzioni seguite dalla CPU per la memorizzazione del risultato della moltiplicazione eseguita con MUL, dobbiamo analizzare i tre casi fondamentali che si possono presentare.

17.9.1 Moltiplicazione tra numeri interi senza segno a 8 bit

Poniamo, AL=190, BH=212 ed eseguiamo l'istruzione:
mul bh
In presenza di questa istruzione, la CPU utilizza implicitamente AL e calcola:
AX = AL * BH = 190 * 212 = 40280
Come si può notare, il risultato a 16 bit 40280, viene disposto nel registro AX.

17.9.2 Moltiplicazione tra numeri interi senza segno a 16 bit

Poniamo, AX=36850 e supponiamo che ES:DI punti in memoria ad una WORD che vale 60321; eseguiamo ora l'istruzione:
mul word ptr es:[di]
L'operatore WORD PTR è necessario per indicare all'assembler che gli operandi sono a 16 bit; il segment override è necessario perché ES non è il registro di segmento naturale per i dati.
In presenza di questa istruzione, la CPU utilizza implicitamente AX e calcola:
DX:AX = AX * ES:[DI] = 36850 * 60321 = 2222828850
Come si può notare, il risultato a 32 bit 2222828850, viene disposto nella coppia di registri DX:AX; nel rispetto della convenzione little endian, il registro DX contiene la WORD più significativa del risultato.

17.9.3 Moltiplicazione tra numeri interi senza segno a 32 bit

Poniamo, EAX=1967850342, ECX=3997840212 ed eseguiamo la seguente istruzione:
mul ecx
In presenza di questa istruzione, la CPU utilizza implicitamente EAX e calcola:
EDX:EAX = EAX * ECX = 1967850342 * 3997840212 = 7867151228445552504
Come si può notare, il risultato a 64 bit 7867151228445552504, viene disposto nella coppia di registri EDX:EAX; nel rispetto della convenzione little endian, il registro EDX contiene la DWORD più significativa del risultato.

17.9.4 Moltiplicazione tra numeri interi senza segno di ampiezza arbitraria

Per moltiplicare numeri interi senza segno di ampiezza arbitraria, possiamo servirci di un algoritmo del tutto simile a quello che si utilizza con carta e penna; per analizzare tale algoritmo, supponiamo di voler calcolare:
3CFEh * Ah = 261ECh
Osserviamo subito che il primo prodotto parziale è:
Ah * Eh = 8Ch
cioè Ch con riporto pari a 8h; il riporto 8h verrà sommato al prodotto parziale successivo.

Il secondo prodotto parziale è:
Ah * Fh = 96h
cioè 6h con riporto pari a 9h; alla cifra 6h dobbiamo sommare il precedente riporto 8h, ottenendo:
6h + 8h = Eh
Osserviamo che questa somma, potrebbe anche provocare un carry (CF=1); in tal caso, sarebbe necessario incrementare di 1 il riporto da sommare al prodotto parziale successivo.

Il terzo prodotto parziale è:
Ah * Ch = 78h
cioè 8h con riporto pari a 7h; alla cifra 8h dobbiamo sommare il precedente riporto 9h, ottenendo:
8h + 9h = 11h
Questa volta, la somma ha provocato un carry (CF=1); il riporto 7h da sommare al prodotto parziale successivo deve essere quindi incrementato di 1 e diventa così 8h.

Il quarto prodotto parziale è:
Ah * 3h = 1Eh
cioè Eh con riporto pari a 1h; alla cifra Eh dobbiamo sommare il precedente riporto 8h, ottenendo:
Eh + 8h = 16h
Anche questa volta, la somma ha provocato un carry (CF=1); il riporto 1h da sommare al prodotto parziale successivo deve essere quindi incrementato di 1 e diventa così 2h.

Non esistendo altri prodotti parziali da eseguire, l'ultimo riporto 2h rappresenta la cifra più significativa del risultato; unendo le cinque cifre che abbiamo ricavato nei calcoli precedenti, otteniamo proprio il prodotto finale 261ECh.

In base alle considerazioni appena esposte, resta un solo dubbio legato al caso in cui la somma tra un prodotto parziale e il riporto precedente, provochi un carry; se si verifica questa eventualità, abbiamo visto che dobbiamo incrementare di 1 la cifra più significativa dello stesso prodotto parziale. La domanda che allora ci poniamo è: questo incremento di 1 può provocare un ulteriore carry?
La risposta è no!
Infatti, il prodotto parziale più grande che possiamo ottenere è:
Fh * Fh = E1h
Se il riporto precedente era Fh (cioè, il più grande possibile), dobbiamo sommarlo alla cifra meno significativa del prodotto parziale E1h, ottenendo:
Fh + 1h = 10h
Come si può notare, questa somma ha provocato un carry, per cui dobbiamo incrementare di 1 la cifra più significativa del prodotto parziale E1h, ottenendo:
Eh + 1h = Fh
Possiamo dire quindi che l'eventualità che si verifichi un ulteriore carry è assolutamente impossibile!

Una volta chiariti questi dettagli, possiamo passare alla implementazione pratica dell'algoritmo per la moltiplicazione tra numeri interi senza segno di ampiezza arbitraria; supponendo di avere a disposizione una CPU 80386 o superiore, ci serviremo dell'istruzione MUL con operandi di tipo DWORD.
In tal caso, ogni DWORD equivale ad una delle cifre esadecimali dell'esempio che abbiamo svolto in precedenza; in particolare, osserviamo che la CPU, nell'eseguire l'istruzione MUL, si serve implicitamente del registro EAX e ci restituisce i vari prodotti parziali nella coppia EDX:EAX. La DWORD contenuta in EDX rappresenta il riporto destinato al prodotto parziale successivo, mentre la DWORD contenuta in EAX, deve essere sommata con l'eventuale riporto precedente; il risultato di questa somma rappresenta una delle DWORD del prodotto finale e deve essere quindi salvato in una apposita locazione di memoria.
Come sappiamo, la somma tra EAX e l'eventuale riporto precedente, può provocare un carry; in tal caso, dobbiamo incrementare di 1 la DWORD contenuta in EDX.
Per effettuare questo incremento, possiamo servirci dell'istruzione:
adc edx, 0
Come si può facilmente constatare, questa istruzione calcola:
EDX = EDX + 0 + CF
Di conseguenza, se CF=0, allora EDX rimane invariato; se, invece, CF=1, allora EDX viene incrementato di 1.
In base a quanto è stato dimostrato in precedenza, questo incremento di EDX non può mai provocare un ulteriore carry; infatti, il prodotto massimo tra operandi di tipo DWORD è:
FFFFFFFFh * FFFFFFFFh = FFFFFFFE00000001h
Otteniamo quindi 00000001h con riporto pari a FFFFFFFEh; se il riporto precedente era FFFFFFFFh (cioè, il più grande possibile), dobbiamo eseguire la somma:
00000001h + FFFFFFFFh = 100000000h
Questa somma provoca un carry, per cui dobbiamo incrementare di 1 il riporto FFFFFFFEh, ottenendo:
FFFFFFFEh + 1h = FFFFFFFFh
Un ulteriore carry è quindi assolutamente impossibile.

Anche il contenuto di EDX deve essere salvato da qualche parte; infatti, la coppia EDX:EAX viene sovrascritta dal prodotto parziale successivo.
In analogia con l'esempio svolto in precedenza, possiamo dire che l'ultimo riporto che otteniamo rappresenta la DWORD più significativa del prodotto finale.

A questo punto possiamo procedere con un esempio pratico; a tale proposito, supponiamo di voler calcolare:
3AC926F7914388C21F8694EDh * 8BA9F6C5h = 20123F9DAD8B72F05DA7AA6295215861h
Moltiplicando un numero a 96 bit per un numero a 32 bit, si ottiene un risultato che richiede non più di 96+32=128 bit; nel blocco dati del nostro programma, possiamo inserire allora le seguenti definizioni: Il codice che esegue la moltiplicazione, assume il seguente aspetto: L'operatore DWORD PTR è stato inserito solo per rendere più chiaro il codice; la sua presenza è superflua in quanto la variabile moltiplicando è stata definita di tipo DWORD e rende quindi esplicito, l'utilizzo di operandi a 32 bit.

Se ora vogliamo visualizzare il risultato con writeHex32, possiamo utilizzare lo stesso metodo illustrato per l'istruzione ADC.

L'esempio appena svolto è molto semplice, grazie al fatto che il moltiplicatore è formato da una sola DWORD, direttamente gestibile dalla CPU; cosa succede se anche il moltiplicatore è formato da due o più DWORD?
Anche in un caso del genere, dobbiamo applicare lo stesso metodo che si segue con carta e penna; a tale proposito, supponiamo di voler calcolare:
1CF8h * 3BA6h = 06BFF0D0h
Il moltiplicatore è formato da 4 cifre, per cui dobbiamo calcolare 4 prodotti con lo stesso metodo illustrato in precedenza; il primo prodotto è:
1CF8h * 6h = 0000ADD0h
Il secondo prodotto è:
1CF8h * Ah = 000121B0h
Il terzo prodotto è:
1CF8h * Bh = 00013EA8h
Il quarto prodotto è:
1CF8h * 3h = 000056E8h
I 4 prodotti così ottenuti, vanno sommati tra loro, ricordando, però, che: Per evitare che i vari shift verso sinistra, applicati a ciascun prodotto parziale, possano provocare il trabocco di cifre significative, è importante che gli stessi prodotti parziali vengano rappresentati con almeno 8 cifre esadecimali (32 bit); infatti, moltiplicando tra loro due fattori a 4 cifre esadecimali, si ottiene un prodotto che richiede, al massimo, 4+4=8 cifre esadecimali.
In riferimento al nostro esempio, il prodotto parziale massimo che possiamo ottenere è:
FFFFh * Fh = 000EFFF1h
Il massimo numero di shift verso sinistra, viene applicato al quarto prodotto parziale, ed è pari a 3 posti; nel nostro caso, il valore 000EFFF1h, diventa EFFF1000h, senza trabocco da sinistra di cifre significative.

Tornando al nostro esempio, la somma tra i 4 prodotti parziali produce il seguente risultato: Per mettere in pratica la tecnica appena illustrata, osserviamo che, come al solito, se decidiamo di operare sulle singole DWORD dei due fattori, ciascuna di queste DWORD rappresenta una delle cifre esadecimali dell'esempio appena illustrato; supponiamo allora di voler calcolare:
2BF5218A9ECB00A3h * 86CDAF9731445EDBh = 1725A100F3199F0751EF6E14C0316571h
Moltiplicando un numero a 64 bit per un numero a 64 bit, si ottiene un risultato che occupa non più di 64+64=128 bit; nel blocco dati del nostro programma, possiamo inserire allora le seguenti definizioni: Applichiamo ora la tecnica che già conosciamo, per moltiplicare il moltiplicando per ciascuna DWORD del moltiplicatore, a partire da quella meno significativa; in questo modo, otteniamo 2 prodotti parziali.
Il primo prodotto parziale consiste nel calcolare:
parziale1 = moltiplicando * (dword ptr moltiplicatore[0])
In questo modo otteniamo il valore a 96 bit:
parziale1 = 2BF5218A9ECB00A3h * 31445EDBh = 0875A8D20E3BA0EFC0316571h
Il secondo prodotto parziale consiste nel calcolare:
parziale2 = moltiplicando * (dword ptr moltiplicatore[4])
In questo modo otteniamo il valore a 96 bit:
parziale2 = 2BF5218A9ECB00A3h * 86CDAF97h = 1725A100EAA3F63543B3CD25h
I 2 prodotti parziali così ottenuti, vanno sommati tra loro, ricordando, però, che: Per evitare che i vari shift verso sinistra, applicati a ciascun prodotto parziale, possano provocare il trabocco di cifre significative, è importante che gli stessi prodotti parziali vengano rappresentati con almeno 32 cifre esadecimali (128 bit); infatti, moltiplicando tra loro due fattori a 16 cifre esadecimali, si ottiene un prodotto che richiede, al massimo, 16+16=32 cifre esadecimali.
Osserviamo che il prodotto parziale massimo che possiamo ottenere è:
FFFFFFFFFFFFFFFFh * FFFFFFFFh = 00000000FFFFFFFEFFFFFFFF00000001h
In riferimento al nostro esempio, il massimo numero di shift verso sinistra viene applicato al secondo prodotto parziale ed è pari a 1 DWORD; nel nostro caso, il valore 00000000FFFFFFFEFFFFFFFF00000001h diventa FFFFFFFEFFFFFFFF0000000100000000h, senza trabocco da sinistra di cifre significative.

Tornando al nostro esempio, la somma tra i 2 prodotti parziali produce il seguente risultato: Per shiftare i vari prodotti parziali, possiamo anche fare a meno delle istruzioni di shifting (che verranno illustrate in un altro capitolo); infatti, osservando la struttura della somma illustrata in precedenza, possiamo scrivere il seguente codice: In sostanza, la DWORD meno significativa del risultato, coincide con la DWORD meno significativa di parziale1; la DWORD più significativa del risultato, coincide con la DWORD più significativa di parziale2 (sommata ad un eventuale carry precedente).

17.9.5 Effetti provocati da MUL sugli operandi e sui flags

In base alle considerazioni esposte in precedenza, risulta che l'esecuzione dell'istruzione MUL con operando a 8 bit, modifica il solo registro AX; l'esecuzione dell'istruzione MUL con operando a 16 bit, modifica i registri DX e AX. L'esecuzione dell'istruzione MUL con operando a 32 bit, modifica i registri EDX e EAX.

La possibilità di effettuare una moltiplicazione tra Reg e Reg, ci permette di scrivere istruzioni del tipo:
mul ax
Come si può facilmente constatare, questa istruzione calcola il quadrato di AX; se, ad esempio, AX=4850, l'istruzione precedente ci fornisce il risultato:
DX:AX = AX * AX = 4850 * 4850 = 23522500 = 48502
L'esecuzione dell'istruzione MUL provoca la modifica dei campi CF, PF, AF, ZF, SF e OF del Flags Register; i campi PF, AF, ZF e SF, assumono un valore indeterminato e non hanno quindi alcun significato.
I campi CF e OF assumono un significato particolare che non ha nulla a che fare con un carry o un overflow; infatti, il loro contenuto indica se, l'esecuzione di MUL con operandi a n bit, ha prodotto un risultato interamente rappresentabile con soli n bit. Se il risultato richiede solamente n bit, la CPU pone CF=0 e OF=0; se il risultato richiede 2n bit, la CPU pone CF=1 e OF=1.

Poniamo, ad esempio, AX=2390, BX=24, ed eseguiamo l'istruzione:
mul bx
In presenza di questa istruzione, la CPU calcola:
DX:AX = AX * BX = 2390 * 24 = 57360 = 0000E010h
Il valore 57360 è minore di 65536 ed è quindi interamente rappresentabile con soli 16 bit; si ottiene, infatti, DX=0000h e AX=E010h.
Per segnalare questa situazione, la CPU pone CF=0 e OF=0.

Poniamo, ad esempio, AX=4500, BX=175, ed eseguiamo l'istruzione:
mul bx
In presenza di questa istruzione, la CPU calcola:
DX:AX = AX * BX = 4500 * 175 = 787500 = 000C042Ch
Il valore 787500 è maggiore di 65535 e non è quindi interamente rappresentabile con soli 16 bit; si ottiene, infatti, DX=000Ch e AX=042Ch.
Per segnalare questa situazione, la CPU pone CF=1 e OF=1.

17.10 L'istruzione IMUL

Con il mnemonico IMUL si indica l'istruzione Signed Multiply (moltiplicazione tra numeri interi con segno); le uniche forme lecite per l'istruzione IMUL, sono le seguenti: Come si può notare, a differenza di quanto accade con MUL, l'istruzione IMUL prevede diverse combinazioni tra operando SRC e operando DEST; tali combinazioni, verranno analizzate in dettaglio più avanti.

Come abbiamo visto nel capitolo dedicato alla matematica del computer, moltiplicando tra loro due numeri interi con segno a n bit in complemento a 2, otteniamo un risultato che occupa, al massimo, 2n bit; inoltre, il prodotto che si ottiene con due fattori a n bit, è sempre compreso tra il valore minimo e il valore massimo, rappresentabili con 2n bit.
Considerando, ad esempio, numeri interi con segno a 8 bit in complemento a 2, il prodotto minimo che possiamo ottenere è:
(-128) * (+127) = -16256
Il valore -16256 è nettamente superiore al valore minimo (-32768) rappresentabile con 16 bit.
Analogamente, il prodotto massimo che possiamo ottenere è:
(-128) * (-128) = +16384
Il valore +16384 è nettamente inferiore al valore massimo (+32767) rappresentabile con 16 bit.

Come già sappiamo, la moltiplicazione provoca un cambiamento di modulo; di conseguenza, MUL e IMUL forniscono lo stesso risultato solo se i due fattori rappresentano numeri interi positivi, sia nell'insieme dei numeri interi senza segno, sia nell'insieme dei numeri interi con segno.

Se vogliamo conoscere le convenzioni seguite dalla CPU per la memorizzazione del risultato della moltiplicazione eseguita con IMUL, dobbiamo analizzare tutti i casi che si possono presentare; il risultato fornito da IMUL, è sempre rappresentato, ovviamente, in complemento a 2.

17.10.1 Istruzione IMUL con un solo operando esplicito

In questo caso, il programmatore specifica, esplicitamente, il solo operando SRC, che può essere di tipo Reg o Mem; è proibito l'uso di operandi di tipo Imm.
La situazione è del tutto simile a quella già esaminata per l'istruzione MUL; infatti, in base all'ampiezza in bit (8, 16 o 32) dell'operando SRC, la CPU decide se moltiplicare, rispettivamente, per AL, per AX o per EAX.
Anche le convenzioni seguite dalla CPU per la memorizzazione del risultato, sono le stesse già illustrate per MUL; possiamo dire quindi che, se l'operando è a 8 bit (SRC8):
AX = AL * SRC8
Se l'operando è a 16 bit (SRC16):
DX:AX = AX * SRC16
Se l'operando è a 32 bit (SRC32):
EDX:EAX = EAX * SRC32
Come al solito, nei tre casi possibili, la parte più significativa del risultato si trova, rispettivamente, in AH, in DX, in EDX.

17.10.2 Istruzione IMUL con due operandi espliciti

In questo caso, il programmatore specifica, esplicitamente, sia l'operando DEST (operando più a sinistra), sia l'operando SRC (operando più a destra); l'operando DEST può essere, esclusivamente, di tipo Reg16 o Reg32. Se l'operando DEST è di tipo Reg16, allora l'operando SRC può essere di tipo Reg16, Mem16 o Imm; se l'operando DEST è di tipo Reg32, allora l'operando SRC può essere di tipo Reg32, Mem32 o Imm. Un operando SRC di tipo Imm, viene eventualmente sottoposto all'estensione del bit di segno, sino a raggiungere l'ampiezza in bit di DEST.

In pratica, con questa forma dell'istruzione IMUL stiamo indicando alla CPU anche l'operando nel quale deve essere memorizzato il prodotto; di conseguenza, la CPU calcola:
DEST = DEST * SRC
Osserviamo, però, che l'operando DEST ha la stessa ampiezza in bit dell'operando SRC; ciò significa che questa forma dell'istruzione IMUL, moltiplica due operandi, SRC e DEST, entrambi a n bit e memorizza il risultato nell'operando DEST a n bit. Di conseguenza, può capitare che il risultato della moltiplicazione ecceda i limiti, minimo e massimo, rappresentabili con n bit; più avanti, vedremo come la CPU segnala questa situazione di errore.

17.10.3 Istruzione IMUL con tre operandi espliciti

In questo caso, il programmatore specifica, esplicitamente, l'operando DEST (operando più a sinistra) nel quale verrà memorizzato il prodotto, l'operando SRC1 (operando centrale) che rappresenta il primo fattore e l'operando SRC2 (operando più a destra) che rappresenta il secondo fattore; l'operando DEST può essere, esclusivamente, di tipo Reg16 o Reg32. L'operando SRC1 può essere di tipo Reg o Mem e deve avere la stessa ampiezza in bit di DEST; l'operando SRC2 deve essere, esclusivamente, di tipo Imm8 e viene quindi sottoposto all'estensione del bit di segno, sino a raggiungere l'ampiezza in bit di DEST.

In pratica, con questa forma dell'istruzione IMUL stiamo indicando alla CPU, l'operando che contiene il primo fattore (SRC1), l'operando che contiene il secondo fattore (SRC2) e anche l'operando nel quale deve essere memorizzato il prodotto tra i due fattori (DEST); di conseguenza, la CPU calcola:
DEST = SRC1 * SRC2
Osserviamo, però, che l'operando DEST ha la stessa ampiezza in bit dell'operando SRC1; ciò significa che questa forma dell'istruzione IMUL moltiplica due operandi, SRC1 e SRC2, entrambi a n bit e memorizza il risultato nell'operando DEST a n bit. Di conseguenza, può capitare che il risultato della moltiplicazione ecceda i limiti, minimo e massimo, rappresentabili con n bit; più avanti, vedremo come la CPU segnala questa situazione di errore.

17.10.4 Moltiplicazione tra numeri interi con segno di ampiezza arbitraria

Per moltiplicare numeri interi con segno di ampiezza arbitraria, si può sfruttare il fatto che il valore assoluto del prodotto è indipendente dal segno dei due fattori; ad esempio:
| 98 * 42 | = 4116
e:
| (-98) * (+42) | = 4116
Possiamo servirci allora dello stesso procedimento già illustrato per l'istruzione MUL; a tale proposito, possiamo suddividere l'algoritmo nelle seguenti fasi: In relazione alla fase n. 4, osserviamo che il segno del prodotto è dato dalle note regole seguenti: Se la moltiplicazione deve fornire un prodotto positivo, non dobbiamo apportare alcuna modifica al risultato ottenuto; se, invece, la moltiplicazione deve fornire un prodotto negativo, dobbiamo cambiare di segno (negare) il risultato ottenuto.
Dalle considerazioni appena esposte, risulta che, rispetto all'esempio mostrato per MUL, abbiamo bisogno anche di un algoritmo per la negazione dei numeri interi con segno di ampiezza arbitraria; quando avremo a disposizione tutti gli strumenti necessari, analizzeremo un esempio pratico.

17.10.5 Effetti provocati da IMUL sugli operandi e sui flags

L'esecuzione dell'istruzione IMUL con un solo operando esplicito a 8, 16 o 32 bit, provoca la modifica, rispettivamente, dei registri AX, DX:AX e EDX:EAX.
La possibilità di utilizzare IMUL con operando SRC di tipo Reg, ci permette di scrivere istruzioni del tipo:
imul eax
L'esecuzione di questa istruzione, memorizza in EDX:EAX il quadrato (sempre positivo) del numero intero con segno contenuto in EAX.

L'esecuzione dell'istruzione IMUL con due operandi espliciti, provoca la modifica del solo operando DEST, il cui contenuto viene sovrascritto dal risultato della moltiplicazione; il contenuto dell'operando SRC rimane inalterato.
Bisogna prestare attenzione ai casi del tipo:
imul bx, [bx]
Dopo l'esecuzione di questa istruzione, l'offset contenuto nel registro puntatore BX viene sovrascritto dal risultato della moltiplicazione.
La possibilità di utilizzare IMUL con operandi SRC e DEST di tipo Reg, ci permette di scrivere istruzioni del tipo:
imul cx, cx
L'esecuzione di questa istruzione, memorizza in CX (salvo overflow) il quadrato (sempre positivo) del numero intero con segno contenuto in CX.

L'esecuzione dell'istruzione IMUL con tre operandi espliciti, provoca la modifica del solo operando DEST, il cui contenuto viene sovrascritto dal risultato della moltiplicazione; il contenuto degli operandi SRC1 e SRC2 rimane inalterato.
Bisogna prestare attenzione ai casi del tipo:
imul bx, [bx+si], +35
Dopo l'esecuzione di questa istruzione, l'offset contenuto nel registro puntatore BX viene sovrascritto dal risultato della moltiplicazione.
Nel caso di istruzioni del tipo:
imul edx, edx, -18
si può notare che la modifica di DEST si ripercuote anche su SRC1.

Nel caso generale, l'esecuzione dell'istruzione IMUL provoca la modifica dei campi CF, PF, AF, ZF, SF e OF del Flags Register; i campi PF, AF, ZF e SF, assumono un valore indeterminato e non hanno quindi nessun significato.
I campi CF e OF assumono, invece, un significato analogo a quello già illustrato per l'istruzione MUL; questa volta, però, il programmatore deve prestare grande attenzione al fatto che la CPU pone CF=1 e OF=1 per segnalare una situazione di errore! Nel caso dell'istruzione IMUL con un solo operando esplicito, la situazione è del tutto simile a quella descritta per MUL; il risultato della moltiplicazione tra operandi a n bit è sempre esatto, in quanto viene memorizzato con una ampiezza di 2n bit.
I campi CF e OF indicano se, l'esecuzione di IMUL con operandi a n bit, ha prodotto un risultato interamente rappresentabile con soli n bit; se il risultato richiede solamente n bit, la CPU pone CF=0 e OF=0, mentre se il risultato richiede 2n bit, la CPU pone CF=1 e OF=1.

Poniamo, ad esempio, AX=-890, BX=24 ed eseguiamo l'istruzione:
imul bx
In presenza di questa istruzione, la CPU calcola:
DX:AX = AX * BX = -890 * 24 = -21360 = 11111111111111111010110010010000b
Trascurando i 16 bit più significativi del risultato, contenuti in DX, otteniamo:
AX = 1010110010010000b = 44176
Per i numeri interi con segno a 16 bit in complemento a 2, il valore 44176 rappresenta il numero negativo -21360; infatti:
44176 - 216 = -21360
Come si può notare, se trascuriamo il contenuto di DX, otteniamo in AX un risultato ugualmente valido; ciò accade in quanto il valore -21360 è maggiore di -32768 ed è quindi interamente rappresentabile con soli 16 bit.
In pratica, il numero negativo -21360 contenuto in DX:AX, può essere rappresentato con soli 16 bit senza il rischio di perdere bit significativi a sinistra; per segnalare questa situazione, la CPU pone CF=0 e OF=0.

Poniamo, ad esempio, AX=-4500, BX=175 ed eseguiamo l'istruzione:
imul bx
In presenza di questa istruzione, la CPU calcola:
DX:AX = AX * BX = -4500 * 175 = -787500 = 11111111111100111111101111010100b
Trascurando i 16 bit più significativi del risultato, contenuti in DX, otteniamo:
AX = 1111101111010100b = 64468
Per i numeri interi con segno a 16 bit in complemento a 2, il valore 64468 rappresenta il numero negativo -1068; infatti:
64468 - 216 = -1068
Come si può notare, se trascuriamo il contenuto di DX, otteniamo in AX un risultato privo di senso; ciò accade in quanto il valore -787500 è minore di -32768 e per la sua rappresentazione richiede quindi più di 16 bit.
In pratica, il numero negativo -787500 contenuto in DX:AX, non può essere rappresentato con soli 16 bit, perché si perderebbero bit significativi a sinistra; per segnalare questa situazione, la CPU pone CF=1 e OF=1. Anche nel caso dell'istruzione IMUL con due operandi espliciti, i due campi CF e OF indicano se, l'esecuzione di IMUL con operandi a n bit, ha prodotto un risultato interamente rappresentabile con soli n bit; questa volta, però, bisogna ricordare che l'istruzione IMUL memorizza il risultato negli n bit dell'operando DEST!
Se il risultato richiede solamente n bit, la CPU pone CF=0 e OF=0; in questo caso, l'operando DEST a n bit, contiene il risultato esatto della moltiplicazione.
Se il risultato richiede più di n bit, la CPU pone CF=1 e OF=1; in questo caso, l'operando DEST a n bit, contiene solamente gli n bit meno significativi del risultato. Tale risultato è quindi inutilizzabile in quanto ha subito un troncamento di cifre significative; in una situazione di questo genere, non ci resta che ricorrere all'istruzione IMUL con un solo operando esplicito (risultato esatto a 2n bit).

Poniamo, ad esempio, CX=-890, DX=24, ed eseguiamo l'istruzione:
imul cx, dx
In presenza di questa istruzione, la CPU calcola:
Temp32 = CX * DX = -890 * 24 = -21360 = 11111111111111111010110010010000b
La CPU legge i 16 bit meno significativi contenuti nel registro temporaneo Temp32 e li copia in CX; si ottiene quindi:
CX = 1010110010010000b = 44176
Per i numeri interi con segno a 16 bit in complemento a 2, il valore 44176 rappresenta il numero negativo -21360; infatti:
44176 - 216 = -21360
Come si può notare, il troncamento effettuato dalla CPU ha prodotto un risultato ugualmente valido; ciò accade in quanto il valore -21360 è maggiore di -32768 ed è quindi interamente rappresentabile con soli 16 bit.
In pratica, il numero negativo -21360, contenuto in Temp32, può essere rappresentato con soli 16 bit, in quanto il troncamento non provoca la perdita di bit significativi a sinistra; per segnalare questa situazione, la CPU pone CF=0 e OF=0.

Poniamo, ad esempio, CX=-4500, DX=175, ed eseguiamo l'istruzione:
imul cx, dx
In presenza di questa istruzione, la CPU calcola:
Temp32 = CX * DX = -4500 * 175 = -787500 = 11111111111100111111101111010100b
La CPU legge i 16 bit meno significativi contenuti nel registro temporaneo Temp32 e li copia in CX; si ottiene quindi:
CX = 1111101111010100b = 64468
Per i numeri interi con segno a 16 bit in complemento a 2, il valore 64468 rappresenta il numero negativo -1068; infatti:
64468 - 216 = -1068
Come si può notare, il troncamento effettuato dalla CPU ha prodotto un risultato privo di senso; ciò accade in quanto il valore -787500 è minore di -32768 e per la sua rappresentazione richiede quindi più di 16 bit.
In pratica, il numero negativo -787500, contenuto in Temp32, non può essere rappresentato con soli 16 bit, in quanto il troncamento provoca la perdita di bit significativi a sinistra; per segnalare questa situazione, la CPU pone CF=1 e OF=1. Il caso dell'istruzione IMUL con tre operandi espliciti, è assolutamente identico al precedente caso b; i campi CF e OF indicano se, l'esecuzione di IMUL con operandi a n bit, ha prodotto un risultato interamente rappresentabile con soli n bit.
Se il risultato richiede solamente n bit, la CPU pone CF=0 e OF=0; in questo caso, l'operando DEST a n bit, contiene il risultato esatto della moltiplicazione.
Se il risultato richiede più di n bit, la CPU pone CF=1 e OF=1; in questo caso, l'operando DEST a n bit, contiene solamente gli n bit meno significativi del risultato. Tale risultato è quindi inutilizzabile in quanto ha subito un troncamento di cifre significative; in una situazione di questo genere, non ci resta che ricorrere all'istruzione IMUL con un solo operando esplicito (risultato esatto a 2n bit).

17.11 L'istruzione DIV

Con il mnemonico DIV si indica l'istruzione Unsigned Divide (divisione di AL/AX/EAX per un numero intero senza segno); le uniche forme lecite per l'istruzione DIV sono le seguenti: Come si può notare, il programmatore deve specificare, esplicitamente, il solo operando SRC, che rappresenta il divisore; il dividendo è, implicitamente, AL, AX o EAX. A seconda dell'ampiezza in bit (8, 16 o 32) dell'operando SRC, la CPU decide se il dividendo deve essere, rispettivamente, AL, AX o EAX; è proibito l'uso di un operando SRC di tipo Imm.

Come abbiamo visto nel capitolo dedicato alla matematica del computer, dividendo tra loro due numeri interi senza segno da n bit ciascuno, otteniamo un risultato (quoziente) minore o uguale al dividendo; inoltre, il resto è sempre inferiore al divisore. Da queste considerazioni segue che la CPU, per memorizzare il quoziente e il resto della divisione, utilizza due registri a n bit.
La divisione si svolge nell'insieme dei numeri interi senza segno e produce quindi sempre un quoziente intero, con troncamento della eventuale parte frazionaria; di conseguenza, il resto è nullo solo quando il dividendo è un multiplo intero del divisore.

Nel capitolo dedicato alle reti combinatorie abbiamo anche visto che l'algoritmo per l'esecuzione di una divisione intera provoca un cambiamento di modulo; proprio per questo motivo, la CPU dispone di una istruzione che divide numeri interi senza segno (DIV) e di una istruzione che divide numeri interi con segno (IDIV).

Se vogliamo conoscere le convenzioni seguite dalla CPU per l'esecuzione della divisione con DIV, dobbiamo analizzare i tre casi fondamentali che si possono presentare; nel seguito del capitolo, indichiamo l'operatore divisione con il simbolo '/' (slash), mentre il simbolo ':' viene utilizzato, come al solito, per separare una coppia di valori disposti secondo la convenzione little endian (parte più significativa a sinistra e parte meno significativa a destra).

17.11.1 Divisione tra numeri interi senza segno a 8 bit

Vogliamo calcolare:
240 / 27 (Q = 8, R = 24)
Poniamo, AL=240, BH=27 ed eseguiamo l'istruzione:
div bh
In presenza di questa istruzione, la CPU utilizza implicitamente il registro AL e lo unisce al registro AH in modo che il dividendo sia rappresentato dalla coppia AH:AL; a questo punto, la CPU calcola:
AH:AL = AH:AL / BH = 240 / 27 = 24:8 = 18h:08h
Come si può notare, il quoziente a 8 bit viene memorizzato in AL, mentre il resto a 8 bit viene memorizzato in AH.

È importantissimo osservare che la CPU utilizza AH:AL come dividendo; di conseguenza, affinché DIV fornisca un risultato valido, è necessario che il programmatore estenda in AH il segno del dividendo stesso, contenuto in AL!
Ricordando che stiamo operando nell'insieme dei numeri interi senza segno, prima di eseguire DIV con operandi a 8 bit dobbiamo porre AH=0 (zero extension del valore contenuto in AL); se dimentichiamo di azzerare AH, l'istruzione DIV produce un risultato che, in certi casi (che verranno analizzati nel seguito), provoca l'interruzione del programma a causa di un overflow di divisione!

17.11.2 Divisione tra numeri interi senza segno a 16 bit

Vogliamo calcolare:
49750 / 832 (Q = 59, R = 662)
Poniamo, AX=49750 e supponiamo che ES:(BX+SI) punti in memoria ad una WORD che vale 832; eseguiamo ora l'istruzione:
div word ptr es:[bx+si]
L'operatore WORD PTR è necessario per indicare all'assembler che gli operandi sono a 16 bit; il segment override è necessario perché ES non è il registro di segmento naturale per i dati.
In presenza di questa istruzione, la CPU utilizza implicitamente il registro AX e lo unisce al registro DX in modo che il dividendo sia rappresentato dalla coppia DX:AX; a questo punto, la CPU calcola:
DX:AX = DX:AX / ES:[BX+SI] = 49750 / 832 = 662:59 = 0296h:003Bh
Come si può notare, il quoziente a 16 bit viene memorizzato in AX, mentre il resto a 16 bit viene memorizzato in DX.

È importantissimo osservare che la CPU utilizza DX:AX come dividendo; di conseguenza, affinché DIV fornisca un risultato valido, è necessario che il programmatore estenda in DX il segno del dividendo stesso, contenuto in AX!
Ricordando che stiamo operando nell'insieme dei numeri interi senza segno, prima di eseguire DIV con operandi a 16 bit dobbiamo porre DX=0 (zero extension del valore contenuto in AX); se dimentichiamo di azzerare DX, l'istruzione DIV produce un risultato che, in certi casi (che verranno analizzati nel seguito), provoca l'interruzione del programma a causa di un overflow di divisione!

17.11.3 Divisione tra numeri interi senza segno a 32 bit

Vogliamo calcolare:
2997860228 / 384000 (Q = 7806, R = 356228)
Poniamo, EAX=2997860228, ECX=384000 ed eseguiamo l'istruzione:
div ecx
In presenza di questa istruzione, la CPU utilizza implicitamente il registro EAX e lo unisce al registro EDX in modo che il dividendo sia rappresentato dalla coppia EDX:EAX; a questo punto, la CPU calcola:
EDX:EAX = EDX:EAX / ECX = 2997860228 / 384000 = 356228:7806 = 00056F84h:00001E7Eh
Come si può notare, il quoziente a 32 bit viene memorizzato in EAX, mentre il resto a 32 bit viene memorizzato in EDX.

È importantissimo osservare che la CPU utilizza EDX:EAX come dividendo; di conseguenza, affinché DIV fornisca un risultato valido, è necessario che il programmatore estenda in EDX il segno del dividendo stesso, contenuto in EAX!
Ricordando che stiamo operando nell'insieme dei numeri interi senza segno, prima di eseguire DIV con operandi a 32 bit dobbiamo porre EDX=0 (zero extension del valore contenuto in EAX); se dimentichiamo di azzerare EDX, l'istruzione DIV produce un risultato che, in certi casi (che verranno analizzati nel seguito), provoca l'interruzione del programma a causa di un overflow di divisione!

17.11.4 Divisione tra numeri interi senza segno di ampiezza arbitraria

Come abbiamo appena visto, nell'eseguire l'istruzione DIV con operandi a 8, 16 o 32 bit, la CPU presuppone che il dividendo si trovi, rispettivamente, in AH:AL, in DX:AX o in EDX:EAX; come mai la CPU richiede che il dividendo venga rappresentato con il doppio dei bit necessari?
Il perché di questo comportamento è legato ad una geniale trovata dei progettisti delle CPU; grazie a questa trovata, è possibile eseguire con relativa facilità, divisioni tra numeri interi di ampiezza arbitraria.
Vediamo subito un esempio pratico che chiarisce questo importante aspetto.

Per dividere numeri interi senza segno di ampiezza arbitraria, possiamo servirci di un algoritmo del tutto simile a quello che si utilizza con carta e penna; per analizzare tale algoritmo, supponiamo di voler calcolare:
9AB6h / Ch (Q = 0CE4h, R = 0006h)
Come sappiamo, la divisione inizia con la cifra più significativa del dividendo; nel nostro caso quindi, il primo quoziente parziale da calcolare è:
9h / Ch  (Q3 = 0h, R3 = 9h)
Il quoziente parziale indicato con Q3 rappresenta la cifra più significativa del quoziente finale; al resto parziale R3 dobbiamo affiancare la seconda cifra (da sinistra) del dividendo, in modo da ottenere 9Ah.

Il secondo quoziente parziale da calcolare è:
9Ah / Ch (Q2 = Ch, R2 = Ah)
Il quoziente parziale indicato con Q2 rappresenta la seconda cifra (da sinistra) del quoziente finale; al resto parziale R2 dobbiamo affiancare la terza cifra (da sinistra) del dividendo, in modo da ottenere ABh.

Il terzo quoziente parziale da calcolare è:
ABh / Ch (Q1 = Eh, R1 = 3h)
Il quoziente parziale indicato con Q1 rappresenta la terza cifra (da sinistra) del quoziente finale; al resto parziale R1 dobbiamo affiancare la quarta cifra (da sinistra) del dividendo, in modo da ottenere 36h.

Il quarto e ultimo quoziente parziale da calcolare è:
36h / Ch (Q0 = 4h, R0 = 6h)
Il quoziente parziale indicato con Q0 rappresenta la cifra meno significativa del quoziente finale; il resto parziale R0 rappresenta il resto finale della divisione.

Unendo i 4 quozienti parziali otteniamo il quoziente finale 0CE4h; inoltre, abbiamo appena visto che l'ultimo resto parziale R0, ci fornisce il resto finale 6h.

Come si può notare, la prima divisione consiste nel dividere un numero a 1 cifra esadecimale, per un numero a 1 cifra esadecimale; una tale divisione produce sempre un quoziente a 1 cifra esadecimale e, ovviamente, un resto a 1 cifra esadecimale.
A partire dalla seconda divisione, dobbiamo dividere un numero a 2 cifre esadecimali, per un numero a 1 cifra esadecimale; una tale divisione può produrre un quoziente a 2 cifre esadecimali?
La risposta è no!
Per dimostrarlo, osserviamo innanzi tutto che il divisore è formato da 1 sola cifra esadecimale, per cui è sempre compreso tra 1h e Fh; di conseguenza, anche il resto, dovendo essere minore del divisore, è formato sempre da 1 sola cifra esadecimale.
Osserviamo ora che affiancando al resto Ri una qualunque cifra Mj del dividendo, otteniamo un numero a due cifre esadecimali, rappresentato da:
RiMj = (Ri * 10h) + Mj
(con la cifra Mj che è sempre compresa tra 0h e Fh).

Ad esempio, se Ri=3h e Mj=Bh, si ha:
RiMj = 3Bh = (3h * 10h) + Bh
A questo punto, possiamo sottoporre RiMj alle seguenti elaborazioni: In sostanza, RiMj è sicuramente minore o uguale a ((Ri + 1) * Fh) + Ri.
Per calcolare un nuovo quoziente parziale, dobbiamo dividere RiMj per il divisore, che indichiamo con N; di conseguenza, possiamo affermare che: Assegnando ora a N un qualunque valore compreso tra 1h e Fh, possiamo constatare che il secondo membro della precedente disequazione è sempre minore o uguale a Fh!
Ad esempio, se N=1h, allora Ri non può superare 0h; di conseguenza, il quoziente parziale massimo che possiamo ottenere è:
(((Ri + 1h) * Fh) + Ri) / N = (((0h + 1h) * Fh) + 0h) / 1h = Fh / 1h = Fh
(con resto max. 0h).

Se N=2h, allora Ri non può superare 1h; di conseguenza, il quoziente parziale massimo che possiamo ottenere è:
(((Ri + 1h) * Fh) + Ri) / N = (((1h + 1h) * Fh) + 1h) / 2h = 1Fh / 2h = Fh
(con resto max. 1h).

Se N=3h, allora Ri non può superare 2h; di conseguenza, il quoziente parziale massimo che possiamo ottenere è:
(((Ri + 1h) * Fh) + Ri) / N = (((2h + 1h) * Fh) + 2h) / 3h = 2Fh / 3h = Fh
(con resto max. 2h).

E così via, sino ad arrivare a N=Fh, con Ri che non può quindi superare Eh; di conseguenza, il quoziente parziale massimo che possiamo ottenere è:
(((Ri + 1h) * Fh) + Ri) / N = (((Eh + 1h) * Fh) + Eh) / Fh = EFh / Fh = Fh
(con resto max. Eh).

Risulta quindi che, a maggior ragione, il generico quoziente parziale (RiMj / N), non può mai superare il valore Fh!

È importante ribadire che, nel calcolo del primo quoziente parziale, il dividendo deve essere formato da 1 sola cifra esadecimale; in caso contrario, può succedere che, ad esempio:
EEh / 4h (Q = 3Bh, R = 2h)
In generale, se otteniamo un quoziente parziale maggiore di Fh, vuol dire che abbiamo commesso qualche errore nello svolgimento dei calcoli; in particolare, se RiMj è maggiore di zero e il divisore (N) vale zero, si ottiene un quoziente parziale infinitamente grande (divisione per zero)!

Passiamo ora alla implementazione pratica dell'algoritmo per la divisione tra numeri interi senza segno di ampiezza arbitraria; supponendo di avere a disposizione una CPU 80386 o superiore, ci serviremo dell'istruzione DIV con operandi di tipo DWORD.
In tal caso, ogni DWORD equivale ad una delle cifre esadecimali dell'esempio che abbiamo svolto in precedenza; in particolare, osserviamo che nell'eseguire l'istruzione DIV, la CPU si serve, implicitamente, della coppia di registri EDX:EAX e ci restituisce i vari quozienti parziali in EAX e i vari resti parziali in EDX.
Nel calcolare il primo quoziente parziale, dobbiamo caricare in EAX la DWORD più significativa del dividendo e dobbiamo porre, come sappiamo, EDX=0 (questo passo è fondamentale); dopo l'esecuzione dell'istruzione DIV (tra EDX:EAX e il divisore) otteniamo, in EAX la DWORD più significativa del quoziente finale, e in EDX il primo resto parziale.
La DWORD contenuta in EAX deve essere salvata in memoria, in quanto, nello stesso registro EAX, dobbiamo caricare la seconda DWORD (da sinistra) del dividendo; grazie all'espediente escogitato dai progettisti delle CPU, questa nuova DWORD caricata in EAX, va quindi ad affiancarsi al contenuto del registro EDX, che rappresenta proprio il valore di cui avevamo bisogno e cioè, il resto parziale della precedente divisione!
La fase successiva consiste quindi nel dividere EDX:EAX per il divisore; in base a quanto abbiamo visto nel precedente esempio, una tale divisione produce sempre un quoziente parziale non superiore a FFFFFFFFh. Per rendercene conto, non dobbiamo fare altro che ripetere la precedente dimostrazione; a tale proposito, dobbiamo tenere presente che, questa volta, il divisore non può superare FFFFFFFFh, il resto è sempre inferiore al divisore e inoltre:
RiMj = (Ri * 100000000h) + Mj
Sempre in analogia con l'esempio svolto in precedenza, possiamo dire che l'ultimo resto parziale che otteniamo, rappresenta anche il resto finale della divisione.

A questo punto possiamo procedere con un esempio pratico; a tale proposito, supponiamo di voler calcolare:
8CB1AF203DA7B9867B04FFD2h / 35D6CE04h (Q = 000000029CFCDB3A3CEA5589h, R = 19016BAEh)
Dividendo un numero a 96 bit per un numero a 32 bit, si ottiene un quoziente che richiede, al massimo, 96 bit e un resto che richiede, al massimo, 32 bit; nel blocco dati del nostro programma, possiamo inserire allora le seguenti definizioni: Il codice che esegue la divisione, assume il seguente aspetto: Se ora vogliamo visualizzare il risultato con writeHex32, possiamo utilizzare lo stesso metodo illustrato per l'istruzione ADC.

L'esempio appena svolto è molto semplice, grazie al fatto che il divisore è formato da una sola DWORD, direttamente gestibile dalla CPU; cosa succede se anche il divisore è formato da due o più DWORD?
In un caso del genere, la situazione diventa più impegnativa; una soluzione può essere quella di simulare il comportamento della rete logica illustrata nella Figura 6.21 del Capitolo 6. Tale soluzione comporta l'uso di un algoritmo che occupa parecchio spazio e necessita anche del procedimento per lo shifting di un numero intero di ampiezza arbitraria; quando avremo a disposizione tutti gli strumenti necessari (istruzioni per i loop e per lo shifting, procedure, etc), analizzeremo un esempio pratico.

17.11.5 Effetti provocati da DIV sugli operandi e sui flags

In base alle considerazioni esposte in precedenza, risulta che l'esecuzione dell'istruzione DIV con operando a 8 bit, modifica i registri AH e AL; l'esecuzione dell'istruzione DIV con operando a 16 bit, modifica i registri DX e AX. L'esecuzione dell'istruzione DIV con operando a 32 bit, modifica i registri EDX e EAX.

La possibilità di effettuare una divisione tra Reg e Reg, ci permette di scrivere istruzioni del tipo:
div ax
Come si può facilmente constatare, l'esecuzione di questa istruzione produce DX=0 e AX=1; naturalmente, è importante che prima dell'esecuzione di DIV, si ponga DX=0 (inoltre, AX deve essere diverso da zero).

L'esecuzione dell'istruzione DIV provoca la modifica dei campi CF, PF, AF, ZF, SF e OF del Flags Register; tutti questi 6 campi, assumono un valore indeterminato e non hanno quindi alcun significato.
Ci si può chiedere allora come faccia la CPU a segnalare eventuali situazioni di errore; per chiarire questo aspetto, dobbiamo analizzare in dettaglio i casi che possono portare l'istruzione DIV a provocare un overflow di divisione. Affinché l'istruzione DIV non provochi un overflow di divisione, devono verificarsi le seguenti condizioni: Se la CPU si accorge che queste condizioni non sono verificate, assume che si sia creata una situazione di errore; in tal caso la CPU, anziché servirsi di un apposito flag, genera (in modalità reale) una INT 00h. Come è stato spiegato nel Capitolo 14, il vettore di interruzione n. 00h è associato ad una ISR che segnala un cosiddetto overflow di divisione; generalmente, tale ISR termina il programma in esecuzione e mostra un messaggio del tipo:
Overflow di divisione
Come molti avranno intuito, una situazione del genere si verifica, ad esempio, quando, prima di eseguire DIV con operandi a 16 bit, dimentichiamo di azzerare DX; per dimostrarlo, supponiamo che in DX sia presente il valore casuale 842 (034Ah).
Poniamo ora AX=46528 (B5C0h), BX=26 ed eseguiamo l'istruzione:
div bx
In presenza di questa istruzione, la CPU divide DX:AX per BX; a causa, però, della nostra dimenticanza, la coppia DX:AX contiene il valore:
DX:AX = 034AB5C0h = 55227840
Di conseguenza, la divisione dovrebbe fornire il risultato:
55227840 / 26 (Q = 2124147, R = 18)
È evidente che il valore 2124147 richiede più di 16 bit e non può essere quindi inserito in AX; in un caso del genere, la CPU rileva la situazione di errore e genera una INT 00h.

Supponiamo, invece, di avere DX=842 (034Ah), AX=46528 (B5C0h), BX=4126 ed eseguiamo l'istruzione:
div bx
In presenza di questa istruzione, la CPU divide DX:AX per BX; la coppia DX:AX contiene il valore:
DX:AX = 034AB5C0h = 55227840
Di conseguenza, la divisione dovrebbe fornire il risultato:
55227840 / 4126 (Q = 13385, R = 1330)
Questa volta, il valore 13385 può essere inserito nei 16 bit di AX; in un caso del genere, la CPU non segnala alcun errore!
Se la nostra intenzione era quella di dividere 55227840 per 4126, otteniamo un quoziente e un resto perfettamente validi; se, però, la nostra intenzione era quella di dividere 46528 per 4126, otteniamo un risultato privo di senso! Come sappiamo dalla matematica, dividendo per zero un numero non nullo, otteniamo un quoziente infinitamente grande; tale quoziente, non può essere quindi inserito in alcun registro della CPU.
Anche in questo caso, ci troviamo in una situazione di errore che viene gestita dalla CPU attraverso la generazione di una INT 00h.

17.12 L'istruzione IDIV

Con il mnemonico IDIV si indica l'istruzione Signed Divide (divisione di AL/AX/EAX per un numero intero con segno); le uniche forme lecite per l'istruzione IDIV, sono le seguenti: Come si può notare, il programmatore deve specificare, esplicitamente, il solo operando SRC, che rappresenta il divisore; il dividendo è, implicitamente, AL, AX o EAX. A seconda dell'ampiezza in bit (8, 16 o 32) dell'operando SRC, la CPU decide se il dividendo deve essere, rispettivamente, AL, AX o EAX; è proibito l'uso di un operando SRC di tipo Imm.

Dalla matematica sappiamo che, dividendo tra loro due numeri interi con segno da n bit ciascuno, otteniamo un resto che, in valore assoluto, è sempre minore del valore assoluto del divisore; di conseguenza, il resto è sempre rappresentabile con soli n bit, senza il rischio di perdere cifre significative.
Per il quoziente, la situazione appare più delicata; nel caso, ad esempio, dei numeri interi con segno a 8 bit in complemento a 2, la CPU si aspetta un quoziente compreso tra -128 e +127, rappresentabile quindi con soli 8 bit.
Osserviamo però che, il quoziente minimo che possiamo ottenere è:
(-128) / (+1) = -128
Questo valore può essere rappresentato con soli 8 bit.
Il quoziente massimo che possiamo ottenere è:
(-128) / (-1) = +128
Questo valore non può essere rappresentato con soli 8 bit!

Ogni volta che la CPU ottiene un quoziente che non rientra tra i limiti minimo e massimo permessi, genera un segnale di errore attraverso i metodi già illustrati per l'istruzione DIV!

Come già sappiamo, la divisione provoca un cambiamento di modulo; di conseguenza, DIV e IDIV forniscono lo stesso risultato solo se il dividendo e il divisore rappresentano numeri interi positivi, sia nell'insieme dei numeri interi senza segno, sia nell'insieme dei numeri interi con segno.

Se vogliamo conoscere le convenzioni seguite dalla CPU per l'esecuzione della divisione con IDIV, dobbiamo analizzare i tre casi fondamentali che si possono presentare; il quoziente e il resto calcolati da IDIV, sono sempre rappresentati, ovviamente, in complemento a 2.

17.12.1 Divisione tra numeri interi con segno a 8 bit

Vogliamo calcolare:
(-115) / (+18) (Q = -6, R = -7)
Poniamo, AL=-115, CL=+18, ed eseguiamo l'istruzione:
idiv cl
In presenza di questa istruzione, la CPU utilizza implicitamente il registro AL e lo unisce al registro AH in modo che il dividendo sia rappresentato dalla coppia AH:AL; a questo punto, la CPU calcola:
AH:AL = AH:AL / CL = (-115) / (+18) = (-7):(-6) = F9h:FAh
Come si può notare, il quoziente a 8 bit viene memorizzato in AL, mentre il resto a 8 bit viene memorizzato in AH.

È importantissimo osservare che la CPU utilizza AH:AL come dividendo; di conseguenza, affinché IDIV fornisca un risultato valido, è necessario che il programmatore estenda in AH il segno del dividendo stesso, contenuto in AL!
Ricordando che stiamo operando nell'insieme dei numeri interi con segno, prima di eseguire IDIV con operandi a 8 bit dobbiamo caricare il dividendo in AL ed eseguire poi l'istruzione:
cbw
(sign extension del valore contenuto in AL).

Come si può facilmente intuire, non è certo casuale il fatto che CBW utilizzi AL come operando SRC e AH:AL come operando DEST.
Se dimentichiamo di estendere in AH il segno del valore contenuto in AL, l'istruzione IDIV produce un risultato privo di senso che, in certi casi (che verranno analizzati nel seguito), provoca l'interruzione del programma a causa di un overflow di divisione!

17.12.2 Divisione tra numeri interi con segno a 16 bit

Vogliamo calcolare:
(-16924) / (-696) (Q = +24, R = -220)
Poniamo, AX=-16924 e supponiamo che CS:(BX+DI+002Fh) punti in memoria ad una WORD che vale -696; eseguiamo ora l'istruzione:
idiv word ptr cs:[bx+di+002Fh]
L'operatore WORD PTR è necessario per indicare all'assembler che gli operandi sono a 16 bit; il segment override è necessario perché CS non è il registro di segmento naturale per i dati.
In presenza di questa istruzione, la CPU utilizza implicitamente il registro AX e lo unisce al registro DX in modo che il dividendo sia rappresentato dalla coppia DX:AX; a questo punto, la CPU calcola:
DX:AX = DX:AX / CS:[BX+DI+002Fh] = (-16924) / (-696) = (-220):(+24) = FF24h:0018h
Come si può notare, il quoziente a 16 bit viene memorizzato in AX, mentre il resto a 16 bit viene memorizzato in DX.

È importantissimo osservare che la CPU utilizza DX:AX come dividendo; di conseguenza, affinché IDIV fornisca un risultato valido, è necessario che il programmatore estenda in DX il segno del dividendo stesso, contenuto in AX!
Ricordando che stiamo operando nell'insieme dei numeri interi con segno, prima di eseguire IDIV con operandi a 16 bit dobbiamo caricare il dividendo in AX ed eseguire poi l'istruzione:
cwd
(sign extension del valore contenuto in AX).

Come si può facilmente intuire, non è certo casuale il fatto che CWD utilizzi AX come operando SRC e DX:AX come operando DEST.
Se dimentichiamo di estendere in DX il segno del valore contenuto in AX, l'istruzione IDIV produce un risultato privo di senso che, in certi casi (che verranno analizzati nel seguito), provoca l'interruzione del programma a causa di un overflow di divisione!

17.12.3 Divisione tra numeri interi con segno a 32 bit

Vogliamo calcolare:
(+2115967835) / (+89736) (Q = +23579, R = +82691)
Poniamo, EAX=+2115967835, ECX=+89736, ed eseguiamo l'istruzione:
idiv ecx
In presenza di questa istruzione, la CPU utilizza implicitamente il registro EAX e lo unisce al registro EDX in modo che il dividendo sia rappresentato dalla coppia EDX:EAX; a questo punto, la CPU calcola:
EDX:EAX = EDX:EAX / ECX = (+2115967835) / (+89736) = (+82691):(+23579) = 00014303h:00005C1Bh
Come si può notare, il quoziente a 32 bit viene memorizzato in EAX, mentre il resto a 32 bit viene memorizzato in EDX.

È importantissimo osservare che la CPU utilizza EDX:EAX come dividendo; di conseguenza, affinché IDIV fornisca un risultato valido, è necessario che il programmatore estenda in EDX il segno del dividendo stesso, contenuto in EAX!
Ricordando che stiamo operando nell'insieme dei numeri interi con segno, prima di eseguire IDIV con operandi a 32 bit dobbiamo caricare il dividendo in EAX, ed eseguire poi l'istruzione:
cdq
(sign extension del valore contenuto in EAX).

Come si può facilmente intuire, non è certo casuale il fatto che CDQ utilizzi EAX come operando SRC e EDX:EAX come operando DEST.
Se dimentichiamo di estendere in EDX il segno del valore contenuto in EAX, l'istruzione IDIV produce un risultato privo di senso che, in certi casi (che verranno analizzati nel seguito), provoca l'interruzione del programma a causa di un overflow di divisione!

17.12.4 Divisione tra numeri interi con segno di ampiezza arbitraria

Per dividere numeri interi con segno di ampiezza arbitraria, si può sfruttare il fatto che, i valori assoluti del quoziente e del resto, sono indipendenti dal segno del dividendo e del divisore; ad esempio:
| 98 / 19 | (Q = 5, R = 3)
e:
| (-98) / (+19) | (Q = +5, R = +3)
Possiamo servirci allora dello stesso procedimento già illustrato per l'istruzione DIV; a tale proposito, possiamo suddividere l'algoritmo nelle seguenti fasi: In relazione alla fase n. 4, osserviamo che i segni del quoziente e del resto sono dati dalle note regole seguenti: Se il quoziente e il resto devono essere positivi, non dobbiamo apportare alcuna modifica ai risultati ottenuti; se, invece, il quoziente e/o il resto devono essere negativi, dobbiamo eseguire gli opportuni cambiamenti di segno (negazioni).
Dalle considerazioni appena esposte, risulta che, rispetto all'esempio mostrato per DIV, abbiamo bisogno anche di un algoritmo per la negazione dei numeri interi con segno di ampiezza arbitraria; quando avremo a disposizione tutti gli strumenti necessari, analizzeremo un esempio pratico.

17.12.5 Effetti provocati da IDIV sugli operandi e sui flags

In base alle considerazioni esposte in precedenza, risulta che l'esecuzione dell'istruzione IDIV con operando a 8 bit, modifica i registri AH e AL; l'esecuzione dell'istruzione IDIV con operando a 16 bit, modifica i registri DX e AX. L'esecuzione dell'istruzione IDIV con operando a 32 bit, modifica i registri EDX e EAX.

La possibilità di effettuare una divisione tra Reg e Reg, ci permette di scrivere istruzioni del tipo:
idiv ax
Come si può facilmente constatare, l'esecuzione di questa istruzione produce DX=0 e AX=1; naturalmente, è importante che prima dell'esecuzione di IDIV, si esegua l'istruzione CWD (inoltre, AX deve essere diverso da zero).

L'esecuzione dell'istruzione IDIV provoca la modifica dei campi CF, PF, AF, ZF, SF e OF del Flags Register; tutti questi 6 campi, assumono un valore indeterminato e non hanno quindi alcun significato.
Per segnalare eventuali situazioni di errore, la CPU utilizza gli stessi metodi già illustrati per l'istruzione DIV; ovviamente, affinché non si verifichi un overflow di divisione, devono verificarsi le seguenti condizioni: