Assembly Base con NASM

Capitolo 4: Il sistema di numerazione binario


Nei precedenti capitoli abbiamo appurato che qualsiasi informazione da elaborare attraverso il computer, deve essere codificata in forma numerica; abbiamo anche visto che i codici numerici usati dal computer sono costituiti da numeri interi basati sul sistema di numerazione posizionale. Nel Capitolo 3, in particolare, è stato descritto il funzionamento di un computer immaginario che lavora con un sistema di numerazione posizionale in base b=10, costituito quindi dal seguente insieme di 10 simboli:
S = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
Questo computer immaginario ha solamente uno scopo didattico in quanto il suo modo di funzionare appare semplicissimo da capire; se provassimo però a realizzare veramente un computer di questo genere, andremmo incontro a notevoli difficoltà legate, in particolare, ad enormi complicazioni circuitali. Un computer "reale", per essere capace di lavorare con un sistema di numerazione in base b=10, dovrebbe avere dei circuiti interni capaci di riconoscere ben 10 simboli differenti; in termini pratici ciò si ottiene attraverso un segnale elettrico capace di assumere 10 diversi livelli di tensione. Come vedremo nel prossimo capitolo, una situazione di questo genere creerebbe anche una serie di problemi di carattere tecnico; possiamo dire quindi che l'idea di realizzare in pratica un computer capace di lavorare in base b=10 è assolutamente da scartare.
Il problema appena descritto può essere risolto in modo piuttosto efficace sfruttando una delle tante potentissime caratteristiche dei sistemi di numerazione posizionali; nel nostro caso, la caratteristica che possiamo sfruttare consiste nel fatto che nei sistemi di numerazione posizionali, il numero di simboli (b) da utilizzare è del tutto arbitrario. Chiaramente, noi esseri umani troviamo naturale contare in base b=10 perché così ci è stato insegnato sin da bambini; per questo motivo, l'idea di metterci a contare utilizzando, ad esempio, una base b=6, ci appare abbastanza assurda. In realtà, non ci vuole molto a capire che tutti i concetti sulla matematica del computer esposti nel precedente capitolo, si applicano ad un sistema di numerazione posizionale in base b qualunque.

4.1 Sistema di numerazione posizionale in base b = 6

Per dimostrare che le proprietà matematiche di un sistema di numerazione posizionale sono indipendenti dalla base b utilizzata, analizziamo quello che succede nel caso, ad esempio, di b=6; in base b=6 ci troviamo ad operare con il seguente insieme di 6 simboli:
S = {0, 1, 2, 3, 4, 5}
Si tratta dei soliti simboli utilizzati nel mondo occidentale per la rappresentazione delle cifre; è importante però tenere presente che questi simboli possono essere scelti in modo arbitrario. A titolo di curiosità, la Figura 4.1 mostra i simboli utilizzati per rappresentare le cifre nelle diverse aree linguistiche del mondo. Se ora proviamo a contare in base b=6, arrivati a 5(6) ci troviamo subito in difficoltà; se pensiamo però ai concetti esposti nel Capitolo 2, ci rendiamo subito conto che, indipendentemente dagli aspetti formali, la situazione è sostanzialmente identica al caso della base b=10.
Anche nel caso della base 6 quindi, i sei simboli usati rappresentano le cosiddette unità (zero unità, una unità, due unità, ... , cinque unità); aggiungendo una unità a cinque unità non sappiamo come rappresentare questo numero perché non ci sono più simboli disponibili. Passiamo allora ai numeri a due cifre scrivendo 10(6); in base 10 questo numero rappresenta zero unità più un gruppo di dieci unità, mentre in base 6 rappresenta zero unità più un gruppo di sei unità.
Con i numeri a due cifre possiamo arrivare sino a 55(6) (cinque unità più cinque gruppi di sei unità); aggiungendo una unità a 55(6) unità ci imbattiamo nella situazione precedente. Introduciamo allora i numeri a tre cifre scrivendo 100(6); in base 10 questo numero rappresenta zero unità più zero gruppi di dieci unità più un gruppo di cento unità (dieci per dieci), mentre in base 6 rappresenta zero unità più zero gruppi di sei unità più un gruppo di trentasei unità (sei per sei).
Con i numeri a tre cifre possiamo arrivare sino a 555(6) (cinque unità più cinque gruppi di sei unità più cinque gruppi di trentasei unità); aggiungendo una unità a 555(6) unità ci imbattiamo ancora nella situazione precedente. Introduciamo allora i numeri a quattro cifre scrivendo 1000(6); in base 10 questo numero rappresenta zero unità più zero gruppi di dieci unità più zero gruppi di cento unità più un gruppo di mille unità (dieci per dieci per dieci), mentre in base 6 rappresenta zero unità più zero gruppi di sei unità più zero gruppi di trentasei unità più un gruppo di duecentosedici unità (sei per sei per sei).
Come si può notare, si tratta dell'identico meccanismo utilizzato per contare in base 10; si tenga presente che in basi diverse da 10, i numeri vengono letti scandendo le loro cifre una per una da sinistra a destra (in base 6, ad esempio, il numero 300(6) non deve essere letto come "trecento" ma come "tre zero zero").

Riprendiamo ora un esempio del Capitolo 2 e consideriamo il numero 333(6) in base b=6; il numero 333(6) è formato da tre cifre uguali ma il loro peso è differente. Osserviamo, infatti, che il 3 di destra rappresenta tre unità, il 3 centrale rappresenta tre gruppi di sei unità, mentre il 3 di sinistra rappresenta tre gruppi di trentasei unità; possiamo scrivere allora: Come già sappiamo, dato un numero n in base b formato da i cifre aventi posizione:
p = 0, 1, 2, ... i - 1
(a partire dalla cifra più a destra), si definisce peso di una cifra di n la potenza di ordine p della base b; dato allora il numero 333(6) in base b=6: Naturalmente, bisogna ricordarsi che stiamo operando in base b=6, per cui il numero 333(6) rappresenta tre unità più tre gruppi di sei unità più un gruppo di trentasei unità; questo numero quindi va letto "tre tre tre". Convertendo 333(6) in base 10 otteniamo:
3 * 62 + 3 * 61 + 3 * 60 = 129
A questo punto possiamo anche effettuare operazioni matematiche sui numeri in base 6; a tale proposito, ci può essere utile la tabellina delle addizioni in base 6 mostrata in Figura 4.2 (tutti i numeri si intendono espressi in base 6). Con l'ausilio di questa tabella proviamo ora ad effettuare la seguente addizione: Partendo allora dalla colonna delle unità, in base alla tabella di Figura 4.2 si ottiene:
2 unità + 5 unità = 11 unità
cioè 1 unità con riporto di un gruppo di 6 unità che verrà aggiunto alla seconda colonna (che è la colonna dei gruppi di 6 unità).
Passando alla seconda colonna otteniamo:
3 gruppi di 6 unità + 2 gruppi di 6 unità = 5 gruppi di 6 unità
più un gruppo di 6 unità di riporto = 10 gruppi di 6 unità, cioè 0 gruppi di 6 unità con riporto di un gruppo di 36 unità che verrà aggiunto alla terza colonna (che è la colonna dei gruppi di 36 unità).
Passando infine alla terza colonna otteniamo:
1 gruppo di 36 unità + 3 gruppi di 36 unità = 4 gruppi di 36 unità
più un gruppo di 36 unità di riporto = 5 gruppi di 36 unità; il risultato finale sarà quindi 501(6).
Come verifica osserviamo che in base 10 il primo addendo 132(6) diventa:
1 * 62 + 3 * 61 + 2 * 60 = 36 + 18 + 2 = 56
Il secondo addendo 325(6) diventa:
3 * 62 + 2 * 61 + 5 * 60 = 108 + 12 + 5 = 125
La somma 501(6) diventa:
5 * 62 + 0 * 61 + 1 * 60 = 180 + 0 + 1 = 181
Ripetendo la somma in base 10 otteniamo proprio:
56 + 125 = 181
Tutto ciò dimostra che le differenze tra i numeri espressi in basi diverse, sono solamente di carattere formale; la sostanza, invece, è sempre la stessa. In definitiva, tutto ciò che viene fatto in base 10 può essere fatto in qualsiasi altra base ottenendo risultati perfettamente equivalenti.

