Avevamo giĂ discusso di come il processore possa mascherare le interruzioni in arrivo tramite INTR.
Esistono quindi alti tipi di interruzioni inviate tramite fili NMI che non possono essere mascherate e sono sempre immediatamente eseguite.
Sulle NMI viaggiano tra le altre cose le eccezioni.
Le eccezioni, quando vengono sollevate, sono quindi gestite immediatamente attraverso routine.
Le routine delle eccezioni sono anchâesse salvate nella IDT, in particolare nelle prime 32 entrate. I loro tipi sono fissi e impliciti, consultabili nel manuale del processore.
Alcune eccezioni famose sono:
0: Divisione per zero1: Single-Step: viene avviato se il flag TF è settato.
Il processore genererĂ quindi unâeccezione alla fine di ogni istruzione eseguita.3: Eccezione di debug (istruzione int3)int3Ă grazie a questa che il debugger riesce a controllare il flusso del programma sul quale è eseguito.
Quando il debugger inserisce un breakpoint in un indirizzo, quello che fa operativamente è sostituire il primo byte a quellâindirizzo con il valore 0xcc, salvando il byte significativo.
Tramite il segnale di continue il debugger rilascia il controllo al programma, che eseguirĂ finchĂŠ non farĂ la fetch dellâeccezione.
Il controllo torna quindi al debugger, che opererĂ finchĂŠ il programmatore non restituirĂ il controllo al programma.
Prima di permettere al programmatore di agire sul codice, il debugger reinserisce il vecchio valore dove aveva salvato 0xcc, e setta il bit TF cosĂŹ da generare unâeccezione di single-step.
Il programma eseguirĂ quindi lâoperazione dove era stato chiamato il breakpoint per poi dare controllo nuovamente al debugger, che reinserirĂ il valore 0xcc cosĂŹ da mantenere il breakpoint per le successive iterazioni.
Il debugger resetta quindi TF e restituisce per lâultima volta il controllo al flusso principale
Mentre le interruzioni possono accedere solo tra unâistruzione e la successiva, le eccezioni possono essere sollevate in un momento qualunque di unâistruzione (lettura, decodifica, esecuzione).
Il loro sollevamento può quindi generare comportamenti diversi da quelli delle interruzioni, che quindi vanno gestiti opportunamente.
Il primo dilemma riguarda come far variare lo stato del processore quando durante un istruzione viene sollevata unâeccezione. Il secondo riguarda invece come riprendere lâesecuzione del programma.
Le eccezioni sono classificabili in tre gruppi, ognuno dei quali ha comportamenti diversi:
| Tipo | Quando viene generata | Indirizzo salvato | Scopo/Effetto |
|---|---|---|---|
Fault |
Durante lâesecuzione di unâistruzione | Indirizzo dellâistruzione che stava eseguendo | La routine dovrebbe sistemare il problema per poter rieseguire lâistruzione |
Trap |
Tra lâesecuzione di unâistruzione e la successiva | Indirizzo dellâistruzione successiva | - |
Abort |
In qualsiasi momento | - | Gestisce errori particolarmente gravi, tipicamente causa lo spegnimento del calcolatore |
Non siamo costretti ad utilizzare le eccezioni fornite dal processore o dalle librerie, ma possiamo scriverne di nostre:
#include <libce.h>
extern "C" void a_divPerZero();
extern "C" void c_divPerZero(natq rip) {
printf("Divisione per 0, all'indirizzo %lx!\n", rip);
}
int main() {
int b = 0;
gate_init(0, a_divPerZero);
/*
* Inizializzo la riga 0 della IDT(DivisionPerZeroFault)
* con la mia funzione
*/
int a = 3 / b;
}
.global divPerZero
divPerZero:
NOP
MOVq (%rsp), %rdi
CALL c_divPerZero
IRETq