Assembly Avanzato con MASM

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