4.2 Sistema di numerazione posizionale in base b = 2

Abbiamo appurato quindi che le proprietà matematiche dei sistemi di numerazione posizionali sono assolutamente indipendenti dalla base b che si vuole utilizzare; possiamo sfruttare allora questa caratteristica per realizzare un computer con la massima semplificazione circuitale possibile.
Come è stato detto in precedenza, i diversi simboli che compongono un sistema di numerazione posizionale vengono gestiti dai circuiti del computer sotto forma di diversi livelli di tensione assunti da un segnale elettrico; semplificare al massimo i circuiti del computer significa ridurre al minimo il numero di simboli da gestire e ciò si ottiene attraverso l'adozione di un sistema di numerazione posizionale caratterizzato dalla minima base b possibile.
Scartando i casi come b=0 e b=1, che non hanno alcun senso, non ci resta che fissare la nostra attenzione sul sistema di numerazione posizionale in base b=2; in questo caso ci troviamo ad operare con il seguente insieme formato da due soli simboli:
S = {0, 1}
Per indicare il fatto che questo sistema di numerazione è basato su due soli simboli, si parla anche di sistema binario; ciascuna cifra (0 o 1) del sistema binario viene chiamata bit (contrazione di binary digit = cifra binaria). Possiamo dire quindi che il bit rappresenta l'unità di informazione, cioè la quantità minima di informazione gestibile dal computer.

Proviamo ora a contare in base 2 applicando le regole che già conosciamo; anche in questo caso quindi i due simboli usati rappresentano le cosiddette unità (zero unità, una unità). Dopo aver scandito la sequenza 0(2), 1(2), aggiungendo una unità a una unità non sappiamo come rappresentare questo numero perché non ci sono più simboli disponibili; passiamo allora ai numeri a due cifre scrivendo 10(2), cioè zero unità più un gruppo di due unità.
Con i numeri a due cifre possiamo arrivare sino a 11(2) (una unità più un gruppo di due unità); aggiungendo una unità a 11(2) unità ci imbattiamo nella situazione precedente. Introduciamo allora i numeri a tre cifre scrivendo 100(2), cioè zero unità più zero gruppi di due unità più un gruppo di quattro unità (due per due).
Con i numeri a tre cifre possiamo arrivare sino a 111(2) (una unità più un gruppo di due unità più un gruppo di quattro unità); aggiungendo una unità a 111(2) unità ci imbattiamo ancora nella situazione precedente. Introduciamo allora i numeri a quattro cifre scrivendo 1000(2), cioè zero unità più zero gruppi di due unità più zero gruppi di quattro unità più un gruppo di otto unità (due per due per due).

Consideriamo ora il numero 1111(2) in base b=2; questo numero è formato da quattro cifre uguali che hanno però peso differente. Applicando, infatti, le proprietà che già conosciamo sui sistemi di numerazione posizionali, possiamo scrivere: Segue quindi che: In base b=2 un numero come 1111(2) rappresenta una unità più un gruppo di due unità più un gruppo di quattro unità più un gruppo di otto unità; questo numero quindi va letto "uno uno uno uno".
Convertendo 1111(2) in base 10 otteniamo:
1 * 23 + 1 * 22 + 1 * 21 + 1 * 20 = 15
La cifra più a destra di un numero binario è quella di peso minore e viene chiamata LSB o least significant bit (bit meno significativo); la cifra più a sinistra di un numero binario è quella di peso maggiore e viene chiamata MSB o most significant bit (bit più significativo).

Si può constatare che più è piccola la base b, più aumenta il numero di cifre necessarie per rappresentare uno stesso numero; ad esempio, il numero 318 espresso in base 10 con tre sole cifre, diventa 1250(6) in base 6 (quattro cifre) e 100111110(2) in base 2 (nove cifre). Non c'è dubbio quindi che un essere umano incontrerebbe notevoli difficoltà nell'uso del sistema binario; nel caso del computer, invece, i due soli simboli 0 e 1 vengono gestiti elettronicamente a velocità vertiginose.

Un altro aspetto particolarmente interessante che, ovviamente, deriva dalle proprietà dei sistemi di numerazione posizionali, riguarda il fatto che in qualunque base b, la base stessa si rappresenta sempre come 10(b); infatti: e così via.

Una volta introdotto il sistema di numerazione binario, è facile capire a cosa si riferisce la definizione di CPU con architettura a 8 bit, a 16 bit, a 32 bit etc; si tratta ovviamente del numero massimo di cifre binarie che la CPU può gestire direttamente (via hardware). Tra i vari esempi reali si possono citare le CPU 8080 a 8 bit, l'8086 a 16 bit, l'80386/80486 a 32 bit e le CPU di classe AMD64/Intel64 a 64 bit.
Una CPU con architettura a 8 bit è in grado di gestire via hardware solo numeri a 8 cifre binarie; se vogliamo lavorare, invece, con numeri formati da più di 8 cifre, dobbiamo procedere come al solito via software suddividendo i numeri stessi in gruppi di 8 cifre.
Una CPU con architettura a 16 bit è in grado di gestire via hardware numeri a 8 e a 16 cifre binarie; se vogliamo lavorare, invece, con numeri formati da più di 16 cifre, dobbiamo procedere via software suddividendo i numeri stessi in gruppi di 8 o 16 cifre.
Una CPU con architettura a 32 bit è in grado di gestire via hardware numeri a 8, a 16 e a 32 cifre binarie; se vogliamo lavorare, invece, con numeri formati da più di 32 cifre, dobbiamo procedere via software suddividendo i numeri stessi in gruppi di 8, 16 o 32 cifre.

Come mai si usano proprio questi strani numeri come 8, 16, 32, 64 etc?
Il perché di questa scelta è legato al fatto che stiamo lavorando in base 2 e, come vedremo anche in seguito, utilizzando in base 2 numeri che si possono esprimere sotto forma di potenza intera del due, si ottengono enormi vantaggi, sia in termini di hardware, sia in termini di software. Per il momento possiamo limitarci ad osservare che:
8 = 23, 16 = 24, 32 = 25, 64 = 26, etc.
Possiamo affermare allora che nel mondo del computer il numero perfetto non è il 3 ma il 2!

