Assembly Avanzato con NASM
Capitolo 17: 80x87 Floating Point Unit - Il coprocessore matematico
Nel precedente capitolo abbiamo analizzato gli aspetti teorici relativi alla
rappresentazione binaria dei numeri in virgola mobile; dai concetti esposti si
evince che l'uso di una semplice CPU per la gestione di tali numeri
comporta tempi di elaborazione nettamente più lunghi rispetto al caso dei numeri
interi. I numeri interi, infatti, vengono codificati via hardware dalla CPU
e così anche le operazioni elementari eseguibili su di essi; al contrario, per i
numeri in virgola mobile e per le operazioni ad essi applicate, bisogna procedere
via software in quanto la CPU stessa non dispone di funzionalità hardware
dedicate a tali aspetti.
La situazione si complica ulteriormente nel momento in cui si ha la necessità di
impiegare la CPU per elaborare funzioni trigonometriche, logaritmiche,
esponenziali, etc, aventi numeri interi o in virgola mobile come operandi; anche
in questo caso, si è costretti ad operare via software con algoritmi piuttosto
complessi. A titolo di curiosità, analizziamo un metodo che si basa sull'uso della
formula di Taylor.
Data una funzione reale \(y = f(x)\) di una variabile reale \(x\), definita in un
intervallo \(I\) (limitato o no), se tale funzione è ivi derivabile \(n\) volte
e se \(x_0\) è un punto di \(I\), si dimostra che per ogni \(x \in I\), con
\(x \ne x_0\), esiste almeno un punto \(\xi_x\) interno all'intervallo di
estremi \(x\) e \(x_0\), tale che:
\[
f(x) = f(x_0) + {{f'(x_0)} \over {1!}} \cdot (x - x_0)^1 +
{{f''(x_0)} \over {2!}} \cdot (x - x_0)^2 +
\cdots + {{f^{(n-1)}(x_0)} \over {(n - 1)!}} \cdot (x - x_0)^{n - 1} +
{{f^{(n)}(\xi_x)} \over {n!}} \cdot (x - x_0)^n
\]
Si ottiene così la formula di Taylor della \(f(x)\), nel punto iniziale
\(x_0\), arrestata alla derivata \(n\)-esima. Il termine:
\[
T_n(x) = {{f^{(n)}(\xi_x)} \over {n!}} \cdot (x - x_0)^n
\]
prende il nome di termine complementare nella forma di Lagrange.
Nel caso particolare in cui \(x_0 = 0\), si ottiene la formula di Mac Laurin
della \(f(x)\) con il termine complementare nella forma di Lagrange:
\[
f(x) = f(0) + {{f'(0)} \over {1!}} \cdot x^1 + {{f''(0)} \over {2!}} \cdot x^2 +
\cdots + {{f^{(n-1)}(0)} \over {(n - 1)!}} \cdot x^{n - 1} +
{{f^{(n)}(\xi_x)} \over {n!}} \cdot x^n
\]
Le formule appena illustrate hanno una importanza enorme in Analisi Matematica in
quanto, sotto ipotesi largamente verificate, permettono di trasformare una qualunque
funzione \(f(x)\) (che soddisfi i requisiti richiesti) in una funzione razionale
intera; in sostanza, anche una funzione trascendente può essere ridotta ad una
sequenza di addizioni, sottrazioni, moltiplicazioni e divisioni tra numeri reali!
Osserviamo innanzi tutto che, come spiegato in precedenza, \(\xi_x\) è un punto,
generalmente incognito, interno all'intervallo di estremi \(x\) e \(x_0\);
trascurando allora il termine \(T_n(x)\), otteniamo una approssimazione della
\(f(x)\) tanto migliore quanto più grande è l'ordine di derivazione \(n\). Il
termine \(T_n(x)\) rappresenta chiaramente l'errore che commettiamo con tale
approssimazione; tanto maggiore è \(n\), tanto minore è l'errore commesso.
Supponiamo, ad esempio, di voler calcolare la funzione \(y = \sin(x)\), con
\(0 \le x \le \pi/4\), arrestata alla derivata ottava; siccome lo \(0\) fa parte
dell'intervallo in cui è definita la funzione, poniamo come punto iniziale
\(x_0 = 0\) (il punto iniziale non deve essere troppo distante dal punto \(x\) in
cui si vuole calcolare la funzione, altrimenti si ottengono risultati piuttosto
imprecisi). Calcoliamoci ora le prime otto derivate successive della funzione e
il loro valore in \(x_0 = 0\):
\begin{array}{ll}
f(x) = \sin(x) &\implies f(0) = \sin(0) = 0 \cr
f'(x) = \cos(x) &\implies f'(0) = \cos(0) = 1 \cr
f''(x) = -\sin(x) &\implies f''(0) = -\sin(0) = 0 \cr
f'''(x) = -\cos(x) &\implies f'''(0) = -\cos(0) = -1 \cr
f^{IV}(x) = \sin(x) &\implies f^{IV}(0) = \sin(0) = 0 \cr
f^{V}(x) = \cos(x) &\implies f^{V}(0) = \cos(0) = 1 \cr
f^{VI}(x) = -\sin(x) &\implies f^{VI}(0) = -\sin(0) = 0 \cr
f^{VII}(x) = -\cos(x) &\implies f^{VII}(0) = -\cos(0) = -1 \cr
f^{VIII}(x) = \sin(x) &\implies f^{VIII}(\xi_x) = \sin(\xi_x)
\end{array}
La formula di Mac Laurin arrestata alla derivata ottava sarà quindi:
\[
\sin(x) = x - {{x^3} \over {3!}} + {{x^5} \over {5!}} -
{{x^7} \over {7!}} + {{x^8} \over {8!}} \cdot \sin(\xi_x)
\]
In questo caso siamo anche in grado di valutare l'errore commesso trascurando
\(T_8(x)\). Osserviamo che, essendo \(0 \le x \le \pi/4\), il valore massimo
assunto dalla \(x\) è pari a \(\pi/4\), che è strettamente minore di \(1\) (e
quindi lo è anche \(x^8\)); inoltre, \(\sin(\xi_x) \le 1\). Si ha quindi:
\[
T_8(x) = {{x^8} \over {8!}} \cdot \sin(\xi_x) \le {{x^8} \over {8!}} \lt
{1 \over {8!}} = 0,0000248015873
\]
In sostanza, i valori approssimati di \(\sin(x)\) calcolati in questo modo, hanno
almeno quattro cifre decimali esatte!
Ad esempio, per \(x = 1/3\) radianti, si ottiene (si faccia il confronto con il
risultato fornito da una calcolatrice scientifica):
\[
\sin({1 \over 3}) = {1 \over 3} - {1 \over {3^3 \cdot 3!}} +
{1 \over {3^5 \cdot 5!}} - {1 \over {3^7 \cdot 7!}} = 0,327194696656
\]
Per valori di \(n\) molto alti, la formula di Taylor garantisce una elevata
precisione, ma i tempi di elaborazione si allungano notevolmente, sino a diventare
proibitivi per una semplice CPU; sarebbe molto vantaggioso quindi implementare
via hardware (con le opportune ottimizzazioni) i metodi di calcolo appena illustrati.
Proprio queste considerazioni hanno portato i progettisti della Intel ad
affiancare alla CPU un secondo microprocessore opzionale che prende il nome di
coprocessore matematico; il cuore di tale dispositivo è costituito dalla sua
unità di calcolo in virgola mobile, denominata FPU o Floating Point Unit.
Nel seguito del capitolo si segue una diffusa tendenza che consiste nell'attribuire
lo stesso significato ad entrambe le denominazioni "coprocessore matematico" e "FPU";
si deve sempre tenere presente però che, per quanto appena visto, tale semplificazione
non è del tutto corretta.
17.1 Evoluzione dell'hardware per la FPU
Nel 1980 la Intel immette sul mercato la sua prima FPU, denominata
8087; si tratta di un microprocessore opzionale, destinato a lavorare in
coppia con la CPU 8086.
Questo nuovo dispositivo supporta via hardware la gestione dei numeri in virgola
mobile, nonché operazioni di tipo aritmetico, esponenziale, logaritmico e
trigonometrico; le istruzioni per tale tipo di calcoli vengono eseguite dalla
8087 ad una velocità circa 100 volte superiore a quella che si
ottiene emulando via software le stesse istruzioni con la CPU 8086!
La Figura 17.1 mostra l'interfaccia tra CPU 8086 e FPU 8087.
La CPU e la FPU condividono gli stessi bus di sistema ed entrambe
ricevono in sequenza tutte le istruzioni da eseguire; ciò impone che i due
dispositivi debbano operare in modo sincrono per evitare situazioni critiche
(come viene illustrato più avanti). La CPU controlla il normale flusso del
programma che, nel caso generale, è costituito da un mix di istruzioni 8086
e 8087; la stessa CPU elabora ciascuna delle istruzioni in arrivo e
poi la passa alla FPU.
Una istruzione destinata alla FPU (o istruzione numerica) viene
identificata dal suo opcode, che inizia sempre con la sequenza binaria
11011b (27); incidentalmente, tale valore coincide con il codice
ASCII dell'istruzione di controllo
ESC e, per questo motivo, nella documentazione tecnica della Intel le
istruzioni numeriche vengono spesso definite escape instructions.
La prossima istruzione da eseguire giunge quindi alla CPU. Se si tratta di una
istruzione numerica che non richiede un accesso in memoria, la CPU semplicemente
la passa alla FPU e attende la prossima istruzione da eseguire; se, invece, si
tratta di una istruzione numerica che richiede un accesso in memoria, la CPU
calcola prima il relativo indirizzo ed effettua una "finta lettura" da 16 bit,
nel senso che il valore letto viene ignorato.
La FPU, dal canto suo, prende in considerazione solo le istruzioni numeriche
e ignora tutte le altre. Se l'istruzione numerica richiede un accesso in memoria di
tipo read, la FPU preleva il dato a 16 bit appena letto dalla
CPU; se il dato da leggere è maggiore di 16 bit, la FPU procede
con ulteriori cicli di lettura. Se l'istruzione numerica richiede un accesso in memoria
di tipo write, la FPU procede con il numero necessario di cicli di
scrittura; in questo caso, i 16 bit letti in precedenza dalla CPU vengono
ignorati.
Dalle considerazioni appena esposte risulta che, di fatto, i due dispositivi possono
trovarsi ad operare in parallelo; in certi casi, questa situazione può però creare dei
problemi. Supponiamo, ad esempio, che le due prossime istruzioni da eseguire siano
A e B, dove A, di tipo 8087, effettua un calcolo e salva il
risultato in memoria, mentre B, di tipo 8086, legge dalla memoria lo stesso
dato salvato in precedenza. La CPU quindi decodifica l'istruzione A e la
passa alla FPU dopo aver effettuato un finta lettura da 16 bit in memoria;
la FPU inizia l'elaborazione dell'istruzione A, mentre la CPU riceve
l'istruzione B. L'istruzione B prevede che la CPU legga dalla
memoria il dato salvato dalla FPU attraverso l'istruzione A; può
capitare però che tale lettura venga effettuata dalla CPU prima che la
FPU abbia terminato l'elaborazione dell'istruzione A (e quindi, prima
che il risultato del calcolo venga salvato in memoria)!
Per rendere possibile la gestione di situazioni di questo genere, la FPU tiene
attivo il suo pin BUSY mentre sta elaborando una istruzione numerica; la
CPU può conoscere lo stato del pin BUSY attraverso il suo pin
TEST (Figura 17.1). Per obbligare la CPU a
testare lo stato del pin BUSY, è necessario ricorrere all'istruzione WAIT;
tale istruzione viene fornita dalla CPU stessa ed è perfettamente equivalente
all'istruzione FWAIT fornita dalla FPU (Capitolo 21 della sezione Assembly
Base).
Possiamo risolvere quindi il nostro problema posizionando una istruzione WAIT (o
FWAIT) tra le due istruzioni A e B; in questo modo, l'esecuzione
del programma avviene secondo la seguente sequenza:
- la CPU decodifica l'istruzione A e la passa alla FPU
dopo aver effettuato una finta lettura da 16 bit in memoria
- la FPU inizia la decodifica dell'istruzione A, mentre la CPU
riceve l'istruzione WAIT e si mette quindi in stato di attesa
- la FPU elabora l'istruzione A e salva il risultato in memoria
- la CPU riprende l'attività e riceve l'istruzione B che consiste
nel leggere il dato appena salvato in memoria dalla FPU
La 8087 è importante storicamente in quanto ha ispirato quello che poi sarebbe
diventato lo standard IEEE 754; in effetti, questa FPU introduce molti
dei concetti illustrati nel precedente capitolo.
I tipi di dati supportati sono: Word Integer a 16 bit, Short Integer
a 32 bit, Long Integer a 64 bit, Packed BCD a 18 cifre,
Short Real a 32 bit, Long Real a 64 bit e Temporary Real
a 80 bit; quest'ultimo è un formato usato internamente dalla 8087 per
garantire la massima precisione possibile durante i calcoli.
Eventuali eccezioni producono una richiesta di interruzione al PIC 8259A (Figura
17.1); se le interruzioni sono disabilitate, la 8087 produce un risultato
predefinito e continua l'esecuzione del programma.
Vengono gestiti sei tipi di eccezione: Invalid Operation (stack overflow o
underflow, forme indeterminate, uso di NaN come operando, etc), Overflow
(il risultato è troppo grande in valore assoluto e, se richiesto, viene codificato come
infinito), Underflow (il risultato è diverso da zero ma troppo piccolo in valore
assoluto e, se richiesto, viene denormalizzato), Zero Divisor (il dividendo è un
numero finito non nullo, mentre il divisore è zero, per cui, se richiesto, il risultato
viene codificato come infinito), Denormalized Operand (almeno uno degli operandi è
denormalizzato), Inexact Result (il risultato non è rappresentabile nel formato
selezionato e verrà arrotondato in base alle impostazioni).
La 8087 introduce una serie di operatori matematici per calcoli aritmetici,
logaritmici, esponenziali e trigonometrici; complessivamente, vengono aggiunti 68
nuovi mnemonici a quelli del set di istruzioni della 8086.
Nel 1982 la Intel immette sul mercato la CPU 80186; alcuni anni dopo
arriva anche la corrispondente FPU 80187, le cui funzionalità sono pressoché le
stesse di quelle della 8087.
L'interfaccia tra 80186 e 80187 continua ad essere del tutto identica a
quella tra 8086 e 8087; per cercare di risolvere i problemi legati a tale
configurazione, successivamente viene prodotta una variante della 80186,
denominata 80C186, che presenta una interfaccia diretta verso la 80187.
In pratica, la 80C186 interagisce con la 80187 attraverso apposite porte
di I/O, come se avesse a che fare con una normale periferica; ciò permette di
migliorare notevolmente la sincronizzazione tra i due dispositivi.
Lo scarso successo ottenuto dalla 80186 induce la Intel a sviluppare,
sempre nel 1982, un nuovo microprocessore denominato 80286; questa
CPU introduce per la prima volta il concetto di modalità protetta. Alla
80286 fa seguito la corrispondente FPU 80287, che non presenta sostanziali
differenze rispetto ai predecessori.
Nel 1985 arriva sul mercato la CPU 80386 della Intel; due anni
dopo viene sviluppata la corrispondente FPU 80387 che presenta la caratteristica
di essere totalmente compatibile con lo standard IEEE 754.
La 80387 dispone di funzioni trigonometriche notevolmente migliorate e introduce
anche istruzioni per il calcolo di seni e coseni, non disponibili nelle precedenti
FPU.
Una svolta importante si verifica nel 1989, quando la Intel immette sul
mercato la nuova CPU 80486 (queste CPU vengono denominate i486 a
causa di un problema legale). Per la variante 80486SX viene resa disponibile
la corrispondente FPU 80487SX; la 80487SX è una vera e propria CPU
che integra anche una FPU e, quando viene installata sul computer, disabilita
completamente la 80486SX.
La sempre maggiore integrazione tra CPU e FPU spinge la Intel a
produrre una ulteriore variante denominata 80486DX; tale microprocessore
comprende al suo interno tutto l'hardware necessario per i calcoli in virgola mobile
e pone quindi fine all'era dei due dispositivi separati. Le successive CPU di
classe Pentium, ad esempio, sono dotate di due core (unità di calcolo per
i numeri interi) e una FPU, mentre quelle di classe Pentium Pro forniscono
due core e due FPU.
Un aspetto importante da sottolineare è che, anche nel campo dei coprocessori matematici,
la Intel ha provveduto a garantire la compatibilità verso il basso; di conseguenza,
un programma scritto, ad esempio, per una FPU 8087, gira perfettamente anche su
FPU 80187 o superiori.
17.2 Struttura interna di una FPU
Nel seguito del capitolo si farà riferimento ad una moderna CPU 80486DX o superiore,
che integra al suo interno almeno una unità di calcolo per i numeri interi (Integer
Unit o IU) e una unità di calcolo per i numeri in virgola mobile (FPU);
la Figura 17.2 mostra tale integrazione attraverso uno schema semplificato.
La logica di controllo della CPU decodifica ogni istruzione del programma attraverso
una sequenza di micro-operazioni; ogni istruzione appena decodificata, viene poi inviata
alla IU e alla FPU. Come è stato già spiegato, le due unità si trovano a
lavorare in parallelo; la FPU scarta tutte le istruzioni non numeriche (il cui
opcode, cioè, non inizia per 11011b ).
La Figura 17.3 mostra più in dettaglio un generico coprocessore matematico o Math
Coprocessor (MCP), di cui la FPU è parte integrante.
La parte più a sinistra del MCP di Figura 17.3 rappresenta la Bus Control
Logic (BCL); il suo compito è quello di far comunicare la CPU con lo stesso
MCP per il trasferimento di dati e istruzioni. La CPU vede la BCL
come una vera e propria periferica; ciò permette una perfetta sincronizzazione grazie
anche al fatto che i due dispositivi utilizzano lo stesso segnale di clock. La
BCL è l'unica parte del MCP che lavora in sincrono con la CPU; le
restanti parti possono lavorare invece anche in modo asincrono (e quindi in parallelo con
la stessa CPU).
Ogni volta che la CPU incontra una istruzione numerica, passa il relativo
opcode alla BCL attraverso il data bus; in Figura 17.3 tale bus è
indicato con D0-D31 ed è connesso al Data Buffer (si tratta quindi di una
architettura a 32 bit).
Per quanto riguarda il trasferimento dati, la BCL non è in grado di comunicare
direttamente con la memoria del computer; qualsiasi operazione di I/O in memoria,
richiesta dal MCP, viene svolta dalla CPU e il relativo dato da leggere o
scrivere, viene scambiato con la stessa BCL sempre attraverso il data bus.
La parte centrale del MCP di Figura 17.3 rappresenta la Data Interface And
Control Unit; il suo compito è quello di gestire il flusso di dati e istruzioni tra
la BCL e la FPU. Come si può notare, i dati vengono disposti in una serie
di registri denominati Data FIFO; si tratta quindi di una classica struttura
FIFO o First In First Out, dove il primo dato ad entrare è anche il primo
ad uscire. Le istruzioni, invece, vengono inviate all'Instruction Decoder; tale
dispositivo provvede a decodificare ogni istruzione attraverso il Micro Instruction
Sequencer.
Questa parte del MCP si occupa anche di attivare il pin BUSY e altri segnali
di controllo ogni volta che è in corso l'elaborazione di una istruzione numerica; inoltre,
assiste la FPU nella gestione delle eccezioni. Nei sistemi integrati come quello di
Figura 17.2, la CPU controlla sempre lo stato del pin BUSY prima di inviare
una nuova istruzione numerica al MCP; ciò rende inutile il ricorso all'istruzione
WAIT/FWAIT, contrariamente a quanto avviene nel caso dell'8087.
La parte più a destra del MCP di Figura 17.3 rappresenta la Floating Point
Unit vera e propria; il suo compito è quello di effettuare materialmente i calcoli di
tipo aritmetico, logico e trascendente (funzioni logaritmiche, trigonometriche, etc).
Gli operandi vengono gestiti attraverso una struttura a stack composta da 8
registri da 80 bit ciascuno; come è stato già spiegato, questo formato interno
viene usato per garantire la massima precisione possibile durante i calcoli.
Si può notare la presenza di un dispositivo denominato Constant ROM; si tratta di
una memoria ROM dove si trovano memorizzate varie costanti matematiche come, ad
esempio, \(\pi\), \(\log_2 e\), \(log_e 2\).
Un altro importante dispositivo è quello denominato CORDICS Nano-Machine; tale
dispositivo fornisce una serie di algoritmi ottimizzati per il calcolo di funzioni
trigonometriche ed iperboliche. L'acronimo CORDIC sta per COordinate Rotation
DIgital Computer.
I bus interni della FPU (mantissa buses e exponent buses) sono dotati
di ben 84 linee, di cui 68 per la mantissa, 15 per la caratteristica
e una per il bit di segno; ciò permette di trasferire gli operandi ad altissima velocità
tra i vari dispositivi che compongono la FPU stessa.
17.3 Registri del MCP
Analizziamo ora in dettaglio i registri del MCP accessibili ai programmatori.
17.3.1 FPU Data Registers (STACK)
Qualsiasi operando da utilizzare durante i calcoli, viene convertito in un formato a
80 bit denominato Temporary Real e poi caricato in uno degli 8
registri che costituiscono la struttura a stack di Figura 17.3; ciò vale anche per i
formati interi e BCD supportati dalla FPU. Ovviamente, trattandosi di
una struttura a stack, i dati vanno a posizionarsi uno dietro l'altro, man mano che
vengono inseriti; di conseguenza, l'ultimo dato ad entrare sarà anche il primo ad
uscire (Last In, First Out).
La Figura 17.4 (a) illustra le caratteristiche dei registri dello stack, chiamati
Data Registers.
Ogni registro da 80 bit utilizza quindi 64 bit per la mantissa, 15
bit per la caratteristica e un bit per il segno; il metodo di codifica è lo stesso
descritto nel precedente capitolo per i numeri in virgola mobile, in conformità con lo
standard IEEE 754.
Con 15 bit per la caratteristica, il bias è \(p = 2^{14} - 1 = 16383\);
per i numeri normalizzati, gli estremi positivi (S=0) sono quindi:
\begin{align}
x_{min} &= 2^{-16382} \cdot (1,00...0{0_2}) = 2^{-16382} \cdot 1
= 2^{-16382} \cr
x_{max} &= 2^{16383} \cdot (1,11...1{1_2}) =
2^{16383} \cdot (1 - 2^{-64}) = 2^{16383} - 2^{16319}
\end{align}
Un operando inserito nello stack di Figura 17.4 (a) rappresenta la "nuova cima dello
stack" (Top Of Stack o TOS); in conformità con lo stack di un programma,
anche la struttura di Figura 17.4 (a) si riempie dall'alto verso il basso (cioè, nel
verso che va da R7 a R0). La posizione del TOS varia quindi in
continuazione in base ai vari inserimenti e/o estrazioni di operandi; come viene
spiegato più avanti, consultando il registro denominato Status Word (Figura
17.3), possiamo sapere quale dei registri di Figura 17.4 (a) contiene attualmente il
TOS.
Per chiarire meglio questi concetti, può essere utile la Figura 17.4 (b), la quale
mostra i registri dello stack, da R0 a R7, disposti in posizione fissa
in una struttura circolare; le locazioni che rappresentano questi registri vengono
indicate con i nomi da ST(0) a ST(7) e possono ruotare sia in senso
antiorario (PUSH), sia in senso orario (POP), in base ai vari inserimenti
e/o estrazioni.
In questo caso vediamo che ST(0) (TOS) è posizionato in R2; di
conseguenza, ST(1) risulta posizionato in R3, ST(2) in R4
e così via. Se si inserisce (PUSH) un nuovo dato nello stack, esso sposterà
il TOS in R1, per cui si avrà ST(0) posizionato in R1,
ST(1) in R2, ST(2) in R3 e così via; se, invece, si estrae
(POP) il dato che si trova in ST(0)=R2, il TOS si sposterà in
R3, per cui si avrà ST(0) posizionato in R3, ST(1) in
R4, ST(2) in R5 e così via.
Consideriamo ora un ulteriore esempio pratico; a tale proposito, ci serviamo
dell'istruzione FLD (Floating Point Number Load) per caricare dei numeri
in virgola mobile a 64 bit (QWORD) nello stack di Figura 17.4 (a). Come
vedremo nel capitolo successivo, se specifichiamo solo l'operando sorgente, FLD
utilizza implicitamente (il nuovo) ST0 come operando destinazione; ogni numero
caricato nello stack, viene posizionato nel nuovo TOS. La Figura 17.5 illustra
ciò che succede con due istruzioni FLD consecutive che caricano nello stack due
dati denominati fvar1 e fvar2; in questo esempio supponiamo che
inizialmente si abbia ST0=R6, dove si trova un altro dato di nome fvar0.
Come si può notare, lo stack si riempie verso il basso; ogni dato appena inserito
diventa il nuovo ST0, mentre i dati precedenti scalano di una posizione
(ST1, ST2, etc). Ovviamente, le estrazioni dallo stack producono una
situazione inversa rispetto agli inserimenti.
Cosa succede se si utilizza un'istruzione come FLD, quando ST0=R0?
Come si vede chiaramente in Figura 17.4 (b), ciò che si verifica è il classico
wrap-around; non essendoci altri registri disponibili, il nuovo ST0
viene posizionato in R7, mentre tutti gli altri dati scalano di una posizione.
Se però lo stack era completamente pieno, il vecchio dato presente in R7
viene sovrascritto proprio dal contenuto del nuovo ST0!
Come vedremo nel seguito del capitolo, in casi del genere si verifica un'eccezione di
tipo stack overflow; analogamente, vedremo che il tentativo di estrarre un dato
quando lo stack è completamente vuoto, produce uno stack underflow.
Un altro aspetto particolarmente importante riguarda la gestione dello stack del
MCP da parte delle procedure presenti in un programma. Come accade per i
normali registri della CPU, le modifiche che una procedura apporta al contenuto
dei Data Registers, compresa la posizione del TOS, permangono anche quando
il controllo viene restituito al chiamante; se necessario, è compito del programmatore
ripristinare lo stato iniziale dello stack del MCP prima che la procedura stessa
termini.
Nella sezione Assembly Base abbiamo anche visto che, se ci si deve interfacciare
con i linguaggi di alto livello, le convenzioni prevedono che un valore di ritorno di
tipo tipo floating point debba essere restituito in ST0 da una procedura; in
questo caso, ovviamente, la procedura stessa non deve preservare il vecchio contenuto di
ST0.
17.3.2 FPU Status Word Register
In ogni istante, lo stato del MCP può essere monitorato attraverso il registro
Status Word di Figura 17.3; la Figura 17.6 illustra la struttura di tale
registro.
I tre bit in posizione 11, 12, 13 permettono di sapere in quale
degli otto Data Registers è presente l'attuale cima dello stack; con tre bit
possiamo specificare un qualunque valore compreso tra 000b e 111b (cioè,
tra 0 e 7), che indica il corrispondente registro tra R0 e R7.
I quattro bit in posizione 8, 9, 10, 14 codificano il
risultato di una operazione aritmetica o di una comparazione tra numeri in virgola
mobile; il significato dei vari codici viene illustrato in dettaglio nel prossimo
capitolo.
I sei bit nelle posizioni da 0 a 5 sono associati al verificarsi delle
eccezioni elencate nella Figura 17.6; come vedremo in seguito, ciascuna di tali
eccezioni può essere mascherata attraverso appositi bit del registro Control
Word (Figura 17.3).
Quando si verifica un'eccezione (non mascherata), il corrispondente bit di Figura
17.6 viene posto a 1; in tal caso, anche il bit in posizione 7 (Error
Summary Status) viene posto a 1 e ciò provoca la chiamata di un apposito
gestore di eccezione secondo il metodo illustrato più avanti.
Il bit in posizione 6 (Stack Fault) viene posto a 1 ogni volta
che si verifica uno stack overflow o uno stack underflow; per distinguere
i due casi, viene modificato il flag C1 del Condition Code (Figura 17.6),
con C1=1 overflow e C1=0 underflow.
Abbiamo visto che l'overflow si verifica quando cerchiamo di inserire un dato nello
stack già completamente pieno; per contro, l'underflow si verifica quando cerchiamo di
estrarre un dato dallo stack già completamente vuoto.
Il bit in posizione 15 (FPU Busy) è presente per compatibilità con la
8087, dove indica che la FPU è occupata ad eseguire un'istruzione;
nelle FPU di classe superiore, il contenuto di tale bit coincide con quello
del bit ES in posizione 7 e non ha nulla a che vedere con lo stato del
pin BUSY.
17.3.3 FPU Control Word Register
Il registro Control Word di Figura 17.3 permette di controllare tutti gli aspetti
legati alla precisione di calcolo della FPU, al metodo di arrotondamento e al
mascheramento delle eccezioni; la Figura 17.7 illustra la struttura di tale registro
(i campi colorati in grigio sono riservati).
Per mascherare una eccezione (bit nelle posizioni da 0 a 5) bisogna porre
a 1 il relativo bit; in tal caso, l'eccezione stessa non viene generata dalla
FPU.
I due bit in posizione 8 e 9 permettono di impostare la precisione dei
calcoli; in questo caso, per precisione si intende il numero di bit riservati alla
mantissa. Le opzioni disponibili sono elencate in Figura 17.8.
I due bit in posizione 10 e 11 permettono di impostare il metodo di
arrotondamento del risultato di un calcolo; i dettagli su questo aspetto sono stati
esposti nel capitolo precedente. Le opzioni disponibili sono elencate in Figura 17.9.
Può capitare che la precisione attualmente impostata (Figura 17.8) non permetta di
rappresentare in modo esatto il risultato di un calcolo; in tal caso, la FPU
effettua un arrotondamento con il metodo da noi scelto (Figura 17.9) e ottiene
un cosiddetto Inexact Result. La stessa FPU pone poi a 1 il
flag PE (Precision Exception) nella Status Word di Figura 17.6.
Se la Overflow Exception è mascherata nella Control Word di Figura
17.7 e il risultato di un calcolo è un numero positivo finito, maggiore del più grande
numero positivo rappresentabile con la precisione attualmente impostata, la FPU
arrotonda tale risultato in base al metodo di arrotondamento da noi prescelto.
Se i due bit del Rounding Control valgono 00b, il risultato viene
arrotondato a \(+\infty\), per 01b viene arrotondato al massimo numero positivo
rappresentabile, per 10b viene arrotondato a \(+\infty\) e per 11b viene
arrotondato al massimo numero positivo rappresentabile.
Se la Overflow Exception è mascherata nella Control Word di Figura
17.7 e il risultato di un calcolo è un numero negativo finito, maggiore (in valore
assoluto) del più grande (in valore assoluto) numero negativo rappresentabile con la
precisione attualmente impostata, la FPU arrotonda tale risultato in base al
metodo di arrotondamento da noi prescelto.
Se i due bit del Rounding Control valgono 00b, il risultato viene
arrotondato a \(-\infty\), per 01b viene arrotondato al massimo (in valore
assoluto) numero negativo rappresentabile, per 10b viene arrotondato a
\(-\infty\) e per 11b viene arrotondato al massimo (in valore assoluto) numero
negativo rappresentabile.
Il bit in posizione 12 è presente per compatibilità con la 80287 e non
ha significato per le FPU successive, le quali trattano \(\pm\infty\) secondo
le convenzioni illustrate nel seguito del capitolo.
17.3.4 FPU Tag Word Register
Il registro Tag Word di Figura 17.3 permette di conoscere in ogni istante il
tipo di contenuto degli 8 registri che formano lo stack della FPU; la
Figura 17.10 illustra tutti i dettagli.
Come si può notare, il registro Tag Word è suddiviso in 8 campi da
2 bit ciascuno; gli 8 campi sono associati ovviamente agli 8
registri hardware dello stack e il loro contenuto è un codice a 2 bit il cui
significato viene illustrato nella Figura 17.11.
Valido significa che il relativo registro dello stack contiene un numero in
virgola mobile normalizzato, correttamente codificato secondo lo standard IEEE
754.
Speciale significa che il relativo registro dello stack contiene un numero
in virgola mobile non valido (NaN o non supportato), \(\infty\) o un numero
in virgola mobile denormalizzato.
La FPU utilizza proprio il registro Tag Word per individuare i casi di
stack overflow e underflow. Come abbiamo visto, l'overflow si verifica quando
l'inserimento di un nuovo dato nello stack fa si che il nuovo ST0 vada a
sovrascrivere un altro dato (lo stack è completamente pieno); l'underflow si verifica
quando l'estrazione di un dato dallo stack fa si che il nuovo ST0 si vada a
posizionare in un registro vuoto (lo stack è completamente vuoto).
Anche i gestori delle eccezioni possono utilizzare il registro Tag Word per
sapere quale problema si è verificato; in questo modo si evita la necessità di
effettuare complicate analisi sul contenuto del registro che ha creato l'eccezione.
17.3.5 FPU Instruction and Operand Pointer Registers
Gli indirizzi dell'istruzione e di un eventuale operando (dato), appena elaborati
dalla FPU, vengono memorizzati in due appositi registri puntatori da 48
bit ciascuno, denominati Instruction Pointer e Operand Pointer; tali
registri non compaiono in Figura 17.3 in quanto non appartengono al MCP. La
Figura 17.12 illustra tutti i dettagli.
Come si può notare, entrambi i registri sono suddivisi in due campi: offset
da 32 bit e segment da 16 bit; in modalità reale, tali due campi
contengono il classico indirizzo logico seg:offset da 16+16 bit (vengono
usati solo i primi 16 bit del campo offset), mentre in modalità protetta
l'indirizzo è costituito da un selettore di segmento da 16 bit e da un offset a
32 bit.
Queste informazioni vengono memorizzate in quanto risultano molto utili per i gestori
delle eccezioni.
17.3.6 FPU Opcode Register
L'opcode dell'istruzione appena elaborata dalla FPU, viene memorizzato in un
apposito registro da 11 bit, denominato Opcode Register; tale registro
non compare in Figura 17.3 in quanto non appartiene al MCP. La
Figura 17.13 illustra tutti i dettagli.
Come vedremo nel capitolo successivo, l'opcode di una istruzione della FPU è
formato da 2 byte; i primi 8 bit del primo byte e i primi 3 bit
del secondo byte vengono salvati negli 11 bit del registro FPU Opcode.
Non è necessario salvare anche i 5 bit più significativi del secondo byte in
quanto, come sappiamo, essi valgono sempre 11011b.
Anche questa informazione viene memorizzata in quanto risulta molto utile per i gestori
delle eccezioni.
17.4 Tipi di dati supportati dalla FPU
La FPU supporta sette tipi di dati numerici che soddisfano lo standard
IEEE 754; tali tipi di dati possono essere suddivisi in tre gruppi:
- numeri reali
- numeri interi
- numeri Packed BCD
Vengono supportati anche i numeri reali denormalizzati, come richiesto dallo standard
IEEE 854.
Si tenga presente che, come è stato spiegato in precedenza, qualsiasi tipo di dato
numerico, prima di essere caricato nello stack della FPU, viene convertito
nel formato Temporary Real (detto anche Extended Real) a 80 bit.
17.4.1 Numeri reali
La FPU supporta i tre formati di numeri reali mostrati in Figura 17.14.
Come abbiamo visto nel precedente capitolo, per i formati Single Real e
Double Real, la mantissa è del tipo \(\pm(1, ...)\), con il bit di valore
1 che viene considerato implicito (e non compare quindi nella mantissa
stessa); in sostanza, per ottenere la mantissa di un numero reale, dobbiamo far
scorrere (a destra o a sinistra) i suoi bit finché il primo bit diverso da zero si
venga a trovare immediatamente alla sinistra della virgola. Per il formato Extended
Real, invece, il primo bit diverso da zero si trova esplicitamente in posizione
63.
Ovviamente, le precedenti considerazioni si riferiscono ai numeri normalizzati non
nulli; per i numeri denormalizzati e per lo zero, il bit implicito o esplicito vale
0.
Il formato Single Real usa 8 bit per la caratteristica e 23 bit
per la mantissa, per cui il bias è \(p = 2^{7} - 1 = 127\); per i numeri
normalizzati, gli estremi positivi (S=0) sono quindi:
\begin{align}
x_{min} &= 2^{-126} \cdot (1,00...0{0_2}) = 2^{-126} \cdot 1
= 2^{-126} \cr
x_{max} &= 2^{127} \cdot (1,11...1{1_2}) =
2^{127} \cdot (1 - 2^{-23}) = 2^{127} - 2^{104}
\end{align}
Il formato Double Real usa 11 bit per la caratteristica e 52 bit
per la mantissa, per cui il bias è \(p = 2^{10} - 1 = 1023\); per i numeri
normalizzati, gli estremi positivi (S=0) sono quindi:
\begin{align}
x_{min} &= 2^{-1022} \cdot (1,00...0{0_2}) = 2^{-1022} \cdot 1
= 2^{-1022} \cr
x_{max} &= 2^{1023} \cdot (1,11...1{1_2}) =
2^{1023} \cdot (1 - 2^{-52}) = 2^{1023} - 2^{971}
\end{align}
Il formato Extended Real usa 15 bit per la caratteristica e 64
bit per la mantissa, per cui il bias è \(p = 2^{14} - 1 = 16383\); per i numeri
normalizzati, gli estremi positivi (S=0) sono quindi:
\begin{align}
x_{min} &= 2^{-16382} \cdot (1,00...0{0_2}) = 2^{-16382} \cdot 1
= 2^{-16382} \cr
x_{max} &= 2^{16383} \cdot (1,11...1{1_2}) =
2^{16383} \cdot (1 - 2^{-64}) = 2^{16383} - 2^{16319}
\end{align}
Nel precedente capitolo è stato illustrato il metodo di codifica dei casi speciali;
indicando con S il segno, e la caratteristica (con bias), i il
bit implicito, m la mantissa, n i numeri normalizzati, d i numeri
denormalizzati e X i bit indefiniti, abbiamo allora la situazione mostrata in
Figura 17.15.
Il bit implicito per i tipi Single Real e Double Real non viene memorizzato
nella codifica binaria.
La mantissa per un sNaN deve essere diversa da zero.
Il valore Indefinite è un numero reale codificato come un particolare qNaN.
17.4.2 Numeri interi
La FPU supporta i tre formati di numeri interi mostrati in Figura 17.16.
Si tratta di numeri con segno rappresentati in complemento a 2; ogni formato
spazia quindi da \(0111...11_2\) (numero positivo più grande) a \(1000...00_2\)
(numero negativo più piccolo). In complemento a 2, come sappiamo, lo zero
viene rappresentato ponendo tutti i bit a 0, compreso il bit di segno; il
valore -0, infatti, corrisponde a \(1000...00_2\), che è il numero negativo
più piccolo.
Il formato Word Integer ha un'ampiezza di 16 bit, con il bit più
significativo che memorizza il segno; gli estremi sono quindi:
\[ x_{min} = -2^{15} \qquad x_{max} = +2^{15} - 1 \]
Il formato Short Integer ha un'ampiezza di 32 bit, con il bit più
significativo che memorizza il segno; gli estremi sono quindi:
\[ x_{min} = -2^{31} \qquad x_{max} = +2^{31} - 1 \]
Il formato Long Integer ha un'ampiezza di 64 bit, con il bit più
significativo che memorizza il segno; gli estremi sono quindi:
\[ x_{min} = -2^{63} \qquad x_{max} = +2^{63} - 1 \]
I numeri interi vengono convertiti in Extended Real prima di essere caricati
nello stack della FPU; qualunque numero intero conforme ai formati di Figura
17.16, è perfettamente rappresentabile come Extended Real.
Indicando con S il segno e con A l'ampiezza (15, 31 o
63 bit) del numero intero, in Figura 17.17 possiamo vedere la codifica binaria
dei tre formati di Figura 17.16.
Si può notare che la codifica di un intero Indefinite (operazione non valida) è
identica a quella del numero negativo più piccolo (\(-2^{15}, -2^{31}, -2^{63}\)); per
distinguere i due casi, è necessario consultare il flag IE del registro Status
Word di Figura 17.6.
17.4.3 Numeri Packed BCD
La FPU supporta il formato Packed BCD mostrato in Figura 17.18.
Come abbiamo visto nel Capitolo 18 del tutorial Assembly Base, questo formato
permette di gestire numeri interi con segno, in base 10, le cui cifre vengono
rappresentate in esadecimale attraverso un valore compreso tra 0h e 9h
(tra 0000b e 1001b); il byte più significativo contiene il segno, con
00000000b (00h) che codifica il segno positivo e 10000000b
(80h) il segno negativo. Lo zero può essere codificato, sia come -0,
sia come +0.
La distinzione tra numeri positivi e negativi avviene unicamente in base al segno;
non si tratta quindi di una rappresentazione in complemento a 2.
Mentre la CPU fornisce istruzioni che operano sulle singole cifre di un numero
BCD, Packed o Unpacked, la FPU supporta direttamente il
formato Packed BCD standard da 10 byte, definito dalla Intel
(Figura 17.18); in tale formato, i primi 9 byte contengono sino a 18 cifre
(ogni cifra esadecimale, infatti, occupa al massimo 4 bit), mentre il decimo byte
(il più significativo) contiene il segno. Nella codifica del segno, lo standard impone
che debba essere preso in considerazione solo il bit più significativo del decimo byte,
mentre gli altri sette bit, nelle posizioni da 0 a 6 (da 72 a
78 in Figura 17.18), devono essere trattati come indefiniti.
Con 18 cifre esadecimali, che simulano quelle decimali, abbiamo quindi i seguenti
estremi:
\[ x_{min} = -10^{18} + 1 \qquad x_{max} = +10^{18} - 1 \]
In pratica, possiamo rappresentare tutti i numeri interi decimali compresi tra
-999999999999999999 e +999999999999999999.
I numeri interi in formato Packed BCD vengono convertiti in Extended Real
prima di essere caricati nello stack della FPU; qualunque numero intero conforme
al formato di Figura 17.18, è perfettamente rappresentabile come Extended Real.
Indicando con S il segno, con A l'ampiezza (18 cifre esadecimali)
del numero intero Packed BCD e con X i bit indefiniti, in Figura 17.19
possiamo vedere la codifica binaria del formato di Figura 17.18.
Il BCD Indefinite è la conseguenza di una operazione non valida; la sua codifica
è data dal valore 11111111b nel nono e decimo byte, mentre i primi otto byte
sono indefiniti.
17.5 Gestione dei NaN
Nell'eseguire un calcolo con la FPU, può capitare di ottenere un risultato privo
di senso (o indeterminato) nel campo numerico reale; ad esempio, la radice quadrata di
un numero negativo. Si dice allora che tale risultato "non è un numero" (Not a
Number o NaN).
Nel capitolo precedente abbiamo visto che la FPU supporta due tipi di NaN:
il signaling NaN (sNaN) e il quiet NaN (qNaN). Quando viene
memorizzato nello stack della FPU, un sNaN ha il bit più significativo
della mantissa posto a 0, mentre nel caso del qNaN tale bit vale 1;
il bit di segno è indefinito per i NaN.
Nel caso in cui si verifichi un'operazione non valida, la FPU genera esclusivamente
un qNaN; tale qNaN viene trattato come un normale operando che può quindi
propagarsi "silenziosamente" nel corso di un calcolo (da cui il nome quiet NaN). Il
programmatore può alterare questa situazione trasformando il qNaN in un sNaN;
a tale proposito, bisogna servirsi dei flags presenti nei registri Status Word e
Control Word.
Supponiamo che un'istruzione produca un'operazione non valida; in tal caso, abbiamo
visto che la FPU pone a 1 il flag IE di Figura 17.6. Se il flag
IM di Figura 17.7 è posto a 1 (eccezione mascherata), la FPU
genera un qNaN ponendo a 1 il bit più significativo della mantissa
relativa al risultato non valido prodotto da un calcolo; il risultato stesso viene poi
salvato nell'operando destinazione dell'istruzione. Se il programmatore pone a 0
il bit IM, la FPU "segnala" un'eccezione di operazione non valida (da cui
il nome signaling NaN); il risultato non viene salvato nell'operando destinazione
dell'istruzione. Come conseguenza dell'eccezione di operazione non valida, viene
chiamato un gestore di eccezione (Exception Handler) appositamente predisposto
dal programmatore (o dal sistema operativo).
I bit della mantissa, ad eccezione di quello più significativo, possono essere usati
dai programmi per contenere informazioni diagnostiche relative al tipo di errore che
si è verificato; il gestore di eccezione può così prendere le decisioni opportune.
I qNaN vengono principalmente usati per scopi diagnostici (debugging);
a tale proposito, si trasforma il qNaN in un sNaN (come visto prima) in
modo che venga chiamato un apposito gestore di eccezione il cui scopo è quello di
memorizzare tutte le informazioni relative al problema che si è verificato. Lo stesso
gestore di eccezione, in seguito, ritrasforma il sNaN in qNaN in modo
che i calcoli possano proseguire; terminata la fase di elaborazione, si vanno a
controllare le informazioni memorizzate in precedenza per sapere se e dove si è
verificato un problema.
Ovviamente, il metodo appena descritto può essere applicato ripetutamente per ogni
errore che si verifica; come abbiamo visto prima, utilizzando i bit liberi della
mantissa possiamo codificare svariate informazioni in modo da distinguere tra loro i
vari NaN.
17.6 Gestione delle eccezioni
Nell'eseguire le varie istruzioni numeriche, la FPU è in grado di individuare
sei casi particolari a cui corrispondono le sei eccezioni seguenti (con i flags ad
esse associati nel registro Status Word di Figura 17.6):
- Invalid Operation (IE))
- Divide-By-Zero (ZE)
- Denormalized Operand (DE)
- Numeric Overflow (OE)
- Numeric Underflow (UE)
- Inexact Result (Precision) (PE)
L'eccezione Invalid Operation comprende lo Stack Fault (overflow e
underflow dello stack) e l'Invalid Arithmetic Operation; il flag SF
permette di distinguere tra questi due casi, con SF=0 che indica
un'operazione aritmetica non valida e SF=1 che indica uno stack fault (in
quest'ultimo caso, abbiamo già visto che C1=0 indica underflow, mentre
C1=1 indica overflow).
Al verificarsi di una di queste eccezioni, il comportamento della FPU dipende
dal corrispondente flag del registro Control Word di Figura 17.7; se il flag è
posto a 1 (eccezione mascherata), la FPU genera automaticamente un
risultato predefinito, mentre se il flag è posto a 0, la FPU chiama un
apposito Exception Handler predisposto dal programmatore (o dal sistema operativo).
17.6.1 Gestione automatica delle eccezioni
Abbiamo visto in precedenza che, in fase di inizializzazione della FPU, tutte
le eccezioni risultano mascherate; se lasciamo invariata questa situazione, stiamo
delegando alla stessa FPU la gestione dei vari casi particolari che si possono
presentare.
La FPU è predisposta per procedere nel modo più opportuno al verificarsi di una
eccezione mascherata; ad esempio, in precedenza è stato spiegato che, nel caso di una
operazione non valida, viene generato un qNaN. In presenza di una eccezione
mascherata, la FPU produce quindi un risultato predefinito, pone a 1 il
relativo flag del registro Status Word (Figura 17.6) e prosegue nella
elaborazione delle istruzioni numeriche; se vogliamo alterare questa situazione,
dobbiamo togliere la maschera al flag che ci interessa e procedere poi via software,
come descritto più avanti.
Si tenga presente che, nel corso delle elaborazioni, possono verificarsi più eccezioni;
se tali eccezioni sono tutte mascherate, i relativi flags vengono posti a 1 e
restano tali in modo che il programmatore possa esaminarli al termine dei calcoli.
17.6.2 Gestione via software delle eccezioni
Può capitare che il programmatore abbia la necessità di gestire direttamente una
determinata eccezione; in tal caso, abbiamo visto che è necessario disporre di un
apposito gestore (Exception Handler) a cui la FPU delega l'elaborazione
dell'eccezione stessa. Esistono due modi attraverso i quali la FPU chiama un
gestore di eccezione:
- native mode
- MS-DOS compatibility mode
Il modo operativo che ci interessa può essere selezionato attraverso il flag NE,
presente nel registro di controllo CR0 delle CPU di classe 80486 DX
o superiore, dotate di FPU integrata al loro interno; la Figura 17.20 illustra
la struttura di tale registro.
Se NE è posto a 1, viene utilizzato il modo nativo, tipico della
modalità protetta. Nel caso in cui si verifichi un'eccezione non mascherata, la
FPU pone a 1 i flags ES e quello relativo all'eccezione stessa
nel registro Status Word; a questo punto, viene chiamato il gestore di
eccezione attraverso un metodo del tutto simile a quello delle interruzioni in
modalità reale. Questi aspetti vengono trattati nel tutorial Modalità Protetta.
Se NE è posto a 0, viene utilizzato il classico metodo MS-DOS
per la gestione delle richieste di interruzione; come sappiamo, in questo caso una
periferica invia una IRQ al PIC 8259A, il quale determina il relativo
vettore di interruzione e poi gira la richiesta alla CPU.
Si tenga presente che il registro CR0 è presente anche sulle CPU 80386;
il bit NE però rimane inutilizzato in quanto viene impiegato solo il metodo
MS-DOS per la gestione delle eccezioni della FPU.
Supponiamo quindi che si verifichi una eccezione non mascherata; la FPU allora
pone a 1 i flags ES e quello relativo all'eccezione stessa nel registro
Status Word e poi invia una IRQ al PIC. Il PIC associa la
IRQ al relativo vettore di interruzione, il quale verrà poi usato dalla
CPU per chiamare una apposita ISR (Interrupt Service Routine);
come si vede nella Figura 3.11 del Capitolo 3, la IRQ predefinita è la numero
13 (eccezioni del coprocessore matematico) e il relativo vettore di interruzione
è il numero 75h.
Si tenga presente che il modo MS-DOS, anche nelle moderne CPU, risente
delle convenzioni adottate nel 1980 per i sistemi che usano una 8086 in
combinazione con una 8087. La Figura 17.1 illustra il metodo corretto (via
PIC 8259A) consigliato dalla Intel per permettere alla 8087 di
inviare una IRQ alla 8086; purtroppo, le cose non sono però andate in
quel modo. Il problema è che all'epoca i PC erano dotati di un unico PIC,
con tutti gli 8 ingressi già occupati; la IBM decise allora di connettere
l'uscita INT della 8087 all'ingresso NMI (Non Maskable
Interrupt) della 8086. Una IRQ generata dalla 8087 provoca
quindi la chiamata della ISR relativa ad una NMI (vettore n. 02h);
tale ISR, che si trova nel BIOS, è scritta in modo da verificare se la
richiesta di interruzione è una vera NMI o una eccezione della FPU. Se
si tratta di una eccezione della FPU, viene chiamata una apposita ISR.
Le CPU successive alla 8086 beneficiano della presenza di due PIC
8259A in cascata e le IRQ generate dalla FPU vengono inviate al
PIC Slave (IRQ13), che le associa al vettore di interruzione n.
75h; la relativa ISR, per mantenere la compatibilità con la 8087,
chiama il vettore di interruzione n. 02h (NMI). A questo punto, tutto si
svolge come ai tempi della 8087; se la ISR associata alla NMI si
accorge di avere a che fare con una eccezione della FPU, chiama un'altra
apposita ISR.
Nei tutorial Assembly Base e Assembly Avanzato, vengono presentati
piccoli programmi didattici che operano in modalità reale; di conseguenza, nel seguito
si farà riferimento al modo compatibile MS-DOS per la gestione delle eccezioni
della FPU. Come è stato spiegato in precedenza, generalmente le ISR per
la gestione delle eccezioni della FPU vengono fornite dal SO o dal
BIOS; in ogni caso, avendo a disposizione una moderna CPU, nessuno ci
impedisce di scrivere direttamente una nostra ISR per il vettore n. 75h,
evitando il passaggio intermedio attraverso il vettore n. 02h.
Tipicamente, un gestore di eccezione della FPU deve svolgere almeno una serie
di compiti predefiniti; in particolare, deve esaminare i vari registri (Status
Word, Control Word, Tag Word, etc) per determinare la natura del
problema che si è verificato. Avendo a disposizione tutte le necessarie informazioni,
il gestore di eccezione può anche tentare (se possibile) di correggere la causa del
problema; prima di restituire il controllo al programma che ha prodotto la IRQ,
è importante inoltre riportare a zero i bit di eccezione nella Status Word.
Analizziamo ora in dettaglio le condizioni che causano una eccezione della FPU.
17.6.3 Invalid Operation Exception
La FPU genera questa eccezione quando viene rilevato un risultato non valido di
una operazione aritmetica o nel caso di un errore nello stack (overflow o underflow);
i bit in posizione 0 (IE) e 7 (ES) del registro Status
Word vengono posti a 1.
Il flag SF permette di sapere che tipo di problema si è verificato; SF=0
indica una operazione aritmetica non valida, mentre SF=1 indica un errore nello
stack. Se SF=1, allora C1=0 indica underflow, mentre C1=1 indica
overflow.
Nel caso di stack fault, l'overflow si verifica quando un nuovo dato caricato nello
stack va a sovrascrivere un registro già occupato (lo stack era completamente pieno);
analogamente, l'underflow si verifica quando si tenta di estrarre dallo stack un dato
da un registro vuoto (lo stack era completamente vuoto).
Se IM=0 nel registro Control Word, la FPU chiama il gestore di
eccezione; se IM=1, la FPU carica il nuovo dato nello stack sovrascrivendo
il registro già occupato.
Il caso di operazione aritmetica non valida può essere dovuto a svariate cause; in
particolare, si possono citare tutte quelle operazioni considerate indeterminate in
matematica, come \((-\infty + \infty)\), \((\infty \div \infty\)), \((0 \div 0)\),
\((\infty \cdot 0)\), etc. Per un elenco completo delle cause che producono una
eccezione di operazione aritmetica non valida, si veda la documentazione indicata nella
bibliografia.
Se IM=0 nel registro Control Word, la FPU chiama il gestore di
eccezione; se IM=1, la FPU genera un risultato indefinito e prosegue
nella elaborazione delle istruzioni numeriche successive.
17.6.4 Divide By Zero Exception
La FPU genera questa eccezione quando viene eseguita una divisione il cui
dividendo è un numero finito non nullo, mentre il divisore è zero; il bit in
posizione 2 (ZE) del registro Status Word viene posto a 1.
Se ZM=0 nel registro Control Word, la FPU chiama il gestore di
eccezione; se ZM=1, la FPU genera un risultato infinito con segno (in
base alle solite regole della divisione tra numeri con segno) e prosegue nella
elaborazione delle istruzioni numeriche successive. Per maggiori dettagli si veda la
lista delle istruzioni della FPU nel capitolo successivo.
17.6.5 Denormalized Operand Exception
La FPU genera questa eccezione quando in un calcolo è presente un numero
denormalizzato oppure quando si tenta di caricare un numero denormalizzato nello stack
in formato Single o Double Real (se il formato è Extended Real, nessuna eccezione viene
generata); il bit in posizione 1 (DE) del registro Status Word
viene posto a 1.
Se DM=0 nel registro Control Word, la FPU chiama il gestore di
eccezione; se DM=1, la FPU normalizza l'operando (se è un numero
denormalizzato in formato Single o Double Real) convertendolo in formato Extended Real
e prosegue nella elaborazione delle istruzioni numeriche successive.
17.6.6 Numeric Overflow Exception
La FPU genera questa eccezione quando un calcolo produce un risultato che eccede
il valore (assoluto) massimo memorizzabile nell'operando destinazione; ciò vale anche
nel caso in cui l'operando destinazione sia lo stack. Queste considerazioni sono valide
solo quando si sta operando sui numeri reali; nel caso di numeri interi o Packed
BCD, un eventuale overflow produce una eccezione di operazione aritmetica non valida.
Il bit in posizione 3 (OE) del registro Status Word viene posto a
1.
Se OM=0 nel registro Control Word, la FPU chiama il gestore di
eccezione; se OM=1, la FPU produce un risultato che dipende dal modo di
arrotondamento in uso (vedere la seguente Figura 17.21) e prosegue nella elaborazione
delle istruzioni numeriche successive.
17.6.7 Numeric Underflow Exception
La FPU genera questa eccezione quando un calcolo produce un risultato inferiore
al valore (assoluto) minimo memorizzabile nell'operando destinazione; ciò vale anche
nel caso in cui l'operando destinazione sia lo stack. Queste considerazioni sono valide
solo quando si sta operando sui numeri reali; nel caso di numeri interi o Packed
BCD, un eventuale underflow produce una eccezione di operazione aritmetica non valida.
Il bit in posizione 4 (UE) del registro Status Word viene posto a
1.
Se UM=0 nel registro Control Word, la FPU chiama il gestore di
eccezione; se UM=1, la FPU produce un risultato denormalizzato in base al
modo di arrotondamento in uso. Se il risultato denormalizzato è esatto, la FPU
prosegue nella elaborazione delle istruzioni numeriche successive; se il risultato è
inesatto, la FPU esamina lo stato del flag PM (Precision) nel registro
Control Word.
17.6.8 Inexact Result (Precision) Exception
La FPU genera questa eccezione quando un calcolo produce un risultato che non è
rappresentabile nel formato richiesto; nel precedente capitolo, ad esempio, abbiamo
visto che 1/3 non è codificabile in binario in modo esatto (ciò accade perché
3 non può essere espresso come potenza intera di 2). Il bit in posizione
5 (PE) del registro Status Word viene posto a 1.
Se PM=0 nel registro Control Word, la FPU chiama il gestore di
eccezione; se PM=1, la FPU produce un risultato arrotondato in base al
modo di arrotondamento in uso e prosegue nella elaborazione delle istruzioni numeriche
successive. Se il risultato è stato arrotondato, la FPU pone C1=1; in
assenza di arrotondamento si ha C1=0.
Bibliografia
Intel® 64 and IA-32 Architectures Software Developer’s Manual - Volume 1 -
Basic Architecture
(disponibile nella sezione
Documentazione tecnica di supporto al corso assembly dell’
Area Downloads di questo sito - 253665-sdm-vol-1.pdf)
Intel387TM DX MATH COPROCESSOR
(disponibile nella sezione
Documentazione tecnica di supporto al corso assembly dell’
Area Downloads di questo sito - FPU80387.pdf)