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:
- il peso del 3 più a destra è 60 = 1
- il peso del 3 centrale è 61 = 6
- il peso del 3 più a sinistra è 62 = 36
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:
- l'1 più a destra ha posizione 0 e peso 20 = 1
- il secondo 1 ha posizione 1 e peso 21 = 2
- il terzo 1 ha posizione 2 e peso 22 = 4
- l'1 più a sinistra ha posizione 3 e peso 23 = 8
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:
- in base 10 il numero 10 si rappresenta come 10(10)
- in base 6 il numero 6 si rappresenta come 10(6)
- in base 2 il numero 2 si rappresenta come 10(2)
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:
- kilo indica la potenza intera del 2 più vicina a mille
(210)
- mega indica la potenza intera del 2 più vicina ad un milione
(220)
- giga indica la potenza intera del 2 più vicina ad un miliardo
(230)
- tera indica la potenza intera del 2 più vicina a mille miliardi
(240)
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:
- possibilità di rappresentare i numeri binari in forma compatta
- estrema semplicità di conversione da una base all'altra
- facilità di individuazione del valore dei singoli bit
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.