A questo punto siamo finalmente in grado di convertire in base 2 tutte le considerazioni esposte nel precedente capitolo relativamente al nostro computer immaginario che lavora in base 10; in questo modo abbiamo la possibilità di constatare che l'adozione del sistema di numerazione posizionale in base 2, comporta enormi semplificazioni nei calcoli matematici, che si traducono naturalmente nella semplificazione dei circuiti che eseguono i calcoli stessi.
Prima di tutto è necessario introdurre alcune convenzioni utilizzate nel mondo dei computer; la prima cosa da dire riguarda il fatto che, per velocizzare le operazioni, i computer gestiscono le informazioni sotto forma di gruppi di bit. Come al solito, il numero di bit contenuti in ogni gruppo non viene scelto a caso, ma è sempre una potenza intera del 2; la Figura 4.3 mostra le convenzioni adottate sulle piattaforme hardware basate sulle CPU della famiglia 80x86. La Figura 4.4 illustra altre convenzioni legate ai multipli del byte. Notiamo subito che, mentre in campo scientifico (sistema internazionale di misura S.I.) i prefissi kilo, mega, giga e tera moltiplicano l'unità di misura a cui vengono applicati, rispettivamente, per mille, per un milione, per un miliardo e per mille miliardi, in campo informatico il discorso è completamente diverso; infatti, questa volta: e così via. In base alla Figura 4.5, il kilobyte diventa kibibyte (simbolo KiB); analogamente, il terabyte diventa tebibyte (simbolo TiB) e così via. Si tenga presente che B è l'abbreviazione di byte, mentre b è l'abbreviazione di bit; inoltre, il simbolo del prefisso kilo è k minuscolo e non K (che è il simbolo del grado Kelvin).
Purtroppo, la confusione regna sovrana e buona parte del mondo informatico continua ad utilizzare i prefissi kilo, mega, giga, etc; in questi tutorial cercheremo di attenerci alle convenzioni IEC di Figura 4.5.

4.3 Numeri interi senza segno

Cominciamo allora a lavorare con i numeri binari riferendoci per semplicità ad un computer con architettura a 8 bit; tutti i concetti che verranno esposti in relazione ai numeri binari a 8 bit si possono estendere facilmente ai numeri binari formati da un numero qualsiasi di bit. Da questo momento in poi, per evitare di fare confusione con i numeri espressi in base 10, tutti i numeri binari verranno rappresentati con il suffisso 'b'; questa è la stessa convenzione adottata dal linguaggio Assembly.

Questa volta il nostro contachilometri è formato da 8 cifre e conta in base 2; partendo allora da 00000000b e procedendo in marcia avanti, vedremo comparire in sequenza i numeri binari 00000000b, 00000001b, 00000010b, 00000011b, 00000100b e così via sino a 11111111b che in base 10 rappresenta il numero 255 (28-1). Se ora percorriamo un altro km, il contachilometri ricomincerà da 00000000b; come ormai già sappiamo, questo accade perché il contachilometri lavora in modulo bn. Nel nostro caso b=2 e n=8, per cui il modulo che stiamo utilizzando è 28 = 256 che in binario si rappresenta come 100000000b; con 8 bit complessivamente possiamo rappresentare 256 codici numerici differenti attraverso i quali possiamo codificare i primi 256 numeri dell'insieme ottenendo le corrispondenze mostrate in Figura 4.6. Attraverso questo sistema di codifica, la nostra CPU può gestire via hardware un sottoinsieme finito di formato quindi dai seguenti 256 numeri naturali:
0, 1, 2, 3, ..., 252, 253, 254, 255

4.3.1 Addizione tra numeri interi senza segno

Vediamo innanzi tutto in Figura 4.7 la tabellina per le addizioni binarie tra singoli bit. Come si può notare, l'unico caso degno di nota è l'ultimo dove sommando 1+1 si ottiene 10b, cioè 0 con riporto di 1; è del tutto superfluo ricordare che in qualsiasi base il riporto massimo (per le addizioni) e il prestito massimo (per le sottrazioni) non può superare 1.
La nostra CPU può sommare via hardware numeri binari a 8 bit; il valore massimo senza segno ottenibile con 8 bit è 255, per cui la CPU porrà CF=1 solo se la somma tra due numeri interi senza segno fornisce un risultato superiore a 255 (11111111b).
In Figura 4.8 vediamo alcuni esempi che a questo punto dovrebbero essere ormai superflui perché anche un bambino sarebbe in grado di effettuare questi calcoli con carta e penna. Se alla fine si ottiene CF=0, significa che il risultato è valido così com'è; in caso contrario dobbiamo aggiungere un 1 alla sinistra del risultato ottenendo quindi un valore a 9 cifre.
Il flag CF può essere utilizzato per sommare via software numeri interi formati da più di 8 bit; le cifre che formano questi numeri devono essere suddivise in gruppi. Come già sappiamo, per ottenere la massima efficienza possibile, conviene fare in modo che ciascun gruppo contenga un numero di cifre pari a quello che rappresenta l'architettura della CPU che si sta utilizzando; nel nostro caso, i numeri formati da più di 8 bit devono essere suddivisi in gruppi di 8 bit a partire da destra. La Figura 4.9 mostra un esempio pratico. (ovviamente, gli zeri a sinistra non sono significativi, per cui 000100111000001011010101b equivale proprio a 100111000001011010101b)

L'ultima addizione ci dà CF=0, per cui il risultato è valido così com'è; in presenza di un riporto finale (CF=1), avremmo dovuto aggiungere un 1 alla sinistra del risultato ottenendo quindi un numero a 25 cifre.

4.3.2 Sottrazione tra numeri interi senza segno

La Figura 4.10 mostra la tabellina per le sottrazioni binarie tra singoli bit. Questa volta l'unico caso degno di nota è il secondo dove la sottrazione 0-1 ci dà come risultato 1 con prestito di 1 dalle colonne successive; per giustificare questa situazione è sufficiente pensare a quello che accade in qualsiasi base b. In generale, il prestito richiesto alle colonne successive dalla colonna in posizione n è pari a bn+1; in base b=2 quindi, se ci troviamo, ad esempio, nella prima colonna (n=0), chiederemo un prestito di 21=2 alle colonne successive, cioè un prestito di 1 gruppo di 2 unità. Ottenuto il prestito possiamo scrivere:
2 - 1 = 10b - 1b = 1b
Questa sottrazione indica che sottraendo 1 unità da un gruppo di 2 unità, si ottiene ovviamente 1 unità.
Se ci troviamo nella seconda colonna (n=1), chiederemo un prestito di 22=4 alle colonne successive, cioè un prestito di 1 gruppo di 4 unità. Ottenuto il prestito possiamo scrivere:
4 - 2 = 100b - 10b = 10b
Questa sottrazione indica che sottraendo 1 gruppo di 2 unità da 1 un gruppo di 4 unità, si ottiene ovviamente 1 gruppo di 2 unità.

La nostra CPU può sottrarre via hardware numeri binari a 8 bit; se il minuendo è maggiore o uguale al sottraendo si ottiene un risultato positivo o nullo con CF=0. Se il minuendo è minore del sottraendo si ottiene CF=1; questo prestito proviene come al solito dai successivi gruppi di 8 bit. La Figura 4.11 mostra alcuni esempi pratici. Se alla fine si ottiene CF=0, vuol dire che il risultato è valido così com'è; in caso contrario la sottrazione si è conclusa con la richiesta di un prestito ai successivi gruppi di 8 bit.
Il flag CF può essere utilizzato per sottrarre via software numeri interi formati da più di 8 bit; le cifre che formano questi numeri devono essere suddivise in gruppi. Nel nostro caso (CPU con architettura a 8 bit), ogni gruppo è formato da 8 cifre; la Figura 4.12 mostra un esempio pratico. (gli zeri a sinistra non sono significativi, per cui 000010000011011011011111b equivale proprio a 10000011011011011111b)

L'ultima sottrazione ci dà CF=0, per cui il risultato è valido così com'è; in presenza di un prestito finale (CF=1), si ottiene un risultato negativo che, come sappiamo, non appartiene all'insieme dei numeri senza segno.

4.3.3 Moltiplicazione tra numeri interi senza segno

