Assembly Base con NASM
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:
%assign PRESSIONE 450000
e con ES:DI che punta a VarDword, possiamo scrivere la
seguente istruzione:
add dword [es:di], PRESSIONE
L'operatore DWORD è 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:
%assign 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:
%assign 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:
- se Temp è uguale a zero, allora DEST è uguale a
SRC
- se Temp è maggiore di zero, allora DEST è maggiore
di SRC
- se Temp è minore zero, allora DEST è minore di
SRC
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 dd C839ABF3h, 93B148F6h ; 93B148F6C839ABF3h
Prima di tutto, creiamo una locazione di memoria da 128 bit con la
seguente definizione:
BigNum128 resd 4 ; 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 [es:di]
L'operatore WORD è 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:
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:
- le cifre del primo prodotto devono essere shiftate di 0 posti
verso sinistra
- le cifre del secondo prodotto devono essere shiftate di 1 posto
verso sinistra
- le cifre del terzo prodotto devono essere shiftate di 2 posti
verso sinistra
- le cifre del quarto prodotto devono essere shiftate di 3 posti
verso sinistra
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 [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 [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:
- le cifre del primo prodotto devono essere shiftate di 0 DWORD
verso sinistra
- le cifre del secondo prodotto devono essere shiftate di 1 DWORD
verso sinistra
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:
- memorizzazione del segno dei due fattori
- cambiamento di segno (negazione) di eventuali fattori negativi
- esecuzione della moltiplicazione tra numeri interi senza segno
(con MUL)
- adeguamento del risultato in base al segno
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!
- a) istruzione IMUL con un solo operando esplicito
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.
- b) istruzione IMUL con due operandi espliciti
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.
- c) istruzione IMUL con tre operandi espliciti
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 [es:bx+si]
L'operatore WORD è 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.
- a) dividendo troppo grande
Affinché l'istruzione DIV non provochi un overflow di divisione,
devono verificarsi le seguenti condizioni:
- nella divisione tra numeri interi senza segno a 8 bit, si deve
ottenere un quoziente compreso tra 0 e 255
- nella divisione tra numeri interi senza segno a 16 bit, si deve
ottenere un quoziente compreso tra 0 e 65535
- nella divisione tra numeri interi senza segno a 32 bit, si deve
ottenere un quoziente compreso tra 0 e 4294967295
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!
- b) divisore uguale a zero
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 [cs:bx+di+002Fh]
L'operatore WORD è 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:
- memorizzazione del segno dei due operandi
- cambiamento di segno (negazione) di eventuali operandi negativi
- esecuzione della divisione tra numeri interi senza segno (con
DIV)
- adeguamento del risultato in base al segno
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:
- nella divisione tra numeri interi con segno a 8 bit in complemento
a 2, si deve ottenere un quoziente compreso tra -128 e
+127
- nella divisione tra numeri interi con segno a 16 bit in complemento
a 2, si deve ottenere un quoziente compreso tra -32768 e
+32767
- nella divisione tra numeri interi con segno a 32 bit in complemento
a 2, si deve ottenere un quoziente compreso tra -2147483648 e
+2147483647