Assembly Base con MASM
Capitolo 3: La matematica del computer
Il concetto fondamentale emerso nel precedente capitolo, è che il computer gestisce
qualunque tipo di informazione sotto forma di codici numerici; di conseguenza, elaborare
queste informazioni significa elaborare numeri, cioè eseguire varie operazioni matematiche
su questi numeri. Lo scopo di questo capitolo è proprio quello di analizzare in dettaglio
tutti gli aspetti relativi a quella che può essere definita la matematica del
computer.
Abbiamo visto che la rappresentazione numerica più adatta per una macchina come il computer
è il sistema posizionale arabo e abbiamo anche visto che con questo sistema non esistono
limiti teorici alla dimensione del numero che si vuole rappresentare; il problema è che
mentre noi esseri umani abbiamo la capacità di immaginare numeri enormi, il computer non può
operare su numeri frutto dell'immaginazione ma li deve gestire fisicamente in apposite aree
interne che possiamo paragonare a dei contenitori. Le dimensioni di questi contenitori sono
limitate e di conseguenza anche la massima dimensione dei numeri che devono contenere sarà
limitata.
Il problema che si presenta sul computer viene efficacemente rappresentato dall'esempio
del contachilometri delle automobili. Supponiamo di comprare un'auto nuova dotata di
contachilometri a 5 cifre che inizialmente segna 00000; durante i viaggi il
contachilometri si incrementa di una unità ad ogni chilometro sino ad arrivare a
99999. A questo punto, se percorriamo un altro chilometro, il contachilometri,
avendo solo 5 cifre, non segnerà 100000 ma 00000!
Questo è esattamente ciò che accade sul computer quando, ad esempio, si sommano due grossi
numeri ottenendo un risultato che eccede la dimensione massima permessa; in questo caso
il computer ci darà un risultato imprevisto. Questo problema sembra estremamente grave e
tale da rendere i computer (in assenza di soluzioni) assolutamente inutilizzabili; come
però vedremo in seguito, per fortuna le soluzioni esistono. Bisogna sottolineare che
l'esempio del contachilometri ha una importanza straordinaria in quanto ci permetterà
anche in seguito di capire perfettamente il modo di operare del computer; per studiare la
matematica del computer ci serviremo inizialmente di un computer "immaginario" che conta
in base 10.
3.1 Un computer immaginario che lavora in base 10
Cominciamo allora con lo stabilire che il nostro computer "immaginario" lavori con il sistema
di numerazione posizionale arabo in base b=10 e sia in grado di gestire numeri a
5 cifre; con 5 cifre possiamo rappresentare tutti i numeri interi compresi
tra 0 e 99999, per un totale di 100000 numeri differenti
(105). A proposito di numero di cifre, spesso si sente parlare di computer
con architettura a 16 bit, 32 bit, 64 bit etc; questi valori
rappresentano in pratica il numero massimo di cifre che l'hardware del computer può
gestire direttamente. Possiamo dire allora che il nostro computer immaginario avrà
una architettura a 5 cifre per la rappresentazione di numeri in base 10; i
100000 numeri ottenibili con queste 5 cifre verranno utilizzati per la
codifica numerica delle informazioni da gestire attraverso il computer.
Un altro aspetto importante da chiarire è: che tipo di operazioni matematiche può eseguire
il computer su questi numeri?
Sarebbe piuttosto complicato, se non impossibile, realizzare un computer capace di calcolare
direttamente funzioni trigonometriche, logaritmiche, esponenziali, etc; per fortuna la
matematica ci viene incontro dimostrandoci che è possibile approssimare in modo più o meno
semplice qualunque (o quasi) funzione con un polinomio di grado prestabilito contenente
quindi solamente le quattro operazioni fondamentali e cioè: addizione, sottrazione,
moltiplicazione e divisione. Questo aspetto è di grandissima importanza per il computer in
quanto permette di ottenere enormi semplificazioni circuitali nel momento in cui si devono
implementare via hardware questi operatori matematici; in seguito vedremo addirittura che
il computer in realtà è in grado di svolgere il proprio lavoro servendosi esclusivamente di
addizioni, scorrimenti di cifre, cambiamenti di segno e pochi altri semplicissimi
operatori.
A questo punto ci interessa vedere come si applicano le quattro operazioni fondamentali ai
codici a 5 cifre del nostro computer ricordando che i circuiti elettronici preposti
ad eseguire le operazioni si trovano come sappiamo nella CPU.
3.2 Numeri interi senza segno
In matematica l'insieme numerico rappresentato dalla sequenza:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...
viene chiamato insieme dei numeri naturali e viene indicato con il simbolo
ℕ; si tratta in sostanza dell'insieme dei numeri interi strettamente positivi per
i quali è implicito il segno + davanti al numero stesso (per questo motivo si
parla anche di numeri interi senza segno). All'insieme ℕ viene in genere aggiunto
anche lo zero che convenzionalmente è privo di segno in quanto rappresenta una quantità
nulla; da questo momento in poi indicheremo con ℕ l'insieme dei numeri naturali
più lo zero. Vediamo allora come bisogna procedere per codificare l'insieme ℕ sul
nostro computer; è chiaro che la CPU sarà in grado di rappresentare solo un
sottoinsieme finito degli infiniti numeri appartenenti a ℕ.
L'implementazione dell'insieme ℕ è piuttosto semplice ed intuitiva; il
nostro computer, infatti, ha una architettura a 5 cifre e con 5 cifre
possiamo codificare tutti i numeri interi senza segno compresi tra 0 e
99999. Si ottiene quindi la codifica rappresentata in Figura 3.1:
In Figura 3.1 tutti i numeri che terminano con una d rappresentano un codice
numerico del computer; nel nostro caso si tratta di codici numerici espressi da numeri
interi in base 10, formati da 5 cifre ciascuno. In questo modo possiamo
evitare di fare confusione tra una determinata informazione e la corrispondente codifica
numerica usata dal computer; da questo momento in poi verrà sempre utilizzato questo
accorgimento.
La Figura 3.1 ci mostra quindi che il computer rappresenta il numero 0 attraverso il
codice 00000d, il numero 1 attraverso il codice 00001d, il numero
2 attraverso il codice 00002d e così via; attraverso questo sistema di
codifica, la nostra CPU può gestire via hardware un sottoinsieme finito di
ℕ formato quindi dai seguenti 100000 numeri naturali:
0, 1, 2, 3, ..., 99996, 99997, 99998, 99999
3.2.1 Addizione tra numeri interi senza segno
Con il sistema di codifica appena illustrato, le addizioni non ci creano problemi a meno
che la somma dei due addendi non superi 99999, cioè il numero più grande che il
computer può rappresentare con 5 cifre; la Figura 3.2 illustra i vari casi che si
possono presentare:
Come si può notare, gli esempi 1 e 2 forniscono il risultato che ci
aspettavamo, mentre i risultati degli esempi 3 e 4 sono (apparentemente)
privi di senso; ricordando l'esempio del contachilometri, siamo in grado di capire cosa
è successo. La CPU ha eseguito i calcoli correttamente, ma ha dovuto troncare
la cifra più significativa (cioè quella più a sinistra che è anche la cifra di peso
maggiore) perché non è in grado di rappresentare un numero di 6 cifre; ecco
quindi che 110000d diventa 10000d e 100000d diventa 00000d.
Quello che sembra un grave problema, viene risolto in modo piuttosto semplice attraverso
un sistema che permette alla CPU di dare al programmatore la possibilità di
interpretare correttamente il risultato; ogni CPU è dotata di una serie di
"segnalatori" chiamati flags attraverso i quali è possibile avere un controllo
totale sul risultato delle operazioni appena eseguite.
Il primo flag che incontriamo si chiama Carry Flag (CF) e cioè segnalatore
di riporto; nella Figura 3.2 è riportato a fianco di ogni risultato lo stato di CF
che vale 0 se il risultato non produce un riporto, vale invece 1 se si è
verificato un riporto. Ecco quindi che nei primi due casi CF=0 indica che il
risultato è valido così com'è; nel terzo caso CF=1 indica che il risultato deve
essere interpretato come 10000 con riporto di 1 (cioè con riporto di un
centinaio di migliaia). Nel quarto caso CF=1 indica che il risultato deve essere
interpretato come 00000 con riporto di 1 (cioè con riporto di un centinaio
di migliaia).
In sostanza il programmatore, subito dopo aver effettuato una addizione, deve verificare lo
stato di CF; in questo modo ha la possibilità di sapere se si è verificato o
meno un riporto. A questo punto si intuisce subito il fatto che grazie a CF possiamo
effettuare persino somme tra numeri teoricamente di qualunque dimensione superando così le
limitazioni imposteci dall'hardware; la tecnica da utilizzare è la stessa che ci viene
insegnata meccanicamente alle scuole elementari e che adesso possiamo chiarire meglio
sfruttando le conoscenze che abbiamo acquisito nel precedente capitolo. Supponiamo, ad
esempio, di voler eseguire la seguente somma:
Perché il numero viene incolonnato in questo modo?
La risposta è data dal fatto che stiamo usando il sistema posizionale arabo e quindi, per
eseguire correttamente una somma dobbiamo incolonnare unità con unità, decine con decine,
centinaia con centinaia, etc. Partendo allora dalla colonna delle unità si ottiene:
5 unità + 9 unità = 14 unità
cioè 4 unità con riporto di una decina che verrà aggiunta alla colonna delle decine.
Passando alla colonna delle decine otteniamo:
7 decine + 4 decine = 11 decine
più una decina di riporto = 12 decine pari a 120 unità, cioè 2 decine
con riporto di un centinaio che verrà aggiunto alla colonna delle centinaia.
Passando infine alla colonna delle centinaia otteniamo:
3 centinaia + 2 centinaia = 5 centinaia
più un centinaio di riporto = 6 centinaia; il risultato finale sarà quindi
624.
Si può subito notare che sommando una colonna qualsiasi, l'eventuale riporto non sarà mai
superiore a 1; infatti, il caso peggiore per la prima colonna è:
9 + 9 = 18
cioè 8 con riporto di 1. Per le colonne successive alla prima, anche in
presenza del riporto (1) il caso peggiore è:
9 + 9 + 1 = 19
cioè 9 con riporto di 1.
Simuliamo ora questo procedimento sul nostro computer sfruttando però il fatto che la
nostra CPU può gestire direttamente (via hardware) numeri a 5 cifre;
questo significa che per sommare numeri con più di 5 cifre, basta spezzarli in
gruppi di 5 cifre (partendo da destra). La CPU somma via hardware due
gruppi corrispondenti di 5 cifre fornendoci alla fine l'eventuale riporto attraverso
CF; il contenuto di CF può essere utilizzato quindi per la somma successiva.
Anche in questo caso, sommando tra loro gruppi di 5 cifre, l'eventuale riporto finale
non potrà mai superare 1; osserviamo, infatti, che la somma massima è:
99999 + 99999 = 199998
cioè 99998 con riporto di 1. Se c'era già un riporto si ottiene 199999,
cioè 99999 con riporto di 1; in definitiva, possiamo dire che CF
contiene il riporto (0 o 1) della somma appena effettuata.
Per effettuare quindi con la nostra CPU la somma tra due numeri, sarà necessario
incolonnare, non le cifre corrispondenti, ma i gruppi di 5 cifre corrispondenti; in
sostanza, le colonne dell'addizione sono formate, non da cifre ma da gruppi di 5
cifre. Partendo dalla colonna più a destra facciamo eseguire la prima somma alla CPU;
in seguito passiamo alla colonna successiva ed eseguiamo la nuova somma tenendo conto
dell'eventuale riporto prodotto dalla somma precedente.
Per chiarire meglio questi concetti, vediamo in Figura 3.3 un esempio relativo a numeri che
possono avere sino a 15 cifre:
La terza ed ultima somma ci dà CF=0 per indicare che non c'è un ulteriore
riporto e quindi il risultato finale è valido così com'è; se alla fine avessimo
ottenuto CF=1, avremmo dovuto aggiungere un 1 alla sinistra delle 15
cifre del risultato.
Con questo sistema, è possibile sommare numeri con (in teoria) un qualunque numero di
cifre, suddividendoli in gruppi di 5 cifre che possono essere gestiti direttamente
dalla CPU; naturalmente, la gestione degli eventuali riporti è a carico del
programmatore che dovrà tenerne conto nel programma che esegue la somma.
È chiaro che se la CPU fosse in grado di gestire direttamente (via hardware)
numeri di 15 cifre, eseguirebbe le somme alla massima velocità possibile; nel
nostro caso, invece, siamo costretti a seguire il metodo appena visto (via software) con
conseguente diminuzione delle prestazioni. Le industrie che producono hardware cercano di
realizzare CPU con architetture sempre più ampie proprio perché questo significa
aumentare in modo notevole le prestazioni.
3.2.2 Sottrazione tra numeri interi senza segno
Passando alle sottrazioni, sappiamo che nell'insieme ℕ questa operazione è
possibile solo quando il primo numero (minuendo) è maggiore o almeno uguale al
secondo numero (sottraendo); se, invece, il minuendo è minore del sottraendo, il
risultato che si ottiene è negativo e quindi non appartiene ad ℕ. In Figura 3.4
vediamo come si comporta la CPU nei vari casi possibili:
Nei primi due esempi di Figura 3.4 la CPU restituisce il risultato che ci aspettavamo
in quanto il minuendo è maggiore (primo esempio) o almeno uguale (secondo esempio) al
sottraendo; negli esempi 3 e 4, invece (minuendo minore del sottraendo), i
risultati forniti appaiono piuttosto strani. Notiamo però che anche nel caso della
sottrazione, la CPU modifica CF che viene posto a 1 ogni volta che
si dovrebbe ottenere un risultato negativo; come deve essere interpretata questa situazione?
Questa volta CF non deve essere interpretato come la segnalazione di un riporto, ma
trattandosi di una sottrazione, CF sta segnalando un prestito (borrow); la
CPU cioè si comporta come se il minuendo continuasse verso sinistra con altri gruppi
di 5 cifre dai quali chiedere il prestito. A questo punto possiamo spiegarci il
risultato degli esempi 3 e 4 della Figura 3.3:
3) 50000 - 60000 = 90000
con prestito di 1 (cioè di 100000) dall'ipotetico successivo gruppo di
5 cifre del minuendo; infatti:
(50000 + 100000) - 60000 = 150000 - 60000 = 90000
con CF=1 che segnala il prestito.
4) 00100 - 50000 = 50100
con prestito di 1 (cioè di 100000) dall'ipotetico successivo gruppo di
5 cifre del minuendo; infatti:
(00100 + 100000) - 50000 = 100100 - 50000 = 50100
con CF=1 che segnala il prestito.
La CPU quindi simula il meccanismo che ci viene insegnato alle scuole elementari
per calcolare una sottrazione; supponiamo, ad esempio, di voler calcolare:
Come nel caso dell'addizione, i due numeri vengono incolonnati in questo modo in quanto
dobbiamo sottrarre unità da unità, decine da decine, centinaia da centinaia, etc.
Partendo allora dalla colonna delle unità si vede subito che da 0 unità non si
possono sottrarre 9 unità; si chiede allora un prestito di 10 unità (cioè
di una decina) alle decine del minuendo, ma non essendoci decine, si chiede allora un
prestito di 10 unità alle centinaia del minuendo che diventano così:
300 - 10 = 290
Possiamo ora sottrarre la prima colonna ottenendo:
10 - 9 = 1
Passiamo alla seconda colonna dove troviamo le 9 decine rimaste in seguito al
prestito precedente; la sottrazione relativa alla seconda colonna ci dà:
9 - 0 = 9
Passiamo infine alla terza colonna dove troviamo le 2 centinaia rimaste; quindi:
2 - 0 = 2
Il risultato finale è: 291.
Attraverso questo sistema la CPU ci permette (come per l'addizione) di sottrarre
tra loro numeri interi senza segno di qualunque dimensione; come al solito la tecnica da
adottare consiste nel suddividere, sia il minuendo, sia il sottraendo, in gruppi di
5 cifre a partire da destra. La Figura 3.5 mostra un esempio pratico relativo alla
sottrazione tra numeri da 8 cifre ciascuno:
La seconda ed ultima sottrazione ci dà CF=0 per indicare che non c'è stato un
ulteriore prestito e quindi il risultato finale è valido così com'è; se avessimo
ottenuto CF=1 il risultato sarebbe stato negativo e quindi non appartenente ad
ℕ.
Come si può notare, la sottrazione relativa alla prima colonna non deve tenere conto di
alcun prestito precedente; le sottrazioni relative, invece, a tutte le colonne successive
alla prima devono verificare il contenuto di CF per sapere se c'è stato un prestito
richiesto dalla precedente sottrazione. Anche in questo caso si può dimostrare che
l'eventuale prestito è rappresentato proprio dal valore 0 o 1 contenuto in
CF; infatti, così come nell'addizione il riporto massimo è 1, anche nella
sottrazione il prestito massimo non supera mai 1. Osserviamo a tale proposito che
in relazione alla sottrazione della prima colonna, il caso peggiore che si può verificare è:
00000 - 99999
che richiede un prestito di 1 dalla colonna successiva; in relazione, invece, alle
sottrazioni delle colonne successive alla prima, il caso peggiore che si può presentare è:
(00000 - CF) - 99999
che anche nel caso di CF=1 richiederà al massimo un prestito di 1 dalla
colonna successiva.
In questo secondo caso si può notare che vengono effettuate due sottrazioni per tenere
conto anche di CF; è evidente però che queste due sottrazioni non potranno mai
richiedere due prestiti. Infatti, se il minuendo vale 00000 e CF vale
1, allora la prima sottrazione richiede un prestito che trasforma il minuendo in:
100000 - CF = 100000 - 1 = 99999
Di conseguenza la seconda sottrazione non potrà mai richiedere un prestito perché il
sottraendo non può essere maggiore di 99999.
Se il minuendo è maggiore di 00000 (ad esempio, 00001), allora la prima
sottrazione non potrà mai richiedere un prestito in quanto CF vale al massimo
1; il prestito (di 1) potrà essere necessario eventualmente per la
seconda sottrazione.
Ci si può chiedere come debba essere interpretato l'esempio 3 della Figura 3.4:
3) 50000d - 60000d = 90000d (invece di -10000) CF = 1
nel caso in cui si stia operando su numeri interi di sole 5 cifre; la risposta
è che nell'insieme ℕ questo risultato non ha senso perché in questo caso il
minuendo non può essere minore del sottraendo. Il risultato, invece, ha senso
nell'insieme dei numeri interi positivi e negativi (detti numeri con segno o numeri
relativi); in questo caso, infatti, come vedremo più avanti 90000d è proprio
la codifica numerica del numero negativo -10000!
Per capire ora il vero significato di CF analizziamo i valori assunti in sequenza
da un numero a 5 cifre che viene via via incrementato di una unità per volta (o
che viene via via decrementato di una unità per volta):
..., 99996, 99997, 99998, 99999, 00000, 00001, 00002, 00003, ...
In questa sequenza possiamo individuare un punto molto importante rappresentato dal
"confine" che separa 99999 e 00000; passando da 99999 a 00000
attraversiamo questo confine da sinistra a destra, mentre passando da 00000 a
99999 attraversiamo questo confine da destra a sinistra.
Osserviamo ora che nel caso dell'addizione tra numeri interi senza segno, se il risultato
finale non supera 99999 (CF=0), vuol dire che siamo rimasti alla sinistra
del confine; se, invece, il risultato finale supera 99999 (CF=1), vuol dire
che abbiamo oltrepassato il confine procedendo da sinistra a destra.
Passando alle sottrazioni tra numeri interi senza segno, se il risultato finale è maggiore
o uguale a 00000 (CF=0), vuol dire che siamo rimasti alla destra del confine;
se, invece, il risultato finale è minore di 00000 (CF=1), vuol dire che
abbiamo oltrepassato il confine procedendo da destra verso sinistra.
In definitiva possiamo dire che CF=1 segnala il fatto che nel corso di una operazione
tra numeri interi senza segno, è stato oltrepassato il confine 00000, 99999
o in un verso o nell'altro; in caso contrario si ottiene CF=0.
3.2.3 Moltiplicazione tra numeri interi senza segno
Contrariamente a quanto si possa pensare, la moltiplicazione non crea alla CPU
alcun problema legato ad un eventuale risultato (prodotto) di dimensioni
superiori al massimo permesso; osserviamo, infatti, che:
per i numeri ad una cifra il prodotto massimo è:
9 x 9 = 81
che è minore di:
10 x 10 = 100
per i numeri a due cifre il prodotto massimo è:
99 x 99 = 9801
che è minore di:
100 x 100 = 10000
per i numeri a tre cifre il prodotto massimo è:
999 x 999 = 998001
che è minore di:
1000 x 1000 = 1000000
e così via.
Possiamo dire quindi che moltiplicando tra loro due fattori formati ciascuno da
una sola cifra, il prodotto massimo non può avere più di 2 cifre (1+1);
moltiplicando tra loro due fattori formati ciascuno da due cifre, il prodotto massimo
non può avere più di 4 cifre (2+2). Moltiplicando tra loro due fattori
formati ciascuno da tre cifre, il prodotto massimo non può avere più di 6 cifre
(3+3); moltiplicando tra loro due fattori formati ciascuno da quattro cifre, il
prodotto massimo non può avere più di 8 cifre (4+4).
In generale, il prodotto tra due fattori formati ciascuno da n cifre, è un numero
che richiede al massimo n+n=2n cifre, cioè il doppio di n; grazie a questa
proprietà delle moltiplicazioni tra numeri interi, la CPU può eseguire prodotti
su numeri interi senza segno a 5 cifre, disponendo il risultato finale in due gruppi
da 5 cifre ciascuno, che uniti assieme forniscono il risultato completo a 10
cifre. La Figura 3.6 mostra alcuni esempi pratici:
Le moltiplicazioni tra numeri interi a 5 cifre vengono eseguite direttamente (via
hardware) dalla nostra CPU che è dotata come abbiamo visto di architettura a
5 cifre; se vogliamo moltiplicare tra loro numeri formati da più di 5 cifre,
dobbiamo procedere via software simulando sul computer lo stesso metodo che si segue usando
carta e penna. Il meccanismo dovrebbe essere ormai chiaro; comunque, in un apposito
capitolo verrà mostrato un programma di esempio. L'aspetto importante da ribadire ancora
una volta è che la gestione di queste operazioni via software da parte del programmatore
è nettamente più lenta della gestione via hardware da parte della CPU.
3.2.4 Caso particolare per la moltiplicazione tra numeri interi senza segno
In relazione alle moltiplicazioni tra numeri interi senza segno, si presenta un caso
particolare che ci permette di velocizzare notevolmente questa operazione sfruttando
una proprietà dei sistemi di numerazione posizionali; questo caso particolare è
rappresentato dalla eventualità che uno dei due fattori sia esprimibile sotto forma
di potenza con esponente intero della base 10. Vediamo, infatti, quello che
succede quando si moltiplica un numero intero senza segno per 10n;
affinché il risultato non superi le 5+5=10 cifre, n può assumere uno tra i
valori 1, 2, 3, 4.
350 x 101 = 350 x 10 = 00350d x 00010d = 00000d 03500d = 3500
Notiamo che la moltiplicazione ha determinato l'aggiunta di uno zero alla
destra di 350; tutto ciò equivale a far scorrere di un posto verso sinistra
tutte le cifre di 350 riempiendo con uno zero il posto rimasto libero a destra.
350 x 102 = 350 x 100 = 00350d x 00100d = 00000d 35000d = 35000
Notiamo che la moltiplicazione ha determinato l'aggiunta di due zeri alla
destra di 350; tutto ciò equivale a far scorrere di due posti verso sinistra
tutte le cifre di 350 riempiendo con due zeri i posti rimasti liberi a destra.
350 x 103 = 350 x 1000 = 00350d x 01000d = 00003d 50000d = 350000
Notiamo che la moltiplicazione ha determinato l'aggiunta di tre zeri alla
destra di 350; tutto ciò equivale a far scorrere di tre posti verso sinistra
tutte le cifre di 350 riempiendo con tre zeri i posti rimasti liberi a destra.
350 x 104 = 350 x 10000 = 00350d x 10000d = 00035d 00000d = 3500000
Notiamo che la moltiplicazione ha determinato l'aggiunta di quattro zeri alla
destra di 350; tutto ciò equivale a far scorrere di quattro posti verso
sinistra tutte le cifre di 350 riempiendo con quattro zeri i posti rimasti
liberi a destra.
In definitiva, ogni volta che uno dei fattori è esprimibile nella forma
10n, è possibile velocizzare notevolmente la moltiplicazione
aggiungendo direttamente n zeri alla destra dell'altro fattore (facendo
così scorrere di n posti verso sinistra tutte le cifre dell'altro fattore);
proprio per questo motivo, tutte le CPU forniscono una apposita istruzione per
lo scorrimento verso sinistra delle cifre di un numero intero senza segno. Nel caso
delle CPU della famiglia 80x86 questa istruzione viene chiamata
SHL o shift logical left (scorrimento logico verso sinistra); è
sottinteso che questa istruzione debba essere usata con numeri interi senza segno.
Quando si impiega questa istruzione, bisogna sempre ricordarsi del fatto che la
CPU può gestire numeri interi senza segno aventi un numero limitato di cifre;
utilizzando quindi un valore troppo alto per l'esponente n, si può provocare
il "trabocco" da sinistra di cifre significative del risultato, ottenendo in questo
modo un valore privo di senso. Se richiediamo, ad esempio, alla CPU lo scorrimento
di 5 posti verso sinistra di tutte le cifre del numero 00350d, otteniamo
come risultato 00000d!
3.2.5 Divisione tra numeri interi senza segno
Anche la divisione non crea problemi in quanto, se si divide un numero (dividendo)
per un altro numero (divisore), il risultato che si ottiene (quoziente)
non potrà mai essere maggiore del dividendo; dividendo quindi tra loro due numeri interi
senza segno a 5 cifre, il quoziente non potrà mai superare 99999. Il caso
peggiore che si può presentare è, infatti:
99999 / 00001 = 99999
Analogo discorso vale anche per il resto della divisione; come si sa dalla
matematica, infatti, il resto di una divisione non può essere maggiore del divisore, e
quindi non potrà mai superare 99999.
La CPU anche in questo caso dispone il risultato in due gruppi da 5 cifre
ciascuno; il primo gruppo contiene il quoziente mentre il secondo gruppo contiene il
resto. Bisogna osservare, infatti, che in questo caso stiamo parlando di divisione
intera, cioè dell'operazione di divisione effettuata nell'insieme ℕ; il
quoziente e il resto che si ottengono devono appartenere a loro volta ad ℕ, per
cui devono essere numeri interi senza segno. Nel caso in cui il dividendo non sia un
multiplo intero del divisore, si dovrebbe ottenere in teoria un numero con la virgola
(numero reale); la CPU in questo caso troncherà al quoziente la parte decimale,
cioè la parte posta dopo la virgola. La Figura 3.7 mostra una serie di esempi pratici;
il simbolo Q indica il quoziente, mentre il simbolo R indica il resto.
Come si può notare, trattandosi di divisione intera, se il dividendo è più piccolo del
divisore, si ottiene zero come risultato (esempio 3).
Anche la divisione tra numeri con più di 5 cifre può essere simulata via software
attraverso lo stesso procedimento che si segue con carta e penna; un esempio in proposito
verrà mostrato in un apposito capitolo.
Quando si effettua una divisione, si presenta un caso critico rappresentato dal
divisore che vale zero; cosa succede se il divisore è zero?
Come si sa dalla matematica, dividendo un numero non nullo per zero, si ottiene un numero
(in valore assoluto) infinitamente grande, tale cioè da superare qualunque altro numero
grande a piacere; naturalmente, la CPU non è in grado di gestire un numero
infinitamente grande, per cui la divisione per zero viene considerata una condizione di
errore (esattamente come accade con le calcolatrici). Si tratta di una situazione
abbastanza delicata che generalmente viene gestita direttamente dal sistema operativo;
il DOS, ad esempio, interrompe il programma in esecuzione e stampa sullo schermo
un messaggio del tipo:
Divisione per zero
In Linux, un programma eseguito dalla console che effettua una divisione per zero, viene
interrotto dal SO con il messaggio:
Eccezione in virgola mobile
Analogamente, Windows mostra una finestra per informare che il programma in
esecuzione verrà interrotto in seguito ad una:
Operazione non valida
3.2.6 Caso particolare per la divisione tra numeri interi senza segno
Anche per le divisioni tra numeri interi senza segno, si presenta il caso particolare
citato prima per le moltiplicazioni; vediamo, infatti, quello che succede quando si
divide un numero intero senza segno per 10n (con n compreso
tra 1 e 4).
85420 / 101 = 85420 / 10 = 85420d / 00010d = [Q = 08542d, R = 00000d]
Notiamo che la divisione ha determinato l'aggiunta di uno zero alla sinistra di
85420 facendo "traboccare" da destra la sua prima cifra; tutto ciò equivale
a far scorrere di un posto verso destra tutte le cifre di 85420 riempiendo con
uno zero il posto rimasto libero a sinistra. La cifra che "trabocca" da destra
rappresenta il resto (0) della divisione.
85420 / 102 = 85420 / 100 = 85420d / 00100d = [Q = 00854d, R = 00020d]
Notiamo che la divisione ha determinato l'aggiunta di due zeri alla sinistra di
85420 facendo "traboccare" da destra le sue prime due cifre; tutto ciò equivale
a far scorrere di due posti verso destra tutte le cifre di 85420 riempiendo con
due zeri i posti rimasti liberi a sinistra. Le due cifre che "traboccano" da destra
rappresentano il resto (20) della divisione.
85420 / 103 = 85420 / 1000 = 85420d / 01000d = [Q = 00085d, R = 00420d]
Notiamo che la divisione ha determinato l'aggiunta di tre zeri alla sinistra di
85420 facendo "traboccare" da destra le sue prime tre cifre; tutto ciò equivale
a far scorrere di tre posti verso destra tutte le cifre di 85420 riempiendo con
tre zeri i posti rimasti liberi a sinistra. Le tre cifre che "traboccano" da destra
rappresentano il resto (420) della divisione.
85420 / 104 = 85420 / 10000 = 85420d / 10000d = [Q = 00008d, R = 05420d]
Notiamo che la divisione ha determinato l'aggiunta di quattro zeri alla sinistra
di 85420 facendo "traboccare" da destra le sue prime quattro cifre; tutto ciò
equivale a far scorrere di quattro posti verso destra tutte le cifre di 85420
riempiendo con quattro zeri i posti rimasti liberi a sinistra. Le quattro cifre che
"traboccano" da destra rappresentano il resto (5420) della divisione.
In definitiva, ogni volta che il divisore è esprimibile nella forma
10n, è possibile velocizzare notevolmente la divisione inserendo
direttamente n zeri alla sinistra del dividendo (facendo così traboccare da
destra le n cifre meno significative del dividendo, le quali rappresentano il
resto della divisione); proprio per questo motivo, tutte le CPU forniscono una
apposita istruzione per lo scorrimento verso destra delle cifre di un numero intero senza
segno. Nel caso delle CPU della famiglia 80x86 questa istruzione viene
chiamata SHR o shift logical right (scorrimento logico verso destra); è
sottinteso che questa istruzione debba essere usata con numeri interi senza segno.
3.3 Numeri interi con segno (positivi e negativi)
Qualunque informazione gestibile dal computer può essere codificata attraverso
i numeri interi senza segno che abbiamo appena esaminato; questo sistema di
codifica presenta però anche un interessantissimo "effetto collaterale" che
permette alla CPU di gestire in modo diretto persino i numeri interi
positivi e negativi (numeri con segno).
In matematica l'insieme numerico rappresentato dalla sequenza:
..., -5, -4, -3, -2, -1, 0, +1, +2, +3, +4, +5, ...
viene chiamato insieme dei numeri interi relativi e viene indicato con il simbolo
ℤ; come si può notare, l'insieme ℕ è incluso nell'insieme ℤ.
Vediamo allora come bisogna procedere per codificare l'insieme ℤ sul nostro
computer; anche in questo caso appare evidente il fatto che la CPU potrà gestire
solo un sottoinsieme finito degli infiniti numeri appartenenti a ℤ.
Rispetto al caso dell'insieme ℕ, la codifica dell'insieme ℤ non appare
molto semplice perché in questo caso dobbiamo rappresentare, oltre ai numeri interi
positivi e allo zero, anche i numeri negativi dotati cioè di segno meno (-);
il computer non ha la più pallida idea di cosa sia un segno più o un segno meno, ma
ormai abbiamo capito che la soluzione consiste come al solito nel codificare qualunque
informazione, compreso il segno di un numero, attraverso un numero stesso.
La prima considerazione importante riguarda il fatto che, come già sappiamo,
l'architettura a 5 cifre della nostra CPU ci permette di rappresentare
via hardware numeri interi compresi tra 00000d e 99999d, per un totale
di 100000 possibili codici numerici; per motivi di simmetria nella codifica
dell'insieme ℤ, questi 100000 codici differenti devono essere divisi
in due parti uguali in modo da poter rappresentare 50000 numeri positivi
(compreso lo zero) e 50000 numeri negativi. In sostanza, dovremmo essere in
grado di rappresentare in questo modo un sottoinsieme finito di ℤ formato dai
seguenti numeri relativi:
-50000, -49999, ..., -3, -2, -1, +0, +1, +2, +3, ..., +49998, +49999
Come si può notare, il numero 0 viene trattato come un numero positivo; proprio
per questo motivo i numeri positivi arrivano solo a +49999.
Nella determinazione di questo metodo di codifica, fortunatamente ci viene incontro
l'effetto collaterale a cui si è accennato in precedenza; a tale proposito, torniamo per
un momento alle sottrazioni tra numeri interi senza segno a 5 cifre e osserviamo
quello che accade in Figura 3.8:
Come sappiamo, la CPU effettua queste sottrazioni segnalandoci un prestito
attraverso CF; lasciamo perdere il prestito e osserviamo attentamente la
Figura 3.8. Sembrerebbe che, per la CPU, 99999d equivalga a -1,
99998d equivalga a -2, 99997d equivalga a -3 e così via;
se questa deduzione fosse giusta, si otterrebbero quindi le corrispondenze mostrate
in Figura 3.9:
Verifichiamo subito quello che accade provando a calcolare:
1 - 1 = (+1) + (-1) = 0
Con il metodo di codifica illustrato in Figura 3.9 si ottiene:
00001d + 99999d = 00000d (CF=1)
Lasciando perdere CF possiamo constatare che il risultato ottenuto (00000d)
appare corretto.
Effettuiamo una seconda prova e calcoliamo:
1 - 50000 = (+1) + (-50000) = -49999
Con il metodo di codifica illustrato in Figura 3.9 si ottiene:
00001d + 50000d = 50001d (CF=0)
Lasciando perdere CF possiamo constatare che il risultato ottenuto è giusto in
quanto 50001d è proprio la codifica di -49999.
Effettuiamo una terza prova e calcoliamo:
-1 - 2 = (-1) + (-2) = -3
Con il metodo di codifica illustrato in Figura 3.9 si ottiene:
99999d + 99998d = 99997d (CF=1)
Lasciando perdere CF possiamo constatare che il risultato ottenuto è giusto in
quanto 99997d è proprio la codifica di -3.
Si tratta di un vero e proprio colpo di scena in quanto il metodo appena descritto sembra
funzionare perfettamente!
Ed infatti, questo è proprio il metodo che la CPU utilizza per gestire i numeri
interi con segno; se proviamo ad effettuare qualsiasi altra prova, otteniamo sempre il
risultato corretto. Anche il nostro famoso contachilometri sembra confermare questi risultati;
supponiamo, infatti, che il contachilometri segni 00000 e che possa funzionare anche al
contrario quando la macchina va in retromarcia. Se procediamo in marcia avanti il
contachilometri segnerà via via:
00001, 00002, 00003, 00004, 00005, ...
come se volesse indicare:
+1, +2, +3, +4, +5, ...
Se procediamo ora in marcia indietro il contachilometri (partendo sempre da 00000)
segnerà via via:
99999, 99998, 99997, 99996, 99995, ...
come se volesse indicare:
-1, -2, -3, -4, -5, ...
Ovviamente scegliamo come confine tra numeri positivi e negativi quello delimitato da
49999 e 50000 perché così dividiamo i 100000 numeri possibili in
due parti uguali; in questo modo, tutti i codici numerici a 5 cifre la cui cifra
più significativa è compresa tra 0 e 4, rappresentano numeri interi
positivi. Analogamente, tutti i codici numerici a 5 cifre la cui cifra più
significativa è compresa tra 5 e 9, rappresentano numeri interi negativi;
l'unico trascurabile difetto di questo sistema è che lo zero appare come un numero positivo,
mentre sappiamo che in matematica lo zero rappresenta il nulla e quindi non ha segno.
A questo punto ci chiediamo: come è possibile che un sistema del genere possa funzionare?
Naturalmente, alla base di quello che abbiamo appena visto non ci sono fenomeni paranormali,
ma bensì la cosiddetta aritmetica modulare; per capire l'aritmetica modulare,
facciamo ancora appello al nostro contachilometri supponendo che stia segnando 99998
(quindi abbiamo percorso 99998 km). Se percorriamo altri 4 km, il
contachilometri segnerà via via:
99998, 99999, 00000, 00001, 00002, ...
invece di:
99998, 99999, 100000, 100001, 100002, ...
Il contachilometri, infatti, non può mostrare più di 5 cifre e quindi lavora in
modulo 100000 (modulo centomila) mostrando cioè non i km percorsi, ma il
resto che si ottiene dalla divisione intera tra i km percorsi e 100000;
infatti, indicando con MOD il resto della divisione intera e supponendo di partire
da 00000 km, si ottiene la situazione mostrata in Figura 3.10:
In pratica, quando la nostra automobile supera i 99999 km percorsi, il contachilometri
si azzera.
Quando la nostra automobile supera i 199999 km percorsi, il contachilometri si azzera
una seconda volta.
Quando la nostra automobile supera i 299999 km percorsi, il contachilometri si azzera
una terza volta.
In generale, ogni volta che raggiungiamo un numero di km percorsi che è un multiplo intero
di 100000, il contachilometri si azzera e ricomincia a contare da 00000.
Dalle considerazioni appena esposte si deduce quanto segue:
se il contachilometri segna 00200 e percorriamo 200 km in marcia
indietro, alla fine il contachilometri segnerà 00000; possiamo dire quindi che:
200 - 200 = 00000d
se il contachilometri segna 00200 e percorriamo 201 km in marcia
indietro, alla fine il contachilometri segnerà non -1 ma 99999; possiamo
dire quindi che:
200 - 201 = 99999d
se il contachilometri segna 00200 e percorriamo 4000 km in marcia
indietro, alla fine il contachilometri segnerà non -3800 ma 96200; possiamo
dire quindi che:
200 - 4000 = 96200d
se il contachilometri segna 00200 e percorriamo 15000 km in marcia
indietro, alla fine il contachilometri segnerà non -14800 ma 85200; possiamo
dire quindi che:
200 - 15000 = 85200d
Questa è l'aritmetica modulare del nostro contachilometri a 5 cifre; tutto ciò
si applica quindi in modo del tutto identico alla nostra CPU con architettura a
5 cifre.
Un'altra importante conseguenza legata alle cose appena viste, è che nell'aritmetica
modulo 100000 possiamo dire che 00000 equivale a 100000; possiamo
scrivere quindi:
3.3.1 Complemento a 10
Dalla Figura 3.11 ricaviamo subito la formula necessaria per convertire in modulo
100000 un numero intero negativo; in generale, dato un numero intero positivo
compreso tra +1 e +50000, per ottenere il suo equivalente negativo in
modulo 100000 dobbiamo calcolare semplicemente:
100000 - n
Supponiamo, ad esempio, di voler ottenere la rappresentazione modulo 100000 di
-100; applicando la formula precedente otteniamo:
100000 - 100 = 99900d
Nell'eseguire questa sottrazione con carta e penna, si presenta spesso la necessità di
richiedere dei prestiti; per evitare questo fastidio possiamo osservare quanto segue:
100000 - n = (99999 + 1) - n = (99999 - n) + 1
In questo caso il minuendo della sottrazione è 99999, per cui non sarà mai
necessario richiedere dei prestiti; la tecnica appena descritta per la codifica dei numeri
interi con segno viene chiamata complemento a 10.
Attraverso questa tecnica la CPU può negare facilmente un numero intero,
cioè cambiare di segno un numero intero; consideriamo, ad esempio, il numero -25000
che in modulo 100000 viene rappresentato come:
(99999 - 25000) + 1 = 74999 + 1 = 75000d
È chiaro allora che negando 75000 (cioè -25000) dovremmo ottenere
+25000; infatti:
(99999 - 75000) + 1 = 24999 + 1 = 25000d
che è proprio la rappresentazione in complemento a 10 di +25000; questo
significa che per la CPU si ha correttamente:
-(-25000) = +25000
In definitiva, con la tecnica appena vista la nostra CPU può gestire via
hardware tutti i numeri relativi compresi tra -50000 e +49999; la codifica
utilizzata prende anche il nome di rappresentazione in complemento a 10 dei
numeri con segno in modulo 100000.
3.3.2 Addizione e sottrazione tra numeri interi con segno
Una volta definito il sistema di codifica in modulo 100000 per i numeri interi
con segno, possiamo passare all'applicazione delle quattro operazioni fondamentali; in
relazione all'addizione e alla sottrazione, si verifica subito una sorpresa estremamente
importante. Riprendiamo a tale proposito l'esempio 3 di Figura 3.4:
50000d - 60000d = 90000d (CF = 1)
Osserviamo subito che se stiamo operando su numeri interi senza segno, la precedente
operazione deve essere interpretata come:
50000 - 60000 = 90000
con prestito di 1 (CF=1); la presenza di un prestito ci indica come
sappiamo che il risultato deriva da:
150000 - 60000 = 90000
Se, invece, stiamo operando su numeri interi con segno, osserviamo subito che 50000d
è la codifica numerica di -50000, mentre 60000d è la codifica numerica
di -40000; la precedente operazione deve essere quindi interpretata come:
(-50000) - (-40000) = -50000 + 40000 = -10000
Ma -10000 in complemento a 10 si scrive proprio 90000d; infatti:
100000 - 10000 = 90000d
L'aritmetica modulare ancora una volta è responsabile di questa meraviglia che permette
alla CPU di eseguire somme e sottrazioni senza la necessità di distinguere tra
numeri interi con o senza segno; per la CPU tutto questo significa una ulteriore
semplificazione circuitale in quanto, in relazione alle addizioni e alle sottrazioni, si
evitano tutte le complicazioni necessarie per distinguere tra numeri interi senza segno
e numeri interi con segno. Nelle CPU della famiglia 80x86 troveremo quindi
un'unica istruzione (chiamata ADD), per sommare numeri interi con o senza segno;
analogamente, troveremo un'unica istruzione (chiamata SUB) per sottrarre numeri
interi con o senza segno.
Naturalmente, il programmatore può avere però la necessità di distinguere tra numeri
interi con o senza segno; in questo caso la CPU ci viene incontro mettendoci a
disposizione oltre a CF, una serie di ulteriori flags attraverso i quali possiamo
avere tutte le necessarie informazioni relative al risultato di una operazione appena
eseguita. In sostanza, la CPU esegue addizioni e sottrazioni senza effettuare
alcuna distinzione tra numeri interi con o senza segno; alla fine la CPU modifica
una serie di flags che si riferiscono alcuni ai numeri senza segno e altri ai numeri
con segno. Sta al programmatore decidere quali flags consultare in base all'insieme
numerico sul quale sta operando.
Abbiamo già conosciuto in precedenza il flag CF che viene utilizzato per
segnalare riporti nel caso delle addizioni e prestiti nel caso delle sottrazioni;
in relazione ai numeri con segno, facciamo la conoscenza con due nuovi flags chiamati
SF e OF. Consideriamo prima di tutto il flag SF che rappresenta il
cosiddetto Sign Flag (flag di segno); dopo ogni operazione la CPU pone
SF=0 se il risultato è positivo o nullo e SF=1 se il risultato è
negativo. La Figura 3.12 mostra alcuni esempi pratici che chiariscono questi aspetti:
Nell'esempio 1, 30000d e 15000d sono positivi, sia nell'insieme
dei numeri senza segno, sia nell'insieme dei numeri con segno; il risultato è
45000d che è un numero positivo in entrambi gli insiemi. Per indicare che
45000d è positivo nell'insieme dei numeri con segno, la CPU pone
SF=0; la CPU pone inoltre CF=0 per indicare che nell'insieme
dei numeri senza segno l'operazione non provoca alcun riporto.
Nell'esempio 2, 95000d e 65000d sono entrambi positivi nell'insieme
dei numeri senza segno, ed entrambi negativi nell'insieme dei numeri con segno; il
risultato è 60000d che è positivo nel primo insieme e negativo nel secondo.
Per indicare che 65000d è negativo nell'insieme dei numeri con segno, la
CPU pone SF=1; la CPU pone inoltre CF=1 per indicare che
nell'insieme dei numeri senza segno l'operazione provoca un riporto.
Nell'esempio 3, 40000d è positivo in entrambi gli insiemi numerici; il
risultato è 80000d che è positivo nel primo insieme e negativo nel secondo.
Per indicare che 80000d è negativo nell'insieme dei numeri con segno, la
CPU pone SF=1; la CPU pone inoltre CF=0 per indicare che
nell'insieme dei numeri senza segno l'operazione non provoca alcun riporto.
Nell'esempio 4, 60000d è positivo nel primo insieme e negativo nel
secondo; il risultato è 20000d che è positivo in entrambi gli insiemi. Per
indicare che 20000d è positivo nell'insieme dei numeri con segno, la
CPU pone SF=0; la CPU pone inoltre CF=1 per indicare che
nell'insieme dei numeri senza segno l'operazione provoca un riporto.
Nel caso in cui si stia operando su numeri interi senza segno formati da sole 5
cifre ciascuno, possiamo dire che in Figura 3.12 gli esempi 1 e 3 producono
un risultato valido così com'è (compreso cioè tra 00000 e 99999); gli
esempi 2 e 4 producono, invece, un riporto (CF=1), per cui bisogna
aggiungere un 1 alla sinistra delle 5 cifre del risultato.
Se stiamo operando, invece, su numeri interi senza segno aventi lunghezza arbitraria
(formati cioè da due o più gruppi di 5 cifre ciascuno), abbiamo già visto
che dobbiamo sfruttare il contenuto di CF per proseguire l'operazione con le
colonne successive alla prima; terminati i calcoli dobbiamo verificare il contenuto
finale di CF per sapere se il risultato ottenuto è valido così com'è
(CF=0) o se deve tenere conto del riporto (CF=1).
Passiamo ora al caso più impegnativo dei numeri interi con segno e supponiamo
inizialmente di operare su numeri formati da sole 5 cifre; al termine di
ogni operazione possiamo consultare SF per conoscere il segno del risultato
finale. Il metodo che utilizza la CPU per stabilire il segno di un numero
è molto semplice; abbiamo già visto, infatti, che nella rappresentazione in
complemento a 10 dei numeri con segno in modulo 100000, un numero
è positivo (SF=0) quando la sua cifra più significativa è compresa tra
0 e 4 ed è invece negativo (SF=1) quando la sua cifra più
significativa è compresa tra 5 e 9.
In relazione alla Figura 3.12 possiamo dire quindi che gli esempi 1 e 4
producono un risultato positivo (SF=0), mentre gli esempi 2 e 3
producono un risultato negativo (SF=1); osservando meglio la situazione ci
accorgiamo però che c'è qualcosa che non quadra. Nell'esempio 1 tutto fila
liscio in quanto sommando tra loro due numeri positivi otteniamo come risultato un
numero positivo; anche nell'esempio 2 tutto è regolare in quanto sommando tra
loro due numeri negativi otteniamo come risultato un numero negativo. Il discorso
cambia, invece, nell'esempio 3 dove sommando tra loro due numeri positivi otteniamo
come risultato un numero negativo; anche nell'esempio 4 vediamo che i conti non
tornano in quanto sommando tra loro due numeri negativi otteniamo come risultato un
numero positivo. Come si spiega questa situazione?
La risposta a questa domanda è molto semplice; abbiamo visto in precedenza che nella
rappresentazione in complemento a 10 dei numeri con segno in modulo 100000,
i numeri positivi vanno da 00000d a 49999d, mentre i numeri negativi
vanno da 50000d a 99999d. È chiaro allora che il risultato di una
somma o di una sottrazione tra numeri con segno a 5 cifre, deve necessariamente
rientrare in questi limiti; a questo punto possiamo capire quello che è successo in
Figura 3.12. Nell'esempio 1 abbiamo sommato i due numeri positivi 30000d
e 15000d ottenendo correttamente un risultato positivo (45000d) compreso
tra 00000d e 49999d; nell'esempio 2 abbiamo sommato i due numeri
negativi 95000d e 65000d ottenendo correttamente un risultato negativo
(60000d) compreso tra 50000d e 99999d. Nell'esempio 3 abbiamo
sommato i due numeri positivi 40000d e 40000d ottenendo però un risultato
negativo (80000d); questo risultato supera il massimo permesso (49999d)
per i numeri positivi a 5 cifre. Nell'esempio 4 abbiamo sommato i due numeri
negativi 60000d e 60000d ottenendo però un risultato positivo
(20000d); questo risultato è inferiore al minimo permesso (50000d) per i
numeri negativi a 5 cifre.
Riassumendo, nel caso dei numeri con segno a 5 cifre possiamo dire che sommando
tra loro due numeri positivi, il risultato finale deve essere ancora un numero positivo
non superiore a +49999 (49999d); analogamente, sommando tra loro due
numeri negativi, il risultato deve essere ancora un numero negativo non minore di
-50000 (50000d).
Se questo non accade vengono superati i limiti (inferiore o superiore) e si dice allora
che si è verificato un trabocco; come molti avranno intuito, la CPU ci
informa dell'avvenuto trabocco attraverso un altro flag che prende il nome di Overflow
Flag (OF), cioè segnalatore di trabocco. Come funziona questo flag?
Lo si capisce subito osservando di nuovo la Figura 3.12 dove si nota che gli esempi
1 e 2 producono un risultato valido; in questo caso la CPU pone
OF=0. La situazione cambia, invece, nell'esempio 3 dove la somma tra due
numeri positivi produce un numero positivo troppo grande (si potrebbe dire "troppo
positivo") che supera quindi il limite superiore di 49999d e sconfina nell'area
riservata ai numeri negativi; in questo caso la CPU ci segnala il trabocco ponendo
OF=1. Anche nell'esempio 4 vediamo che la somma tra due numeri negativi
produce un numero negativo troppo piccolo (si potrebbe dire "troppo negativo") che supera
quindi il limite inferiore di 50000d e sconfina nell'area riservata ai numeri
positivi; anche in questo caso la CPU ci segnala il trabocco ponendo OF=1.
Sia nell'esempio 3, sia nell'esempio 4, il segno del risultato è l'opposto
di quello che sarebbe dovuto essere; la CPU verifica il segno dei due numeri
(operandi) prima di eseguire l'operazione e se vede che il segno del risultato
non è corretto, pone OF=1.
Osserviamo che nel caso della somma tra un numero positivo e uno negativo, non si potrà
mai verificare un trabocco; infatti, il caso peggiore che si può presentare è:
0 + (-50000) = 0 - 50000 = -50000
Nel nostro sistema di codifica l'operazione precedente diventa:
00000d + 50000d = 50000d
con CF=0, SF=1 e OF=0.
Per capire ora il vero significato di OF analizziamo i valori assunti in sequenza
da un numero a 5 cifre che viene via via incrementato di una unità per volta (o
che viene via via decrementato di una unità per volta):
..., 49996, 49997, 49998, 49999, 50000, 50001, 50002, 50003, ...
In questa sequenza possiamo individuare un punto molto importante rappresentato dal
"confine" che separa 49999 e 50000; passando da 49999 a 50000
attraversiamo questo confine da sinistra a destra, mentre passando da 50000 a
49999 attraversiamo questo confine da destra a sinistra.
Se la somma tra due numeri positivi fornisce un risultato non superiore a 49999
(SF=0), vuol dire che siamo rimasti alla sinistra del confine (OF=0); se,
invece, il risultato finale supera 49999 (SF=1), vuol dire che abbiamo
oltrepassato il confine (OF=1) procedendo da sinistra a destra.
Se la somma tra due numeri negativi fornisce un risultato non inferiore a 50000
(SF=1), vuol dire che siamo rimasti alla destra del confine (OF=0); se,
invece, il risultato finale è inferiore a 50000 (SF=0), vuol dire che abbiamo
oltrepassato il confine (OF=1) procedendo da destra a sinistra.
In definitiva possiamo dire che OF=1 segnala il fatto che nel corso di una operazione
tra numeri interi con segno, è stato oltrepassato il confine 49999, 50000 o
in un verso o nell'altro; in caso contrario si ottiene OF=0.
Qualcuno abituato a lavorare con i linguaggi di alto livello, leggendo queste cose avrà
la tentazione di scoppiare a ridere pensando che l'Assembly sia un linguaggio
destinato a pazzi fanatici che perdono tempo con queste assurdità; chi fa questi
ragionamenti però non si rende conto che questi problemi riguardano qualsiasi linguaggio
di programmazione. Le considerazioni esposte in questo capitolo si riferiscono, infatti,
al modo con cui la CPU gestisce le informazioni al suo interno; questo modo di
lavorare della CPU si ripercuote ovviamente su tutti i linguaggi di programmazione,
compreso l'Assembly.
Si può citare un esempio famoso riguardante Tetris, che assieme a Pacman
fu uno dei primi gloriosi giochi per computer comparsi sul mercato; Tetris era
stato scritto in BASIC e per memorizzare il punteggio veniva usato un tipo di dato
INTEGER, cioè un intero con segno che nelle architetture a 16 bit equivale
al tipo signed int del C o al tipo Integer del Pascal. Come
vedremo nel prossimo capitolo, una variabile di questo tipo può assumere tutti i valori
compresi tra -32768 e +32767; i giocatori più abili, riuscivano a superare
abbondantemente i 30000 punti e, arrivati a 32767, bastava fare un altro punto
per veder comparire sullo schermo il punteggio di -32768!
Chi ha letto attentamente questo capitolo avrà già capito il perché; chi, invece, prima
stava ridendo adesso avrà modo di riflettere!
Torniamo ora alla nostra CPU per analizzare quello che succede nel momento in cui
si vogliono eseguire via software, addizioni o sottrazioni su numeri interi con segno
aventi ampiezza arbitraria (formati quindi da più di 5 cifre); in questo caso
il metodo da applicare è teoricamente molto semplice in quanto dobbiamo procedere
esattamente come è stato già mostrato in Figura 3.3 e in Figura 3.5 per i numeri interi
senza segno. Una volta che l'operazione è terminata, dobbiamo consultare non CF
ma OF per sapere se il risultato è valido (OF=0) o se si è verificato
un overflow (OF=1); se il risultato è valido, possiamo consultare SF per
sapere se il segno è positivo (SF=0) o negativo (SF=1).
Per giustificare questo procedimento, è necessario applicare tutti i concetti esposti
in precedenza in relazione all'aritmetica modulare; osserviamo, infatti, che nel caso
dei numeri interi con segno a 5 cifre, la CPU utilizza via hardware la
rappresentazione in complemento a 10 dei numeri in modulo 100000. Che tipo
di rappresentazione si deve utilizzare nel caso dei numeri interi con segno formati da
più di 5 cifre?
È chiaro che anche in questo caso, per coerenza con il sistema di codifica utilizzato
dalla CPU, dobbiamo servirci ugualmente del complemento a 10; è necessario
inoltre stabilire in anticipo il modulo dei numeri interi con segno sui quali vogliamo
operare. Come abbiamo già visto in relazione ai numeri interi senza segno, è opportuno
scegliere sempre un numero di cifre che sia un multiplo intero di quello gestibile via
hardware dalla CPU (nel nostro caso un multiplo intero di 5); in questo
modo è possibile ottenere la massima efficienza possibile nell'algoritmo che esegue
via software una determinata operazione matematica.
Supponiamo, ad esempio, di voler eseguire via software, addizioni e sottrazioni
su numeri interi con segno a 10 cifre; in questo caso è come se avessimo a che
fare con un contachilometri a 10 cifre e quindi il modulo di questi numeri è:
1010 = 10000000000
Ci troviamo quindi ad operare con una rappresentazione in complemento a 10 dei
numeri interi con segno in modulo 10000000000; dividiamo come al solito questi
1010 numeri in due parti uguali in modo da poter codificare
5000000000 numeri interi positivi (compreso lo zero) e 5000000000
numeri interi negativi. In analogia a quanto abbiamo visto nel caso dei numeri interi
con segno a 5 cifre, anche in questo caso, dato un numero intero positivo n
compreso tra +1 e +5000000000, il suo equivalente negativo si otterrà
dalla formula:
10000000000 - n = (9999999999 - n) + 1
In base a questa formula si ottengono le corrispondenze mostrate in Figura 3.13.
Come possiamo notare dalla Figura 3.13, si tratta di una situazione sostanzialmente identica
a quella già vista per i numeri interi con segno a 5 cifre; anche in questo caso
quindi, il segno del numero è codificato nella sua cifra più significativa. Tutte le
codifiche numeriche la cui cifra più significativa è compresa tra 0 e 4
rappresentano numeri interi positivi (compreso lo zero); tutte le codifiche numeriche
la cui cifra più significativa è compresa tra 5 e 9 rappresentano
numeri interi negativi.
In definitiva, se il programmatore vuole eseguire addizioni o sottrazioni su numeri
interi con segno formati al massimo da 10 cifre, deve prima di tutto convertire
questi numeri nella rappresentazione in complemento a 10 modulo 10000000000;
a questo punto si può eseguire l'addizione o la sottrazione con lo stesso metodo già
visto in Figura 3.3 e in Figura 3.5. La Figura 3.14 illustra alcuni esempi pratici (i numeri
interi con segno sono già stati convertiti nella rappresentazione di Figura 3.13):
Se stiamo operando sui numeri interi senza segno, tutte le codifiche numeriche mostrate
in Figura 3.14 si riferiscono ovviamente a numeri positivi a 10 cifre (compreso lo
zero); in questo caso, al termine dell'operazione consultiamo CF per sapere se
c'è stato o meno un riporto finale.
Se stiamo operando sui numeri interi con segno, tutte le codifiche numeriche mostrate
in Figura 3.14 si riferiscono ovviamente a numeri a 10 cifre, sia positivi (compreso
lo zero), sia negativi, rappresentati in complemento a 10; l'operazione si svolge
esattamente come per i numeri interi senza segno, verificando attraverso CF la
presenza di eventuali riporti (per l'addizione) o prestiti (per la sottrazione). Terminata
l'operazione dobbiamo consultare non CF ma OF per sapere se il risultato è
valido o se c'è stato un overflow; è chiaro, infatti, che solo l'ultima colonna contiene
la cifra che codifica il segno.
Supponendo quindi di operare sui numeri interi con segno, l'esempio 1 deve
essere interpretato in questo modo:
(+2099948891) + (+1124066666) = +3224015557
L'operazione viene svolta in modo corretto in quanto la somma di questi due numeri
positivi è un numero positivo non superiore a +4999999999; la CPU ci
dice, infatti, che il risultato è valido (OF=0) ed è positivo (SF=0).
L'esempio 2 deve essere interpretato in questo modo:
(-959977240) + (-1249967112) = -2209944352
L'operazione viene svolta in modo corretto in quanto la somma di questi due numeri
negativi è un numero negativo non inferiore a -5000000000; osserviamo,
infatti, che il numero negativo -2209944352 in complemento a 10 modulo
10000000000 si scrive:
10000000000 - 2209944352 = 7790055648
La CPU ci dice che il risultato è valido (OF=0) ed è negativo
(SF=1).
L'esempio 3 deve essere interpretato in questo modo:
(-4473988655) + (-3979976850) = +1546034495
L'operazione viene svolta in modo sbagliato in quanto la somma di questi due numeri
negativi è un numero negativo inferiore a -5000000000 che sconfina nell'insieme
dei numeri positivi; la CPU ci dice, infatti, che si è verificato un overflow
(OF=1) in quanto sommando due numeri negativi è stato ottenuto un numero
positivo (SF=0).
L'esempio 4 deve essere interpretato in questo modo:
(+4020077921) + (+3980055480) = -1999866599
L'operazione viene svolta in modo sbagliato in quanto la somma di questi due numeri
positivi è un numero positivo superiore a +4999999999 che sconfina nell'insieme
dei numeri negativi; la CPU ci dice, infatti, che si è verificato un overflow
(OF=1) in quanto sommando due numeri positivi è stato ottenuto un numero
negativo (SF=1).
3.3.3 Moltiplicazione tra numeri interi con segno
Come sappiamo dalla matematica, moltiplicando tra loro due numeri interi con segno
si ottiene un risultato il cui segno viene determinato con le seguenti regole:
Osserviamo inoltre che in valore assoluto il risultato che si ottiene è del tutto
indipendente dal segno; abbiamo, ad esempio:
(+350) x (+200) = +70000
e:
(-350) x (+200) = -70000
In valore assoluto si ottiene +70000 in entrambi i casi.
In base a queste considerazioni, possiamo dedurre il metodo seguito dalla nostra
CPU per eseguire via hardware moltiplicazioni tra numeri con segno a 5
cifre (espressi naturalmente in complemento a 10 modulo 100000); le
fasi che vengono svolte sono le seguenti:
- la CPU converte se necessario i due fattori in numeri positivi
- la CPU esegue la moltiplicazione tra numeri positivi
- la CPU aggiunge il segno al risultato in base alle regole esposte in precedenza
Prima di analizzare alcuni esempi pratici, è necessario ricordare che moltiplicando
tra loro due numeri interi a n cifre, si ottiene un risultato avente al massimo
2n cifre; possiamo dire quindi che in relazione ai numeri interi con segno, la
moltiplicazione produce un effetto molto importante che consiste in un cambiamento di
modulo. Nel caso, ad esempio, della nostra CPU, possiamo notare che moltiplicando
tra loro due numeri interi a 5 cifre, otteniamo un risultato a 10 cifre;
per i numeri interi con segno questo significa che stiamo passando dal modulo
100000 al modulo 10000000000. È chiaro quindi che al momento di
aggiungere il segno al risultato della moltiplicazione, la nostra CPU deve
convertire il risultato stesso in un numero intero con segno espresso in modulo
10000000000.
Questa situazione non può mai produrre un overflow; osserviamo, infatti, che in modulo
10000000000 i numeri negativi vanno da -5000000000 a -1 e i
numeri positivi vanno da +0 a +4999999999. Moltiplicando tra loro due
numeri interi con segno a 5 cifre, il massimo risultato positivo ottenibile è:
(-50000) x (-50000) = +2500000000
che è nettamente inferiore a +4999999999.
Moltiplicando tra loro due numeri interi con segno a 5 cifre, il minimo risultato
negativo ottenibile è:
(+49999) x (-50000) = -2499950000
che è nettamente superiore a -5000000000.
Vediamo ora alcuni esempi pratici che hanno anche lo scopo di evidenziare la differenza
esistente tra la moltiplicazione di numeri interi senza segno e la moltiplicazione di
numeri interi con segno; questa differenza dipende proprio dal cambiamento di modulo
provocato da questo operatore matematico.
1) Vogliamo calcolare:
(+33580) x (+41485) = +1393066300
Nell'insieme dei numeri senza segno, si ha la seguente codifica:
33580d x 41485d = 1393066300d
Nell'insieme dei numeri con segno, si ha la seguente codifica:
33580d x 41485d = 1393066300d
I risultati che si ottengono nei due casi sono identici in quanto le due codifiche
33580d e 41485d rappresentano numeri positivi, sia nell'insieme degli
interi senza segno, sia nell'insieme degli interi con segno.
2) Vogliamo calcolare:
(-11100) x (+10000) = -111000000
La codifica numerica di -11100 è 88900d, mentre la codifica numerica
di +10000 è 10000d; nell'insieme dei numeri interi senza segno si
ottiene:
88900d x 10000d = 0889000000d
Nell'insieme dei numeri interi con segno la CPU nota che 88900d è un
numero negativo (-11100) per cui lo converte nel suo equivalente positivo
attraverso la seguente negazione:
100000d - 88900d = 11100d
che è proprio la rappresentazione in complemento a 10 di +11100; a
questo punto la CPU svolge la moltiplicazione ottenendo:
11100d x 10000d = 0111000000d
In base al fatto che meno per più = meno, la CPU deve negare il
risultato ottenendo la rappresentazione in complemento a 10 di -111000000;
in modulo 10000000000 si ha quindi:
10000000000d - 111000000d = 9889000000d
Come si può notare, nell'insieme dei numeri interi senza segno si ottiene
0889000000d, mentre nell'insieme dei numeri interi con segno si ottiene
9889000000d; questi due risultati sono completamente diversi tra loro e
questo significa che la nostra CPU nell'eseguire una moltiplicazione ha bisogno
di sapere se vogliamo operare sui numeri interi senza segno o sui numeri interi con
segno. Questa è una diretta conseguenza del fatto che la moltiplicazione provoca
un cambiamento di modulo; l'addizione e la sottrazione non provocano alcun cambiamento
di modulo, per cui la CPU non ha in questo caso la necessità di distinguere
tra numeri interi senza segno e numeri interi con segno. Nel caso, ad esempio, delle
CPU della famiglia 80x86, l'istruzione per le moltiplicazioni tra
numeri interi senza segno viene chiamata MUL; l'istruzione per le
moltiplicazioni tra numeri interi con segno viene, invece, chiamata IMUL.
Se vogliamo moltiplicare tra loro numeri interi con segno formati da più di 5
cifre, dobbiamo procedere come al solito via software simulando lo stesso procedimento
che si segue con carta e penna; naturalmente in questo caso dobbiamo anche applicare
le considerazioni appena esposte in relazione al cambiamento di modulo. Per poter
svolgere via software questa operazione il programmatore deve quindi scrivere anche le
istruzioni necessarie per la negazione di numeri interi con segno formati da più
di 5 cifre; in un apposito capitolo vedremo un esempio pratico.
3.3.4 Caso particolare per la moltiplicazione tra numeri interi con segno
Abbiamo già visto che per le moltiplicazioni tra numeri interi senza segno si presenta
un caso particolare rappresentato dalla eventualità che uno dei due fattori sia
esprimibile sotto forma di potenza con esponente intero della base 10; tutte
le considerazioni esposte per i numeri interi senza segno si applicano anche ai numeri
interi con segno. Le CPU della famiglia 80x86 forniscono una apposita
istruzione chiamata SAL o shift arithmetic left (scorrimento aritmetico
verso sinistra); è sottinteso che questa istruzione debba essere utilizzata con numeri
interi con segno. Osserviamo però che a causa del fatto che il segno di un numero viene
ricavato dalla sua cifra più significativa (quella più a sinistra), gli effetti
prodotti dall'istruzione SAL sono identici agli effetti prodotti dall'istruzione
SHL per cui queste due istruzioni sono interscambiabili.
Consideriamo, ad esempio, il codice numerico 99325d che per i numeri interi senza
segno rappresenta 99325, mentre per i numeri interi con segno rappresenta
-675; utilizziamo ora l'istruzione SHL per lo scorrimento di un posto
verso sinistra delle cifre di 99325d. In questo modo otteniamo 93250d;
applicando l'istruzione SAL a 99325d si ottiene l'identico risultato.
Per l'istruzione SAL valgono le stesse avvertenze già discusse per SHL;
anche con SAL quindi, con un eccessivo scorrimento di cifre verso sinistra,
si corre il rischio di perdere cifre significative del risultato; nel caso poi dei
numeri interi con segno, si rischia anche di perdere la cifra più significativa
che codifica il segno del numero stesso.
Consideriamo, ad esempio, il codice 99453 che rappresenta -547; se
usiamo SAL (o SHL) per far scorrere di un posto verso sinistra le
cifre di 99453d otteniamo 94530d. In complemento a 10 questa
è la codifica di:
-(100000 - 94530) = -5470 = (-547 x 10)
Il risultato quindi è giusto in quanto abbiamo moltiplicato -547 per
10.
Se usiamo ora SAL per far scorrere di due posti verso sinistra le cifre di
99453d otteniamo 45300d; in complemento a 10 questa è la
codifica di +45300 che non ha niente a che vedere con il risultato che ci
attendevamo.
3.3.5 Divisione tra numeri interi con segno
Come sappiamo dalla matematica, dividendo tra loro due numeri interi con segno si
ottengono un quoziente ed un resto i cui segni vengono determinati con le seguenti
regole:
Osserviamo inoltre che in valore assoluto il quoziente e il resto che si ottengono
sono del tutto indipendenti dal segno; abbiamo, ad esempio:
(+350) / (+200) = [Q = +1, R = +150]
e:
(-350) / (+200) = [Q = -1, R = -150]
In valore assoluto si ottiene Q = +1, R = +150 in entrambi i casi.
In base a queste considerazioni, possiamo dedurre il metodo seguito dalla nostra
CPU per eseguire via hardware divisioni tra numeri interi con segno a 5
cifre (espressi naturalmente in complemento a 10 modulo 100000); le
fasi che vengono svolte sono le seguenti:
- la CPU converte se necessario il dividendo e il divisore in numeri
positivi
- la CPU esegue la divisione tra numeri positivi
- la CPU aggiunge il segno al quoziente e al resto in base alle regole
esposte in precedenza
Prima di analizzare alcuni esempi pratici, è necessario ricordare che la divisione
intera tra numeri interi a n cifre, produce un quoziente intero e un resto
intero aventi al massimo n cifre; nel caso, ad esempio, della nostra CPU,
dividendo tra loro due numeri interi a 5 cifre, otteniamo un quoziente a
5 cifre e un resto a 5 cifre. Se si sta operando sui numeri interi
con segno, bisogna anche tenere presente che, sia il quoziente, sia il resto, devono
essere compresi tra -50000 e +49999; se si ottiene un quoziente minore
di -50000 o maggiore di +49999, la CPU crede di trovarsi nella
stessa situazione della divisione per zero (quoziente troppo grande), per cui il
SO interrompe il nostro programma e visualizza un messaggio di errore. Con
i numeri interi con segno a 5 cifre, questa situazione si verifica nel
seguente caso particolare:
(-50000) / (-1) = [Q = +50000, R = 0]
Per i numeri interi con segno a 5 cifre, +50000 supera il massimo valore
positivo permesso +49999; in tutti gli altri casi, l'overflow è impossibile.
Vediamo ora alcuni esempi pratici che hanno anche lo scopo di evidenziare la differenza
che esiste tra la divisione di numeri interi senza segno e la divisione di numeri interi
con segno; questa differenza dipende dal fatto che, come vedremo in un prossimo capitolo,
la CPU calcola la divisione attraverso un metodo che provoca un cambiamento di
modulo.
1) Vogliamo calcolare:
(+31540) / (+6728) = [Q = +4, R = +4628]
Nell'insieme dei numeri senza segno, si ha la seguente codifica:
31540d / 06728d = [Q = 00004d, R = 04628d]
Nell'insieme dei numeri con segno, si ha la seguente codifica:
31540d / 06728d = [Q = 00004d, R = 04628d]
I risultati che si ottengono nei due casi sono identici in quanto le due codifiche
31540d e 06728d rappresentano numeri positivi, sia nell'insieme degli
interi senza segno, sia nell'insieme degli interi con segno.
2) Vogliamo calcolare:
(-30400) / (+10000) = [Q = -3, R = -400]
La codifica numerica di -30400 è 69600d, mentre la codifica numerica
di +10000 è 10000d; nell'insieme dei numeri interi senza segno si
ottiene:
69600d / 10000d = [Q = 00006d, R = 09600d]
Nell'insieme dei numeri interi con segno la CPU nota che 69600d è un
numero negativo (-30400) per cui lo converte nel suo equivalente positivo
attraverso la seguente negazione:
100000d - 69600d = 30400d
che è proprio la rappresentazione in complemento a 10 di +30400; a
questo punto la CPU svolge la divisione ottenendo:
30400d / 10000d = [Q = 00003d, R = 00400d]
In base al fatto che meno diviso più = quoziente negativo e resto negativo,
la CPU deve negare, sia il quoziente, sia il resto; per il quoziente si ha:
100000d - 00003d = 99997d
Per il resto si ha:
100000d - 00400d = 99600d
Come si può notare, nell'insieme dei numeri interi senza segno si ottiene
Q=00006d e R=09600d, mentre nell'insieme dei numeri interi con segno
si ottiene Q=99997d e R=99600d; questi due risultati sono completamente
diversi tra loro e questo significa che la nostra CPU nell'eseguire una
divisione ha bisogno di sapere se vogliamo operare sui numeri interi senza segno o sui
numeri interi con segno. Nel caso, ad esempio, delle CPU della famiglia
80x86, l'istruzione per le divisioni tra numeri interi senza segno viene
chiamata DIV; l'istruzione per le divisioni tra numeri interi con segno viene,
invece, chiamata IDIV.
Se vogliamo dividere tra loro numeri interi con segno formati da più di 5
cifre, dobbiamo procedere come al solito via software simulando lo stesso procedimento
che si segue con carta e penna; naturalmente, in questo caso dobbiamo anche applicare
le considerazioni appena esposte in relazione al cambiamento di modulo. Per poter
svolgere via software questa operazione il programmatore deve quindi scrivere anche le
istruzioni necessarie per la negazione di numeri interi con segno formati da più
di 5 cifre; in un apposito capitolo vedremo un esempio pratico.
3.3.6 Caso particolare per la divisione tra numeri interi con segno
Abbiamo già visto che per le divisioni tra numeri interi senza segno si presenta
un caso particolare rappresentato dalla eventualità che il divisore sia esprimibile
sotto forma di potenza con esponente intero della base 10; tutte le
considerazioni esposte per i numeri interi senza segno valgono anche per i numeri
interi con segno. Le CPU della famiglia 80x86 forniscono una apposita
istruzione chiamata SAR o shift arithmetic right (scorrimento aritmetico
verso destra); è sottinteso che questa istruzione debba essere utilizzata con numeri
interi con segno.
Questa volta però le due istruzioni SHR e SAR non sono interscambiabili
in quanto producono effetti molto differenti; l'istruzione SAR, infatti, ha il
compito di far scorrere verso destra le cifre di un numero intero con segno, provvedendo
anche a preservare il segno del risultato.
Consideriamo, ad esempio, il codice numerico 00856d che rappresenta il valore
+856, sia per i numeri interi senza segno, sia per i numeri interi con segno;
utilizziamo l'istruzione SHR per lo scorrimento di un posto verso destra delle
cifre di 00856d. In questo modo otteniamo 00085d; come si può notare,
SHR ha diviso per 10 il numero positivo 856 ottenendo correttamente
il quoziente 85.
Utilizziamo ora l'istruzione SAR per lo scorrimento di un posto verso destra
delle cifre di 00856d; anche in questo caso otteniamo 00085d e cioè
il quoziente 85. L'istruzione SAR, infatti, constatando che 00856d
è un numero positivo, ha aggiunto uno zero alla sua sinistra in modo da ottenere un
risultato ancora positivo.
Consideriamo ora il codice numerico 99320d che per i numeri interi senza
segno rappresenta 99320, mentre per i numeri interi con segno rappresenta
-680; utilizziamo l'istruzione SHR per lo scorrimento di un posto verso
destra delle cifre di 99320d. In questo modo otteniamo 09932d; come si
può notare, SHR ha diviso per 10 il numero positivo 99320
ottenendo correttamente il quoziente 9932.
Utilizziamo ora l'istruzione SAR per lo scorrimento di un posto verso destra
delle cifre di 99320d; possiamo così constatare che questa volta si ottiene
99932d che è la codifica di:
-(100000 - 99932) = -68 = (-680) / 10
Anche in questo caso il risultato è corretto in quanto SAR, constatando che
99320d è un numero negativo, ha aggiunto un 9 e non uno 0 alla
sua sinistra in modo da ottenere un risultato ancora negativo; se utilizziamo quindi
SHR con i numeri interi con segno otteniamo risultati privi di senso. I
concetti appena esposti sull'estensione del segno di un numero, vengono chiariti
nel seguito del capitolo.
3.4 Estensione del segno
Sempre in relazione ai numeri interi con segno, è necessario esaminare una situazione
molto delicata che si verifica nel momento in cui abbiamo la necessità di effettuare
un cambiamento di modulo; in sostanza, dobbiamo analizzare quello che succede quando
vogliamo passare da modulo m a modulo n.
Supponiamo, ad esempio, di voler passare da modulo m=100000 a modulo
n=10000000000; vogliamo cioè convertire i numeri interi a 5 cifre in
numeri interi a 10 cifre.
Nel caso dei numeri interi senza segno la soluzione è semplicissima; osserviamo,
infatti, che con 10 cifre possiamo codificare tutti i numeri interi senza
segno compresi tra 0 e +9999999999; otteniamo quindi le
corrispondenze mostrate in Figura 3.15:
Possiamo dire quindi che nel passaggio da modulo m=100000 a modulo
n=10000000000, il codice 00000d diventa 0000000000d, il codice
00005d diventa 0000000005d, il codice 03850d diventa
0000003850d, il codice 88981d diventa 0000088981d e così via;
in sostanza, i codici a 5 cifre mostrati in Figura 3.1 vengono convertiti in
codici a 10 cifre aggiungendo 5 zeri alla sinistra di ciascuno di essi.
Passiamo ora al caso più delicato dei numeri interi con segno; come si può
facilmente intuire, in questo caso non dobbiamo fare altro che estendere al modulo
n=10000000000 tutti i concetti già esposti per il modulo m=100000.
Prima di tutto dividiamo i 1010 possibili codici numerici in due
parti uguali; a questo punto, con i codici compresi tra 0000000000d e
4999999999d possiamo rappresentare tutti i numeri interi positivi compresi tra
0 e +4999999999. Con i codici numerici compresi tra 5000000000d
e 9999999999d possiamo rappresentare tutti i numeri interi negativi compresi
tra -5000000000 e -1; le corrispondenze che si ottengono vengono mostrate
in Figura 3.16:
Possiamo constatare quindi che nel passaggio da modulo m=100000 a modulo
n=10000000000, i codici numerici a 5 cifre compresi tra 00000d
e 49999d (che rappresentano numeri interi positivi), vengono trasformati nei
corrispondenti codici numerici compresi tra 0000000000d e 0000049999d;
questo significa che tutti i numeri interi positivi a 5 cifre vengono
convertiti in numeri interi positivi a 10 cifre aggiungendo 00000 alla
loro sinistra.
Possiamo constatare inoltre che nel passaggio da modulo m=100000 a modulo
n=10000000000, i codici numerici a 5 cifre compresi tra 50000d
e 99999d (che rappresentano numeri interi negativi), vengono trasformati nei
corrispondenti codici numerici compresi tra 9999950000d e 9999999999d;
questo significa che tutti i numeri interi negativi a 5 cifre vengono
convertiti in numeri interi negativi a 10 cifre aggiungendo 99999
alla loro sinistra.
Queste importantissime considerazioni rappresentano il concetto fondamentale di
estensione del segno di un numero intero con segno; tutte le cose appena
viste possono essere dimostrate anche con il complemento a 10 applicato
ai numeri interi con segno in modulo 10000000000.
Il numero negativo -3 (99997d) in modulo 10000000000 diventa:
10000000000 - 3 = 9999999997d
Il numero negativo -450 (99550d) in modulo 10000000000 diventa:
10000000000 - 450 = 9999999550d
Il numero negativo -10998 (89002d) in modulo 10000000000 diventa:
10000000000 - 10998 = 9999989002d
Il numero negativo -50000 (50000d) in modulo 10000000000 diventa:
10000000000 - 50000 = 9999950000d
L'aritmetica modulare permette quindi alla CPU di estendere facilmente il segno
di un numero intero con segno; nel caso di un numero intero positivo l'estensione del
segno consiste nell'aggiunta di zeri alla sinistra del numero stesso, mentre nel caso
di un numero intero negativo l'estensione del segno consiste nell'aggiunta di una
serie di 9 alla sinistra del numero stesso.
Un'ultima considerazione riguarda il fatto che nel cambiamento di modulo generalmente
ha senso solo il passaggio da modulo m a modulo n con n maggiore
di m (che è il caso appena esaminato); infatti, se n è minore di
m, si può verificare una perdita di cifre significative nella rappresentazione
dei numeri. Supponiamo, ad esempio, di lavorare in modulo m=10000000000 e di voler
passare al modulo n=100000; in modulo m, il numero negativo
-1458563459 si codifica come:
10000000000 - 1458563459 = 8541436541d
Per convertire questo codice numerico in modulo n=100000, dovremmo troncare
le sue 5 cifre più significative ottenendo così il codice 36541d;
ma in modulo n=100000 questa è la codifica del numero positivo +36541.
Come si può notare, non solo abbiamo perso 5 cifre significative del numero
originario, ma siamo anche passati da un numero negativo ad un numero positivo; questo
è proprio quello che può succedere con i linguaggi di alto livello come il C
quando, ad esempio, nelle architetture a 16 bit si copia il contenuto di un
long int (intero con segno a 32 cifre) in uno short int (intero
con segno a 16 cifre).
3.5 Equivalenza tra le quattro operazioni
Supponiamo di avere una CPU capace di eseguire solamente addizioni, negazioni e
scorrimenti di cifre sui numeri interi; una CPU di questo genere è perfettamente
in grado di eseguire anche sottrazioni, moltiplicazioni e divisioni.
Cominciamo con l'osservare che, dati due numeri interi A e B positivi o
negativi, possiamo scrivere:
A - B = (+A) + (-B)
In sostanza, la sottrazione tra A e B equivale alla somma tra A
e l'opposto di B; la nostra CPU quindi può trasformare la differenza tra
due numeri nella somma tra il primo numero e la negazione del secondo numero.
In relazione alla moltiplicazione, dati due numeri interi A e B positivi
o negativi, il prodotto tra A e B in valore assoluto è pari ad A
volte B oppure a B volte A; anche la moltiplicazione tra numeri
interi può essere quindi convertita in una serie di addizioni. Nel caso, ad esempio, di
A=3 e B=6 possiamo scrivere:
A x B = 3 x 6 = 3 + 3 + 3 + 3 + 3 + 3 = 6 + 6 + 6 = 18
Se uno o entrambi i fattori sono negativi, la CPU può seguire il metodo già
illustrato in precedenza per la moltiplicazione tra numeri interi con segno; la nostra
CPU può quindi effettuare anche moltiplicazioni tra numeri interi attraverso
una serie di addizioni. Se uno dei fattori (ad esempio, B) può essere espresso
nella forma 10n, abbiamo visto che il prodotto di A per
B consiste nell'aggiungere n zeri alla destra di A, cioè nel far
scorrere le cifre di A di n posti verso sinistra.
In relazione alla divisione, dati due numeri interi A e B positivi
o negativi (con B diverso da zero), il quoziente tra A e B in
valore assoluto è pari a quante volte B è interamente contenuto in A
(sottrazioni successive); il resto della divisione in valore assoluto è la parte di
A che rimane dopo l'ultima sottrazione. Nel caso, ad esempio, di A=350 e
B=100, possiamo scrivere:
In sostanza B=100 può essere sottratto 3 volte da A, per cui
Q=3; alla fine rimane 50 che rappresenta il resto R. Se uno o
entrambi i numeri A e B sono negativi, la CPU può seguire il
metodo già illustrato in precedenza per la divisione tra numeri interi con segno; la
nostra CPU può quindi effettuare anche divisioni tra numeri interi attraverso
una serie di sottrazioni che a loro volta possono essere convertite in addizioni. Se il
divisore B può essere espresso nella forma 10n, abbiamo visto
che il quoziente tra A e B consiste nel togliere n cifre dalla
destra di A, cioè nel far scorrere le cifre di A di n posti verso
destra; le n cifre che escono da destra rappresentano il resto della divisione.
Se A è un intero positivo, nello scorrimento delle sue cifre verso destra
bisogna riempire con zeri i posti rimasti liberi a sinistra (estensione del segno);
se A è un intero negativo, nello scorrimento delle sue cifre verso destra
bisogna riempire con dei 9 i posti rimasti liberi a sinistra (estensione del
segno).
La possibilità di convertire in addizioni le sottrazioni, le moltiplicazioni e le
divisioni, comporta enormi semplificazioni circuitali per le CPU; questo è
proprio quello che accade nella realtà. Infatti, i circuiti elettronici che devono
effettuare moltiplicazioni e divisioni, eseguono in realtà una serie di somme, di
negazioni e di scorrimenti di cifre.
3.6 Numeri reali
Da quanto abbiamo visto in questo capitolo, la CPU è in grado di gestire via
hardware solo ed esclusivamente numeri interi con o senza segno; molto spesso però si
ha la necessità di eseguire dei calcoli piuttosto precisi su numeri con la virgola
(numeri reali). In questo caso si presentano due possibilità: o si scrive un apposito
software capace di permettere alla CPU di maneggiare i numeri reali, oppure si
ricorre al cosiddetto coprocessore matematico.
La gestione via software dei numeri reali e soprattutto la gestione delle operazioni
che si possono eseguire sui numeri reali, comporta l'utilizzo di algoritmi piuttosto
complessi; questa complessità si ripercuote negativamente sulle prestazioni della
CPU.
Fortunatamente, tutte le CPU 80486DX e superiori, contengono al loro interno
anche il coprocessore matematico; questo dispositivo viene anche chiamato FPU
o Fast Processing Unit (unità di elaborazione veloce). La FPU permette
di operare via hardware sui numeri reali e mette a disposizione anche una serie di
complesse funzioni matematiche gestibili ad altissima velocità; tra queste funzioni
si possono citare quelle logaritmiche, esponenziali, trigonometriche, etc. Gli algoritmi
che permettono di implementare queste funzioni, si basano principalmente sugli sviluppi
in serie di Taylor e Mc Laurin; come si sa dalla matematica, attraverso
gli sviluppi in serie è possibile approssimare una funzione (anche trascendente), con
un polinomio di grado prestabilito.
La FPU viene trattata in un apposito capitolo della sezione Assembly
Avanzato.
Giunti alla fine di questo capitolo, possiamo dire di aver appurato che sul computer
qualsiasi informazione viene gestita sotto forma di numeri; abbiamo anche visto come
la CPU rappresenta questi numeri e come vengono applicate ad essi le quattro
operazioni matematiche fondamentali. Non abbiamo però ancora risposto ad una domanda:
come fa il computer a sapere che cosa è un numero?
Non è ancora tempo per dare una risposta perché nel prossimo capitolo procederemo ad
effettuare ulteriori (e colossali) semplificazioni relative sempre alla codifica numerica
dell'informazione sul computer.