La Figura 4.13 mostra la tabellina per le moltiplicazioni binarie tra singoli bit. Applicando i concetti esposti nel precedente capitolo, possiamo dire che moltiplicando tra loro due numeri binari a 8 bit, si ottiene come prodotto un numero binario che non può superare i 16 bit; la CPU moltiplica quindi i due fattori a 8 bit e ci restituisce il risultato a 16 bit disposto in due gruppi da 8 bit ciascuno.
In Figura 4.14 vediamo un esempio pratico che simula lo stesso procedimento che si usa per eseguire la moltiplicazione con carta e penna; questo esempio dimostra chiaramente le notevoli semplificazioni che si ottengono eseguendo la moltiplicazione in base 2. Per verificare questo risultato osserviamo che in base 10 il primo fattore corrisponde a 202, il secondo fattore corrisponde a 156, mentre il prodotto corrisponde a 31512; ricalcolando la moltiplicazione in base 10 otteniamo proprio:
202 x 156 = 31512
Analizzando la Figura 4.14 ci si rende subito conto che la CPU non ha bisogno di eseguire alcuna moltiplicazione; notiamo innanzi tutto la semplicità con la quale si ottengono i prodotti parziali. Gli unici due casi che si possono presentare sono:
11001010b x 0b = 00000000b
e:
11001010b x 1b = 11001010b
In generale, dato un fattore A, si possono presentare solo i seguenti due casi:
A x 0 = 0 e A x 1 = A
Le cifre degli 8 prodotti parziali così ottenuti vengono sottoposte ad uno scorrimento verso sinistra; per il primo prodotto parziale lo scorrimento è di zero posti, per il secondo è di un posto, per il terzo è di due posti e così via. Alla fine i vari prodotti parziali vengono sommati tra loro in modo da ottenere il risultato a 16 bit.
Si tenga presente che queste sono solamente considerazioni di carattere teorico; in realtà le CPU utilizzano algoritmi molto più potenti ed efficienti, che spesso vengono tenuti segreti dalle case costruttrici.

Consideriamo infine il caso particolare che si presenta quando uno dei fattori può essere posto nella forma 2n; dato, ad esempio, il numero binario 00001110b, possiamo osservare che: In sostanza, moltiplicare un numero binario A (intero senza segno) per 2n significa aggiungere n zeri alla destra di A; tutto ciò equivale a far scorrere di n posti verso sinistra tutte le cifre di A, riempiendo con zeri gli n posti che si liberano a destra. Questa proprietà ci permette di velocizzare enormemente la moltiplicazione; osserviamo, infatti, che in questo caso la CPU evita di effettuare tutti i calcoli visibili in Figura 4.14.
Come al solito, un valore eccessivo di n può provocare il trabocco da sinistra di cifre significative del risultato; ad esempio, un qualunque numero binario a 8 bit, dopo 8 scorrimenti verso sinistra diventa in ogni caso 00000000b!

4.3.4 Divisione tra numeri interi senza segno

La Figura 4.15 mostra la tabellina per le divisioni binarie tra singoli bit; chiaramente, sono proibiti tutti casi per i quali il divisore è 0. Applicando i concetti esposti nel precedente capitolo, possiamo dire che dividendo tra loro due numeri binari a 8 bit, si ottiene un quoziente non superiore al dividendo e un resto non superiore al divisore; la CPU esegue quindi la divisione intera e ci restituisce un quoziente a 8 bit e un resto a 8 bit.
In Figura 4.16 vediamo un esempio pratico che simula lo stesso procedimento che si usa per eseguire le divisioni con carta e penna. La divisione ci fornisce Q=1110b e R=0110b; in base 10 il dividendo diventa 230, il divisore diventa 16, il quoziente diventa 14 e il resto diventa 6. Ricalcolando la divisione in base 10 otteniamo, infatti:
230 / 16 = [Q = 14, R = 6]
Analizzando la Figura 4.16 ci si rende subito conto che la CPU non ha bisogno di eseguire alcuna divisione; tutto si svolge, infatti, attraverso confronti tra numeri binari, sottrazioni e scorrimenti di cifre.
Se il dividendo A è minore del divisore B, la CPU ci restituisce direttamente Q=0 e R=A; se il dividendo A è uguale al divisore B, la CPU ci restituisce direttamente Q=1 e R=0. Se il dividendo è maggiore del divisore, viene applicato il metodo mostrato in Figura 4.16; come si può notare, si procede esattamente come per la base 10.
Anche in questo caso però si tratta di considerazioni di carattere teorico; in realtà le CPU eseguono la divisione utilizzando algoritmi molto più efficienti. Nei capitoli successivi verrà mostrata un'altra tecnica che permette alla CPU di eseguire le divisioni molto più rapidamente; in ogni caso, appare chiaro il fatto che le divisioni e le moltiplicazioni vengono eseguite dalla CPU molto più lentamente rispetto alle addizioni e alle sottrazioni.

Consideriamo infine il caso particolare che si presenta quando il divisore può essere posto nella forma 2n; dato, ad esempio, il numero binario 01110000b, possiamo osservare che: In sostanza, dividere un numero binario A (intero senza segno) per 2n significa aggiungere n zeri alla sinistra di A; tutto ciò equivale a far scorrere di n posti verso destra tutte le cifre di A, riempiendo con zeri gli n posti che si liberano a sinistra. Questa proprietà ci permette di velocizzare enormemente la divisione; osserviamo, infatti, che in questo caso la CPU evita di effettuare tutti i calcoli visibili in Figura 4.16.
Nella divisione di A per 2n, le n cifre meno significative di A traboccano da destra; come abbiamo visto nel precedente capitolo, queste n cifre formano il resto della divisione intera.

4.4 Numeri interi con segno

Come già sappiamo, con 8 bit possiamo rappresentare 28=256 diversi codici numerici; in analogia a quanto è stato fatto per la base 10, suddividiamo questi 256 codici in due gruppi da 128. Osserviamo ora che, con il nostro contachilometri binario a 8 cifre, partendo da 00000000b e procedendo in marcia avanti, vedremo comparire la sequenza:
00000000b, 00000001b, 00000010b, 00000011b, 00000100b, ...
Partendo sempre da 00000000b e procedendo in marcia indietro, vedremo comparire la sequenza:
11111111b, 11111110b, 11111101b, 11111100b, 11111011b, ...
Possiamo dire quindi che tutti i codici da 0 a 127 rappresentano i numeri interi positivi compresi tra +0 e +127; tutti i codici da 128 a 255 rappresentano i numeri interi negativi compresi tra -128 e -1. Si ottengono in questo modo le corrispondenze mostrate in Figura 4.17. La Figura 4.17 evidenzia una delle tante semplificazioni legate all'uso del sistema binario; osserviamo, infatti, che tutti i numeri interi positivi hanno il MSB che vale 0, mentre tutti i numeri interi negativi hanno il MSB che vale 1. Questa situazione permette alla CPU di individuare con estrema facilità il segno di un numero.

4.4.1 Complemento a 1 e complemento a 2

