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: 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: 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.