Il nostro contachilometri binario a 8 cifre lavora in modulo 28=256 (che in binario si scrive 100000000b); ogni volta che superiamo 11111111b, il contachilometri ricomincia a contare da 00000000b. Possiamo dire quindi che nell'aritmetica modulare del nostro contachilometri, 00000000b equivale a 100000000b (cioè 0 equivale a 256); possiamo scrivere quindi: Con questa tecnica possiamo ricavare facilmente la codifica binaria di un numero intero negativo; dato un numero intero positivo n compreso tra +1 e +128, la codifica binaria del corrispondente numero negativo (-n) si ottiene dalla formula:
100000000b - n = 256 - n
Nell'eseguire questa sottrazione con carta e penna, possiamo evitare il fastidio di eventuali prestiti osservando che:
100000000b - n = (11111111b + 1b) - n = (11111111b - n) + 1b
Vediamo un esempio pratico relativo al numero positivo 00000011b (+3); per ricavare il corrispondente numero negativo -3, possiamo scrivere:
(11111111b - 00000011b) + 1b = 11111100b + 1 = 11111101b
Attraverso la Figura 4.17 possiamo constatare che 11111101b è proprio la codifica binaria del numero negativo -3.
Analizzando il calcolo appena effettuato, ci accorgiamo che la prima sottrazione non fa altro che invertire tutti i bit del numero n; in sostanza, tutti i bit che valgono 0 diventano 1 e tutti i bit che valgono 1 diventano 0. L'operazione che consiste nell'inversione di tutti i bit di un numero binario, prende il nome di complemento a 1; per svolgere questa operazione, tutte le CPU della famiglia 80x86 forniscono una apposita istruzione chiamata NOT. La successiva somma di NOT(n) con 1b ci fornisce l'opposto del numero n, cioè il numero n cambiato di segno; nel complesso, l'operazione che porta al cambiamento di segno di un numero binario n prende il nome di complemento a 2. Per svolgere questa operazione, tutte le CPU della famiglia 80x86 forniscono una apposita istruzione chiamata NEG; in base alle considerazioni appena esposte possiamo dire che:
NEG(n) = NOT(n) + 1
L'utilizzo del sistema binario permette quindi alla CPU di negare (cambiare di segno) facilmente un numero n; la CPU non deve fare altro che invertire tutti i bit di n e sommare 1 al risultato ottenuto.
Provando, ad esempio, a negare 11111101b (-3) dovremmo ottenere:
-(-3) = +3 = 00000011b
Infatti:
(11111111b - 11111101b) + 1b = 00000010b + 1b = 00000011b = +3
In definitiva, con la tecnica appena vista la nostra CPU può gestire via hardware tutti i numeri interi con segno che formano il seguente sottoinsieme finito di :
-128, -127, ..., -3, -2, -1, +0, +1, +2, ..., +126, +127
Il sistema di codifica binaria di questi numeri prende il nome di rappresentazione in complemento a 2 dei numeri interi con segno in modulo 256.

4.4.2 Addizione e sottrazione tra numeri interi con segno

Consideriamo innanzi tutto i due codici 00111100b e 10111100b; trattando questi codici come numeri interi senza segno, otteniamo:
00111100b + 10111100b = (+60) + (+188) = +248 = 11111000b
Trattando questi codici come numeri interi con segno, otteniamo:
00111100b + 10111100b = (+60) + (-68) = -8 = 11111000b
Si conferma quindi il fatto che nell'eseguire addizioni e sottrazioni la CPU non ha bisogno di dover distinguere tra numeri interi con o senza segno; avremo quindi un'unica istruzione (ADD) per le addizioni e un'unica istruzione (SUB) per le sottrazioni.

Se il programmatore ha bisogno di distinguere tra numeri interi con segno e numeri interi senza segno, deve servirsi come al solito dei flags CF, OF e SF; analizziamo innanzi tutto in Figura 4.18 una serie di esempi relativi a numeri binari a 8 bit. Interpretando questi codici come numeri interi senza segno abbiamo:
1) 55 + 30 = 85, 2) 250 + 240 = 234, 3) 120 + 110 = 230, 4) 130 + 140 = 14
Negli esempi 1 e 3 si ha CF=0, per cui il risultato è valido così com'è; negli esempi 2 e 4 si ha CF=1 in quanto il risultato ottenuto supera 255. In questo caso dobbiamo aggiungere un 1 alla sinistra del risultato binario; considerando l'esempio 2 abbiamo, infatti:
111101010b = 490 = 250 + 240
Interpretando i codici numerici di Figura 4.18 come numeri interi con segno, abbiamo:
1) (+55) + (+30) = +85, 2) (-6) + (-16) = -22, 3) (+120) + (+110) = -26, 4) (-126) + (-116) = +14
Negli esempi 1 e 2 siamo rimasti nell'intervallo [-128, + 127] (OF=0), per cui il risultato è valido così com'è; l'esempio 1 produce un risultato positivo (SF=0), mentre l'esempio 2 produce un risultato negativo (SF=1).
Negli esempi 3 e 4 si ha OF=1, in quanto il risultato è andato in overflow; osserviamo, infatti, che nell'esempio 3 sommando due numeri positivi si ottiene un risultato "troppo positivo" che supera +127 e sconfina nell'insieme dei numeri negativi a 8 bit. Analogamente, nell'esempio 4 sommando due numeri negativi si ottiene un risultato "troppo negativo" che scende sotto -128 e sconfina nell'insieme dei numeri positivi a 8 bit; ovviamente, in questi due casi il contenuto di SF è privo di significato!

Se abbiamo a che fare con addizioni e sottrazioni che coinvolgono numeri interi con segno formati da più di 8 bit, dobbiamo procedere via software utilizzando gli algoritmi che già conosciamo; come è stato spiegato nel Capitolo 3, quando si opera con i numeri interi con segno bisogna stabilire in anticipo il modulo che vogliamo utilizzare, in modo da poter ottenere la corretta rappresentazione in complemento a 2 dei numeri stessi.
Supponiamo, ad esempio, di voler operare su numeri interi con segno a 32 bit; in questo caso il modulo è pari a 232. In analogia a quanto abbiamo visto per i numeri interi con segno a 8 bit, i numeri interi positivi saranno rappresentati dalle codifiche comprese tra 00000000000000000000000000000000b e 01111111111111111111111111111111b, mentre i numeri interi negativi saranno rappresentati dalle codifiche comprese tra 10000000000000000000000000000000b e 11111111111111111111111111111111b; proviamo allora ad effettuare la seguente addizione:
(+1029443343) + (-2000000000) = -970556657
La codifica binaria di +1029443343 è 00111101010111000000111100001111b, mentre la codifica binaria di +2000000000 è 01110111001101011001010000000000b; prima di tutto dobbiamo cambiare di segno +2000000000. A tale proposito invertiamo tutti i bit di +2000000000 e sommiamo 1 al risultato ottenendo:
10001000110010100110101111111111b + 1b = 10001000110010100110110000000000b
Come si può notare, il MSB vale 1 e ci indica quindi la codifica di un numero negativo; a questo punto possiamo procedere con l'addizione come viene mostrato in Figura 4.19. Le addizioni relative alle colonne 2, 3 e 4 coinvolgono anche CF per tener conto di eventuali riporti provocati dalle precedenti addizioni; terminata l'ultima addizione (colonna 4) 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 conoscerne il segno; i flags OF e SF vanno consultati solo alla fine perché, come sappiamo, il segno degli addendi si trova codificato nel loro MSB.
Se ora convertiamo in base 10 il risultato dell'addizione, otteniamo 3324410639; per i numeri interi con segno a 32 bit questa è proprio la codifica del numero negativo:
-(232 - 3324410639) = -970556657

4.4.3 Moltiplicazione tra numeri interi con segno

Come abbiamo visto nel precedente capitolo, in presenza di una moltiplicazione tra numeri interi con segno, la CPU memorizza il segno del risultato, converte se necessario i due fattori in numeri positivi, esegue la moltiplicazione e infine applica il segno al risultato; moltiplicando due numeri interi con segno a 8 bit, si ottiene un risultato a 16 bit che verrà restituito dalla CPU in due gruppi da 8 bit.
Abbiamo anche visto che la moltiplicazione provoca un cambiamento di modulo, per cui la CPU ha bisogno di sapere se vogliamo operare sui numeri interi senza segno o sui numeri interi con segno; come già sappiamo, per gestire questi due casi dobbiamo utilizzare le due istruzioni MUL e IMUL.
Supponiamo, ad esempio, di voler eseguire la seguente moltiplicazione:
250 x 120 = 30000
Con la nostra CPU a 8 bit, il numero 250 viene codificato come 11111010b, mentre il numero 120 viene codificato come 01111000b; interpretando questi due codici come numeri interi senza segno, si ottiene:
11111010b x 01111000b = 01110101b 00110000b
Per i numeri interi con segno il codice 11111010b rappresenta il numero negativo -6, per cui la moltiplicazione diventa in questo caso:
(-6) x (+120) = -720
La CPU nega quindi il primo numero ottenendo:
NEG(11111010b) = 00000101b + 1b = 00000110b
che è proprio la codifica binaria di +6. La moltiplicazione binaria produce quindi:
00000110b x 01111000b = 00000010b 11010000b
Il risultato deve essere negativo, per cui la CPU lo nega in modulo 216 ottenendo:
NEG(00000010b 11010000b) = 11111101b 00101111b + 1b = 11111101b 00110000b
Per i numeri interi con segno a 16 bit questa è proprio la codifica binaria di -720; infatti:
216 - 720 = 64816 = 11111101b 00110000b
In definitiva, nell'insieme dei numeri interi senza segno si ottiene 01110101b 00110000b, mentre nell'insieme dei numeri interi con segno si ottiene 11111101b 00110000b; a causa del cambiamento di modulo, i due risultati sono completamente diversi tra loro!

Se uno dei due fattori può essere posto nella forma 2n, possiamo velocizzare la moltiplicazione attraverso l'istruzione SAL; come già sappiamo, a causa del fatto che il segno di un numero è codificato nel suo MSB, il risultato prodotto da SAL è identico a quello prodotto da SHL.
Consideriamo, ad esempio, il numero 00111100b e moltiplichiamolo per 24; applicando SHL otteniamo:
SHL(00111100b, 4) = 11000000b
Applicando SAL otteniamo:
SAL(00111100b, 4) = 11000000b
Le due istruzioni SHL e SAL sono quindi interscambiabili; le CPU della famiglia 80x86 le forniscono entrambe solo per motivi di simmetria con la coppia SHR, SAR.

4.4.4 Divisione tra numeri interi con segno

Anche per la divisione tra numeri interi con segno, abbiamo visto che la CPU converte se necessario il dividendo e il divisore in numeri interi positivi, esegue la divisione intera e applica infine il segno al quoziente e al resto; questa operazione produce un cambiamento di modulo, per cui la CPU ha bisogno di sapere se vogliamo operare sui numeri interi senza segno o su quelli con segno. Per poter gestire questi due casi dobbiamo utilizzare le due apposite istruzioni DIV e IDIV.
Consideriamo, ad esempio, i due numeri 250 e 120 che vengono codificati in binario come 11111010b e 01111000b; trattando questi codici come numeri interi senza segno, si ottiene:
250 / 120 = [Q = 2, R = 10]
In binario si ha quindi:
11111010b / 01111000b = [Q = 00000010b, R = 00001010b]
Trattando, invece, i due codici come numeri interi con segno, si ottiene:
(-6) / (+120) = [Q = 0, R = -6]
In binario la CPU nota che 11111010b è un numero negativo, per cui lo nega ottenendo:
NEG(11111010b) = 00000101b + 1b = 00000110b
che è proprio la rappresentazione di +6; a questo punto viene eseguita la divisione tra numeri positivi:
00000110b / 01111000b = [Q = 00000000, R = 00000110b]
Siccome il resto deve essere negativo, la CPU lo nega ottenendo:
NEG(00000110b) = 11111001b + 1b = 11111010b
che è proprio la rappresentazione a 8 bit di -6.
In definitiva, nell'insieme dei numeri interi senza segno otteniamo Q=00000010b e R=00001010b; nell'insieme dei numeri interi con segno otteniamo, invece, Q=00000000b e R=11111010b. A causa del cambiamento di modulo, i due risultati sono completamente diversi tra loro!

Se il divisore può essere posto nella forma 2n, possiamo velocizzare la divisione attraverso l'istruzione SAR; questa istruzione è completamente diversa da SHR in quanto fa scorrere verso destra le cifre del dividendo preservando il MSB del dividendo stesso.
Consideriamo, ad esempio, il numero 00111101b e dividiamolo per 22; applicando SHR otteniamo:
SHR(00111101b, 2) = 00001111b
Applicando SAR otteniamo:
SAR(00111101b, 2) = 00001111b
I due risultati sono identici in quanto 00111101b è positivo anche nell'insieme dei numeri interi con segno.
Consideriamo ora il numero 10110001b e dividiamolo per 22; applicando SHR otteniamo:
SHR(10110001b, 2) = 00101100b
Applicando SAR otteniamo:
SAR(10110001b, 2) = 11101100b
Questa volta i due risultati sono differenti in quanto 10110001b nell'insieme dei numeri con segno è negativo; l'istruzione SHR aggiunge zeri alla sinistra del numero, mentre SAR tiene conto del segno e aggiunge degli 1 alla sinistra del numero.

4.5 Estensione del segno

Applichiamo ora alla base b=2 i concetti esposti nel precedente capitolo in relazione al cambiamento di modulo per i numeri interi con segno; vediamo, ad esempio, come si deve procedere per convertire numeri interi con segno a 8 bit in numeri interi con segno a 16 bit.
Per effettuare questa conversione non dobbiamo fare altro che applicare tutti i concetti relativi alla rappresentazione in complemento a 2 dei numeri interi con segno a 16 bit; con 16 bit possiamo rappresentare 216=65536 codici numerici (da 0 a 65535). Tutti i codici da 0000000000000000b a 0111111111111111b rappresentano i numeri interi positivi compresi tra +0 e +32767; tutti i codici da 1000000000000000b a 1111111111111111b rappresentano i numeri interi negativi compresi tra -32768 e -1.
Si vede subito allora che, ad esempio, il numero positivo a 8 bit 00111100b diventa 0000000000111100b; analogamente, il numero positivo a 8 bit 01111111b diventa 0000000001111111b.
Il numero negativo a 8 bit 11000111b (-57) diventa 1111111111000111b; infatti:
216 - 57 = 65479 = 1111111111000111b
In sostanza, per prolungare da 8 a 16 bit un numero intero positivo, non dobbiamo fare altro che aggiungere 8 zeri alla sua sinistra; per prolungare da 8 a 16 bit un numero intero negativo, non dobbiamo fare altro che aggiungere 8 uno alla sua sinistra.

4.6 Proprietà generali delle codifiche numeriche a n cifre in base b

Una volta scelta la base b del sistema di numerazione posizionale su cui operare e una volta stabilito il numero massimo di cifre disponibili, restano determinate in modo univoco tutte le caratteristiche dell'insieme numerico che dobbiamo codificare; in particolare, possiamo ricavare una serie di proprietà generali relative ai limiti inferiore e superiore dei numeri rappresentabili.
Nel precedente capitolo abbiamo avuto a che fare con un computer immaginario che lavora con codici numerici a 5 cifre espressi in base b=10; la Figura 4.20 mostra le caratteristiche dei numeri interi rappresentabili con questa codifica. In questo capitolo abbiamo analizzato il caso di un computer reale che lavora con codici numerici a 8 cifre espressi in base b=2; la Figura 4.21 mostra le caratteristiche dei numeri interi rappresentabili con questa codifica. Analizzando la Figura 4.20 e la Figura 4.21 possiamo ricavare facilmente le proprietà generali dei numeri rappresentabili con un sistema di codifica numerica a n cifre in base b; la Figura 4.22 mostra proprio queste proprietà. Tutti i linguaggi di programmazione di alto livello forniscono al programmatore una serie di tipi di dati interi con o senza segno; questi dati vengono visualizzati sullo schermo in formato decale (base 10) ma, ovviamente, vengono gestiti internamente in base 2. Le formule illustrate nella Figura 4.22 ci permettono di capire facilmente il perché delle caratteristiche di questi tipi di dati.
Consideriamo, in particolare, i tipi di dati interi forniti dai linguaggi di alto livello destinati alle CPU con architettura a 16 bit; nel caso, ad esempio, del linguaggio C, abbiamo a disposizione i tipi di dati interi mostrati in Figura 4.23. La Figura 4.24 mostra i principali tipi di dati interi supportati dal linguaggio Pascal. La Figura 4.25 infine mostra i due soli tipi di dati interi supportati dal linguaggio BASIC. Come si può notare, i compilatori e gli interpreti dei linguaggi di alto livello (cioè i programmi che traducono il codice sorgente in codice macchina), supportano spesso tipi di dati interi aventi un numero di bit superiore a quello dell'architettura della CPU; questi tipi di dati vengono gestiti via software dai compilatori o dagli interpreti stessi.

Nel precedente capitolo è stato citato il caso del gioco Tetris originariamente scritto in BASIC; il punteggio del gioco veniva gestito attraverso il tipo INTEGER (intero con segno a 16 bit) e i giocatori più bravi riuscivano facilmente a provocare un overflow. In sostanza, bastava superare di uno i 32767 punti per veder comparire sullo schermo il punteggio negativo -32768; in base ai concetti appena esposti possiamo giustificare facilmente questa situazione. Osserviamo che in binario il valore a 16 bit 32767 si scrive 0111111111111111b; abbiamo quindi:
0111111111111111b + 1b = 1000000000000000b
Ma per i numeri interi con segno a 16 bit questa è proprio la rappresentazione in complemento a 2 del numero negativo -32768; infatti:
216 - 32768 = 1000000000000000b
Chi avesse ancora dei dubbi può verificare in pratica questi concetti attraverso i programmi mostrati nella Figura 4.26; tali programmi fanno riferimento, naturalmente, a compilatori e interpreti destinati alla modalità reale 8086 supportata dal DOS.
Figura 4.26 - Overflow di un intero con segno a 16 bit

Nel caso degli interpreti BASIC, se si esegue il programma dall'interno dell'editor integrato, compare una finestra che informa dell'avvenuto overflow; se, invece, si ha a che fare con una versione compilata del BASIC (come il Quick Basic) e si effettua la compilazione dal prompt del DOS, l'overflow si verificherà solo in fase di esecuzione del programma.

4.7 Rappresentazione dei numeri in altre basi

Come molti avranno notato, passando da base 10 a base 2 (cioè da una base ad un'altra più piccola), è necessario usare molte più cifre per rappresentare uno stesso numero; volendo rappresentare, ad esempio, il numero 3000000 (tre milioni) in base 2 si ottiene 001011011100011011000000b. Volendo esprimere in base 2 numeri dell'ordine di qualche miliardo, si rischia di essere sepolti da una valanga di 0 e 1. Il computer gestisce i numeri binari a velocità vertiginose, mentre noi esseri umani incontriamo notevoli difficoltà ad usare i numeri in questo formato; d'altra parte, bisogna anche dire che per un programmatore sarebbe assurdo rinunciare ai notevoli vantaggi che si ottengono lavorando in base 2. Capita frequentemente, ad esempio, di dover gestire nei programmi dati numerici dove ogni singolo bit ha un preciso significato; è evidente allora che sarebbe meglio avere questi numeri espressi in base 2 piuttosto che in base 10, visto che in quest'ultimo caso risulta piuttosto difficile distinguere i singoli bit.
È necessario allora trovare un formato di rappresentazione dei numeri binari, che presenti le seguenti caratteristiche fondamentali: Il primo requisito viene soddisfatto scegliendo ovviamente una base maggiore di 2; il secondo e più importante requisito, invece, viene soddisfatto facendo in modo che la nuova base si possa esprimere sotto forma di potenza intera di 2 per i motivi che vedremo tra poco.
Si può subito notare che la base 10 soddisfa il primo requisito ma non il secondo perché non è possibile esprimere 10 come 2n con n intero positivo; per questo motivo, utilizzare nei programmi (non solo in Assembly) numeri in base 10, può portare spesso a delle complicazioni.
Esistono svariate basi numeriche che soddisfano tutti i requisiti elencati in precedenza; le due basi che hanno avuto però maggiore diffusione sono la base 8 e la base 16.

4.7.1 Sistema di numerazione posizionale in base b = 8

Il sistema di numerazione posizionale in base 8 viene chiamato ottale e si ottiene a partire dall'insieme formato dai seguenti 8 simboli:
S = {0, 1, 2, 3, 4, 5, 6, 7}
In Assembly i numeri espressi in ottale sono contrassegnati dal suffisso O oppure Q (maiuscolo o minuscolo); è preferibile usare la lettera Q perché la lettera O può essere confusa con lo zero. Per convenzione, i numeri privi di suffisso si considerano espressi in base 10; in caso di necessità si può anche usare il suffisso D per i numeri in base 10.
Come ormai già sappiamo, tutte le considerazioni svolte per le basi 2 e 10 si possono estendere con estrema facilità alla base 8; l'aspetto importante da ricordare è che, dato un numero in base 8, il peso di una cifra in posizione p è espresso da 8p.
Dimostriamo ora come il passaggio da base 8 a base 2 e viceversa sia immediato; a tale proposito, applichiamo le proprietà matematiche dei sistemi di numerazione posizionali. Innanzi tutto osserviamo che per esprimere in base 2 tutti i numeri interi positivi da 0 a 7, sono sufficienti 3 bit; infatti, con 3 bit possiamo rappresentare la sequenza:
000b, 001b, 010b, 011b, 100b, 101b, 110b, 111b
Questa sequenza corrisponde appunto ai numeri interi positivi da 0 a 7; quindi, una qualsiasi cifra ottale può essere espressa attraverso 3 cifre binarie.
Osserviamo poi che in ottale la base 8 può essere espressa come 8=23; in base a queste osservazioni, possiamo effettuare in modo semplicissimo le conversioni da base 2 a base 8 e viceversa. La Figura 4.27 illustra il metodo di conversione da ottale a binario. Osservando la Figura 4.27 possiamo constatare che per convertire rapidamente un numero ottale in binario, basta sostituire ad ogni cifra ottale la sua codifica in binario a 3 bit; tutto ciò è una diretta conseguenza del fatto che 8=23. Dato allora il numero ottale 75q, possiamo osservare che la codifica binaria di 7q è 111b, mentre la codifica binaria di 5q è 101b; unendo i due codici binari si ottiene proprio 111101b.

La conversione inversa (da binario ad ottale) è altrettanto immediata; basta applicare, infatti, il procedimento inverso a quello appena illustrato. Si prende quindi il numero binario, lo si suddivide in gruppi di tre cifre partendo da destra (aggiungendo se necessario zeri non significativi a sinistra), si sostituisce ogni gruppo di tre bit con la corrispondente cifra ottale e il gioco è fatto.
Vediamo l'esempio della Figura 4.27 al contrario; prendiamo il numero binario 111101b e suddividiamolo in gruppi di tre cifre ottenendo 111b 101b. Osserviamo ora che 111b in ottale corrisponde a 7q, mentre 101b in ottale corrisponde a 5q; unendo le due cifre ottali si ottiene proprio 75q.
Come si può notare, le conversioni da una base all'altra sono veramente rapide quando tra le due basi esiste la relazione matematica che abbiamo visto prima; per poter eseguire queste conversioni in modo veloce e sicuro, è consigliabile munirsi di una apposita tabella di conversione tra le principali basi numeriche (questa tabella, a dispetto della sua semplicità, offre un aiuto enorme ai programmatori).

4.7.2 Sistema di numerazione posizionale in base b = 16

Il sistema ottale garantisce una rappresentazione compatta dei numeri binari e ci permette di passare facilmente da base 8 a base 2 o viceversa; purtroppo però i numeri ottali non garantiscono l'altro importante requisito e cioè, la possibilità per il programmatore di individuare facilmente, dalla rappresentazione ottale, il valore di ogni singolo bit. Proprio per questo motivo, la rappresentazione ottale è stata quasi del tutto soppiantata dalla rappresentazione esadecimale; come si intuisce dal nome, si tratta di un sistema di numerazione posizionale in base b=16 formato quindi dal seguente insieme di 16 simboli:
S = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F}
Questi simboli equivalgono alle cifre decali comprese tra 0 e 15.
Osserviamo innanzi tutto che, in relazione alla base 16, si ha 16=24; in base allora a quanto abbiamo visto per la base 8, possiamo dire che per esprimere in binario una qualunque cifra esadecimale, dobbiamo utilizzare al massimo 4 bit. Con 4 bit possiamo esprimere tutti i numeri binari che vanno da 0000b a 1111b; questa sequenza corrisponde a tutti i numeri in base 10 compresi tra 0 e 15. La stessa sequenza corrisponde quindi a tutti i numeri esadecimali compresi tra 0 e F; per convenzione, in Assembly tutti i numeri esadecimali recano il suffisso H (o h). La Figura 4.28 mostra un esempio di conversione da base 16 a base 2. Osservando la Figura 4.28 possiamo constatare che per convertire rapidamente un numero esadecimale in binario, basta sostituire ad ogni cifra esadecimale la sua codifica in binario a 4 bit; tutto ciò è una diretta conseguenza del fatto che 16=24. Dato allora il numero esadecimale 9Bh, possiamo osservare che la codifica binaria di 9h è 1001b, mentre la codifica binaria di Bh è 1011b; unendo i due codici binari si ottiene proprio 10011011b.

La conversione inversa (da binario ad esadecimale) è altrettanto immediata; basta applicare, infatti, il procedimento inverso a quello appena illustrato. Si prende quindi il numero binario, lo si suddivide in gruppi di quattro cifre partendo da destra (aggiungendo se necessario zeri non significativi a sinistra), si sostituisce ogni gruppo di quattro bit con la corrispondente cifra esadecimale e il gioco è fatto.
Vediamo l'esempio della Figura 4.28 al contrario; prendiamo il numero binario 10011011b e suddividiamolo in gruppi di quattro cifre ottenendo 1001b 1011b. Osserviamo ora che 1001b in esadecimale corrisponde a 9h, mentre 1011b in esadecimale corrisponde a Bh; unendo le due cifre esadecimali si ottiene proprio 9Bh.
Ancora una volta si consiglia di avere sempre a portata di mano la tabella di conversione tra le principali basi numeriche che, tra l'altro, mostra in modo evidente la corrispondenza tra cifre o gruppi di cifre in basi diverse.

Il sistema di numerazione esadecimale permette di rappresentare i numeri binari in forma estremamente compatta e garantisce anche una notevole semplicità nelle conversioni da base 16 a base 2 e viceversa. Ma l'altro importante pregio del sistema di numerazione esadecimale, è rappresentato dal fatto che un numero espresso in base 16 ci permette di intuire facilmente il valore di ogni suo singolo bit; considerando, ad esempio, il numero esadecimale 7ABFh, si intuisce subito che i primi quattro bit (quelli più a destra) valgono 1111b.

Il sistema di numerazione esadecimale rappresenta la vera soluzione al nostro problema e per questo motivo viene utilizzato in modo massiccio non solo da chi programma in Assembly, ma anche da chi usa i linguaggi di programmazione di alto livello; tanto è vero che tutti i linguaggi di programmazione supportano i numeri esadecimali. La Figura 4.29 illustra, ad esempio, la rappresentazione esadecimale del numero 7ABFh secondo la sintassi dei principali linguaggi di alto livello. Riassumendo le cose appena viste possiamo dire che, in generale, le conversioni numeriche da base 2 a base b=2k e viceversa, sono immediate in quanto ogni cifra del numero in base b corrisponde ad un gruppo di k cifre in binario.

4.7.3 Conversioni numeriche che coinvolgono la base 10

Come abbiamo già visto, non è possibile esprimere la base 10 sotto forma di potenza intera di 2; spesso però capita di dover eseguire conversioni in base 10 da altre basi o viceversa e quindi bisogna saper affrontare anche questo problema. La soluzione più semplice e sbrigativa consiste nel procurarsi una calcolatrice scientifica capace di maneggiare i numeri binari, ottali, esadecimali, etc, oltre che ovviamente i numeri decimali; queste calcolatrici mettono a disposizione funzioni di conversione istantanea da una base all'altra evitando la necessità di eseguire calcoli spesso fastidiosi. È anche possibile utilizzare le calcolatrici "software" fornite dai SO come Windows, Linux, MacOSX, etc; tali calcolatrici, dispongono di una modalità scientifica che offre diverse funzioni di conversione tra basi numeriche.
Chi decide, invece, di affrontare direttamente queste conversioni, può rendersi conto che i calcoli da svolgere sono abbastanza semplici; la conversione di un numero da una base qualunque alla base 10 è una ovvia conseguenza della struttura dei sistemi di numerazione posizionali. La Figura 4.30 mostra una serie di esempi pratici. Per la conversione inversa, cioè da base 10 ad un'altra base b, si può utilizzare un algoritmo chiamato metodo delle divisioni successive. Si prende il numero da convertire e lo si divide ripetutamente per la base b (divisione intera) finché il quoziente non diventa zero; i resti di tutte le divisioni eseguite rappresentano le cifre del numero nella nuova base b a partire dalla meno significativa. La Figura 4.31 mostra un esempio di conversione da base 10 a base 16. Disponendo ora in sequenza i resti di queste divisioni (a partire dall'ultimo) otteniamo 3C2Fh che è proprio la rappresentazione in base 16 di 15407.

Raramente capita di dover passare da una base b1 a una base b2 entrambe diverse da 10 e tali che una non è una potenza intera dell'altra; in questi casi, conviene usare la base 10 come base intermedia passando quindi da base b1 a base 10 e da base 10 a base b2 attraverso gli algoritmi appena illustrati. Per approfondire ulteriormente questi concetti, si consiglia di consultare un testo di elettronica digitale.

A questo punto, possiamo dire di aver raggiunto il grado di semplificazione necessario per implementare via hardware tutti i concetti esposti in questo capitolo; nel prossimo capitolo vedremo appunto quali tecniche vengono utilizzate per rappresentare fisicamente le informazioni all'interno del computer.