Modalità Protetta
Capitolo 6: Modalità Protetta 80386 - Caratteristiche avanzate
In questo capitolo vengono illustrate le caratteristiche avanzate della modalità
protetta 80386; in particolare, si esaminano gli aspetti relativi ai meccanismi
di protezione, al multitasking, all'I/O con le porte hardware e alla gestione di
interruzioni ed eccezioni.
6.1 Protezione a livello dei segmenti
Nel Capitolo 2 abbiamo visto che i meccanismi di protezione assumono una notevole
importanza nei sistemi multitasking, in quanto evitano che i vari task in esecuzione
possano interferire tra loro; ogni task può specificare il proprio spazio di
indirizzamento privato e ciò permette alla CPU di bloccare eventuali tentativi
di accesso ad aree riservate. Questo aspetto diventa particolarmente significativo
quando si tratta di proteggere il SO da comportamenti anomali (o anche
malevoli) da parte dei vari task; in particolare, a differenza di quanto accade in
modalità reale, in modalità protetta un task che compie un'azione illegale (o che va
in crash), può essere terminato senza compromettere il normale funzionamento dello
stesso SO.
Abbiamo anche visto che un importante effetto collaterale della protezione è dato dal
fatto che viene notevolmente agevolata la ricerca di eventuali bug nei programmi; un
task che, ad esempio, tenta erroneamente di far puntare SS:ESP ad uno stack non
accessibile in scrittura, provoca un'eccezione della CPU, accompagnata da
informazioni dettagliate che aiutano il programmatore ad individuare e correggere il
problema.
Quando i meccanismi di protezione sono attivi, prima di eseguire una qualsiasi
istruzione che comporta un accesso alla memoria, la CPU effettua una serie
di verifiche il cui scopo è impedire che un task compia eventuali azioni non
consentite; tali verifiche si concentrano su:
- tipo di segmento
- limiti di un segmento
- restrizioni sulla memoria indirizzabile
- restrizioni sugli entry point delle procedure
- restrizioni sul set di istruzioni
Qualunque violazione riscontrata dalla CPU durante queste verifiche,
produce un'eccezione.
Per stabilire se si sta per verificare una violazione della protezione, la
CPU si serve di una serie di informazioni ricavate da appositi descrittori,
i quali si suddividono in:
- descrittori di segmento
- descrittori speciali
- descrittori di gates
Un generico descrittore è un blocco da 8 byte la cui struttura generale è
illustrata in Figura 6.1; tale struttura può variare notevolmente a seconda del tipo
di descrittore.
Nel caso, ad esempio, di un descrittore di segmento, le informazioni di cui la
CPU ha bisogno sono contenute nell'ACCESS BYTE, nei campi
BASE e LIMIT e nei vari attributi come i bit G e D/B;
ogni volta che carichiamo un selettore in un registro di segmento, la CPU
accede al relativo descrittore, legge tali informazioni e le salva nella parte
nascosta del registro stesso. La Figura 6.2 mostra il caso del registro DS.
Grazie a questa tecnica, ogni accesso in memoria che coinvolge un registro di
segmento comporta una serie di controlli di protezione che, di fatto, avvengono
via hardware, con un impatto minimo sulla velocità di esecuzione.
6.1.1 Verifiche sul tipo di segmento
Ogni segmento è destinato ad uno scopo ben preciso, secondo le regole specificate
dal campo TYPE nel relativo descrittore; la CPU effettua una serie di
verifiche per assicurarsi che tali regole vengano rispettate.
Nell'ACCESS BYTE di Figura 6.1, i descrittori di segmento hanno S=1,
mentre quelli speciali e di gates hanno S=0; un descrittore di gate è destinato
a contenere un puntatore all'entry point di una procedura (che può essere anche
una ISR) o di un TSS.
Per i descrittori speciali e di gates (S=0), il valore contenuto nel campo
TYPE dell'ACCESS BYTE assume il significato mostrato dalla tabella di
Figura 6.3.
Nel caso invece dei descrittori di segmento (S=1), abbiamo visto nel
precedente capitolo che il campo TYPE indica le caratteristiche del
segmento stesso; la Figura 6.4 mostra l'ACCESS BYTE per i segmenti di
codice.
La Figura 6.5 mostra l'ACCESS BYTE per i segmenti di dati/stack.
Ad esempio, W=1 indica che un segmento di dati è accessibile anche in
scrittura, mentre R=1 indica che un segmento di codice è accessibile
anche in lettura; in genere, si pone R=1 quando un segmento di codice
contiene anche dati.
Per accedere, esclusivamente in lettura, a dati presenti in un segmento di codice
con R=1, si può usare direttamente CS (segment override) o si può
far puntare al segmento stesso uno dei registri DS, ES, FS,
GS.
Ogni volta che carichiamo un selettore in un registro di segmento, la CPU
si assicura che vengano rispettate una serie di regole; in particolare:
- in CS si può caricare solo il selettore di un segmento eseguibile
- in DS, ES, FS o GS si può caricare il selettore
di un segmento eseguibile solo se tale segmento ha R=1
- in SS si può caricare solo il selettore di un segmento di dati con
W=1
Ogni volta che accediamo in memoria, la CPU si assicura che un segmento non
venga usato in modo improprio; in particolare:
- è categoricamente proibito scrivere in un segmento eseguibile
- non si può scrivere in un segmento di dati con W=0
- non si può leggere da un segmento di codice con R=0
Essendo proibito l'accesso in scrittura ad un segmento di codice, eventuali
dati contenuti in esso non sono modificabili (dati costanti).
6.1.2 Verifiche sul campo LIMIT di un descrittore di segmento
Le verifiche sul campo LIMIT hanno lo scopo di impedire che un task possa
leggere o scrivere in aree della memoria non appartenenti al proprio spazio di
indirizzamento privato.
Come abbiamo visto nel precedente capitolo, il valore reale del campo LIMIT
dipende dal bit G (Granularity), dal bit D/B (Default/Big)
e, per i segmenti di dati, dal bit ED (Expand Down); per il momento
assumiamo D/B=1 (modo 32 bit) e ED=0 (segmenti di dati Expand
Up).
Se G=0, i 20 bit del campo LIMIT contengono un valore in
BYTE, compreso quindi tra 0 e FFFFFh (tra 0 e
220-1); gli offset vanno da 0 a LIMIT, per cui il
segmento minimo è formato da 1 byte (il solo offset 0), mentre il
massimo è 1 MiB (offset da 0 a FFFFFh).
Se G=1, il valore contenuto nei 20 bit del campo LIMIT viene
scalato di un fattore 212 (4096), per cui la dimensione di
un segmento risulta essere un multiplo intero di 4096 byte; si tenga presente
che in questo caso, il limite effettivo è dato da (LIMIT+1)*4096-1. Il segmento
minimo quindi è formato da 4096 byte (offset da 0 a 4095),
mentre il massimo è 4 GiB (offset da 0 a (FFFFFh+1)*4096-1).
Per i segmenti di dati Expand Down (ED=1), il significato del campo
LIMIT rimane inalterato, ma cambia completamente l'area indirizzabile; gli
indirizzi validi sono quelli non validi nei segmenti con ED=0 e viceversa.
Se G=0, gli offset validi vanno da LIMIT+1 a 220-1.
Se G=1, gli offset validi vanno da (LIMIT+1)*4096 a
232-1.
Si può notare che quando ED=1, per avere il segmento più grande possibile
bisogna porre LIMIT=0.
Qualsiasi tentativo di accedere ad un'area della memoria al di fuori dei limiti
prestabiliti, provoca un'eccezione della CPU.
Tutte queste considerazioni sono valide anche per le tabelle come la GDT e
le varie LDT; in questi casi, come sappiamo, le parti nascoste dei registri
GDTR e LDTR riservano 16 bit al campo LIMIT in quanto
ogni tabella può contenere al massimo 8192 descrittori da 8 byte
ciascuno, per un totale di 64 KiB. Per la corretta gestione di una tabella
con N descrittori, è fondamentale quindi impostare il campo LIMIT a
8*N-1.
6.1.3 Livelli di privilegio
In presenza di una istruzione che prevede un salto verso un differente segmento di
codice o un accesso ad un segmento di dati, la CPU effettua ulteriori
verifiche finalizzate a stabilire se il task correntemente in esecuzione ha tutte
le necessarie autorizzazioni; a tale proposito, si ricorre come sappiamo ad un
meccanismo basato sui livelli di privilegio.
Abbiamo visto che sono disponibili quattro livelli di privilegio, numerati da
0 a 3, con 0 che rappresenta il privilegio più alto e 3
il più basso; la regola generale prevede che un task che gira con livello di
privilegio n, non possa accedere (o saltare) ad un altro segmento il cui
livello di privilegio sia inferiore a n (non si può cioè accedere ad un
segmento più privilegiato).
Grazie a questo meccanismo, è possibile ad esempio assegnare al SO il massimo
livello di privilegio (0), mentre i normali programmi vengono fatti girare al
minimo livello di privilegio (3); ai device driver (software di gestione
delle varie periferiche, come hard disk, lettori CD/DVD, tastiere, etc), in genere
vengono assegnati livelli di privilegio intermedi (1 o 2). Con questo
tipo di gerarchia, il SO risulta adeguatamente isolato dai device driver e
dai programmi; in particolare, il crash di un programma o di un device driver non
pregiudica l'operatività del SO.
Come si vede in Figura 6.1, ogni descrittore di segmento specifica il proprio
livello di privilegio, che rappresenta quindi il Descriptor Privilege Level
(DPL). Non appena inizia la fase di esecuzione di un task, il DPL del
relativo segmento di codice diventa il Current Privilege Level (CPL)
e viene memorizzato nei due bit meno significativi della parte visibile di CS,
come illustrato dalla Figura 6.6; come al solito, TI indica in quale tabella
(GDT o LDT) è contenuto il descrittore del segmento di codice in
esecuzione, mentre INDEX è l'indice del descrittore nella tabella stessa.
Durante la fase di esecuzione, il CPL di un task può variare (ad esempio, in
seguito alla chiamata di una procedura definita in un altro segmento con differente
DPL); ogni nuovo valore assunto dal CPL viene salvato nei due bit meno
significativi di CS.
Consideriamo, ad esempio, un task con CPL=2 che chiama una procedura definita
in un segmento di codice con DPL=3; nello stack viene salvato il vecchio
contenuto di CS (che ha CPL=2), mentre la procedura viene eseguita con
il nuovo contenuto di CS (che ha CPL=3). Il CPL del task quindi
vale inizialmente 2, poi diventa 3 quando viene chiamata la procedura
e infine torna a 2 quando la procedura stessa restituisce il controllo.
Per motivi di sicurezza, un task che gira a livello di privilegio n è
obbligato ad usare uno stack con lo stesso livello di privilegio; ne consegue che
un task deve predisporre tanti stack quanti sono i livelli di privilegio assunti
durante la fase di esecuzione!
Nel caso dell'esempio precedente, il task deve quindi disporre di uno stack per il
livello di privilegio 2 e uno per il 3.
Le considerazioni appena esposte comportano che lo stesso CPL presente nei
due bit meno significativi di CS, venga memorizzato anche nei corrispondenti
bit del registro SS; in sostanza, SS punta ad uno stack che ha lo
stesso livello di privilegio del task in esecuzione.
Il livello di privilegio è presente anche nei due bit meno significativi dei
selettori da caricare nei registri di segmento per i dati; in tal caso, come si
vede in Figura 6.7, prende il nome di "livello di privilegio desiderato" o
Requested Privilege Level (RPL).
Come sappiamo, il RPL rappresenta il livello di privilegio di chi ha creato
il selettore; ciò è necessario per evitare che un task, a causa di un bug o anche
in malafede, possa usare in modo improprio i selettori stessi (che sono veri e
propri puntatori, a segmenti, a procedure, ad altri task, etc).
Consideriamo, ad esempio, un servizio del SO che gira con privilegio
0 e richiede come parametro un puntatore (selettore) ad un buffer nel quale
scrivere dei dati; un task con CPL=3 che chiama quel servizio potrebbe anche
passare un selettore con RPL=0, che punta ad un'area riservata dello stesso
SO. In assenza di precauzioni, il servizio chiamato ha a disposizione un
selettore che permette l'accesso in scrittura al massimo livello di privilegio;
lo stesso servizio quindi può scrivere i dati richiesti nell'area riservata,
provocando un disastro!
Per evitare questo problema, il servizio chiamato può "aggiustare" il RPL
del selettore ricevuto come parametro, rendendolo uguale al CPL del
caller (che nell'esempio appena illustrato è uguale a 3); come abbiamo visto
in precedenza, il CPL del caller si trova nel vecchio contenuto di CS
salvato nello stack al momento di chiamare il servizio.
Le verifiche che la CPU esegue sui livelli di privilegio, differiscono a
seconda del tipo di segmento; dobbiamo considerare quindi i due casi, rappresentati
dai segmenti di codice e di dati.
6.1.4 Restrizioni sull'accesso ai segmenti di dati
Se vogliamo accedere ad una locazione di memoria presente in un segmento di dati,
dobbiamo caricare il relativo selettore in uno dei registri dedicati, DS,
ES, FS o GS (oppure SS se la locazione si trova nello
stack); prima di caricare il selettore, la CPU deve assicurarsi che il task
in esecuzione abbia tutti i necessari requisiti.
Vengono esaminati i seguenti tre livelli di privilegio:
- Il CPL del task in esecuzione; tale informazione è contenuta nei due
bit meno significativi di CS (Figura 6.6).
- Il DPL presente nel descrittore del segmento di dati a cui vogliamo
accedere.
- Il RPL del selettore che punta al descrittore del segmento di dati a
cui vogliamo accedere; tale informazione viene salvata nei due bit meno significativi
del registro di segmento utilizzato (nel caso di SS si ha RPL=CPL).
Il caricamento del selettore nel registro di segmento viene autorizzato solo se,
numericamente:
DPL ≥ max(CPL, RPL)
In sostanza, il task in esecuzione deve avere almeno gli stessi privilegi del
segmento di dati a cui vuole accedere; ad esempio, se un task ha CPL=3,
anche se nel selettore da caricare in un registro di segmento impostiamo
RPL=0, possiamo accedere solo ad un segmento di dati con DPL=3 in
quanto il valore massimo tra CPL e RPL è max(3,0)=3.
I segmenti di dati a cui un task è autorizzato ad accedere rappresentano il suo
dominio indirizzabile. Chiaramente, il dominio indirizzabile di un task
varia al variare del suo CPL. Se CPL=0, il task può accedere a
qualsiasi segmento di dati di sua proprietà; se CPL=3, solo i segmenti di
dati con DPL=3 risultano accessibili.
6.1.5 Restrizioni sull'accesso ai dati presenti nei segmenti di codice
Come accade in modalità reale, anche in modalità protetta è possibile avere
segmenti di codice che contengono dati; essendo vietato in questo caso l'accesso
in scrittura, tali dati risultano essere solo leggibili e quindi non modificabili
(dati costanti).
In realtà, esiste un modo per accedere anche in scrittura ai dati presenti in un
segmento di codice appartenente ad un task; a tale proposito, il task deve definire
un segmento di dati scrivibile (W=1), sovrapposto allo stesso segmento di
codice.
Per accedere ai dati presenti in un segmento di codice, sono possibili i seguenti
tre metodi:
- Caricare in un registro di segmento dati il selettore di un segmento di
codice avente C=0 (non conforme), R=1 (leggibile), E=1
(eseguibile).
- Caricare in un registro di segmento dati il selettore di un segmento di
codice avente C=1 (conforme), R=1 (leggibile), E=1
(eseguibile).
- Usare direttamente CS tramite segment override (CS deve
già puntare al segmento di codice avente R=1, in cui sono presenti i
dati da leggere).
In relazione al metodo 1, ci troviamo nella stessa situazione descritta
in 6.1.4 per l'accesso ad un segmento di dati.
Il metodo 2 è sempre possibile in quanto stiamo accedendo ad un segmento
di codice conforme (C=1); come sappiamo, infatti, in questo caso l'accesso
avviene con lo stesso CPL del task che deve leggere i dati, anche se il
segmento acceduto ha DPL<CPL.
Anche il metodo 3 è sempre valido, in quanto stiamo usando CS che
punta ad un segmento di codice il cui DPL non è altro che il CPL
(contenuto nello stesso CS) del task correntemente in esecuzione.
6.1.6 Restrizioni sul trasferimento del controllo
Le istruzioni come JMP, CALL, RET, INT, IRET,
così come le eccezioni e le interruzioni hardware, provocano come sappiamo un
trasferimento del controllo; la fase di esecuzione salta dall'indirizzo corrente
ad un indirizzo destinazione.
Nel caso di salto NEAR, si rimane sempre all'interno dello stesso segmento
di codice, per cui il livello di privilegio non cambia; la CPU deve solo
verificare che il salto stesso rispetti il campo LIMIT (contenuto nella
parte nascosta di CS).
Nel caso di salto FAR, l'indirizzo destinazione si trova in un differente
segmento, per cui la CPU deve anche verificare il rispetto dei livelli di
privilegio; questo tipo di salto può essere effettuato con JMP o CALL
attraverso due metodi:
- L'operando dell'istruzione seleziona il descrittore di un altro segmento
di codice.
- L'operando dell'istruzione è un Call Gate.
In presenza del primo metodo, la CPU esamina il CPL del task in
esecuzione e il DPL del segmento di codice a cui saltare; la regola generale
prevede che si debba avere CPL=DPL. In questo caso sono consentite entrambe
le istruzioni JMP e CALL.
Se CPL>DPL (salto verso un segmento di codice più privilegiato), si
possono ugualmente usare entrambe le istruzioni JMP e CALL, purché
il segmento di codice a cui saltare sia conforme (C=1); in questo caso,
infatti, abbiamo visto che il codice presente nel segmento di destinazione del
salto viene eseguito con lo stesso livello di privilegio (CPL) del caller.
I segmenti conformi di codice in genere vengono utilizzati per definire librerie
di procedure destinate ai normali programmi, quando l'esecuzione di tali procedure
non comporta l'accesso a parti protette del SO; se non venisse rispettata
questa limitazione, si verrebbe a creare una situazione di evidente vulnerabilità!
L'uso dell'istruzione JMP è categoricamente proibito quando il segmento
di codice a cui saltare è non conforme (C=0) e ha un DPL diverso
dal CPL del caller!
In modalità protetta però, si presenta spesso il caso di un task che deve chiamare
delle procedure di servizio, definite in un differente segmento di codice che ha
C=0 (non conforme) e DPL<CPL (privilegi maggiori del caller); in
una situazione del genere, si può ricorrere all'istruzione CALL il cui
operando è un selettore che punta al descrittore di un Call Gate.
6.1.7 Call Gates
Un Call Gate permette ad un programma di chiamare in modo sicuro una procedura
definita in un segmento di codice non conforme e più privilegiato; esempi di tali
procedure possono essere i servizi forniti dai device driver o dal SO.
L'operando dell'istruzione CALL è una coppia Selector:Offset, dove
la componente Offset viene ignorata; la componente Selector punta ad un
descrittore di Call Gate che può risiedere nella GDT o in una LDT
(ma non nella IDT) e assume la struttura mostrata in Figura 6.8.
I 3 bit colorati in grigio sono riservati e devono valere 0.
Il campo SEGMENT SELECTOR a 16 bit è un selettore che punta al
descrittore (nella GDT o in una LDT) del segmento di codice dove
è definita la procedura da chiamare.
Il campo OFFSET a 32 bit è l'offset da cui parte la procedura
(entry point).
Il campo DWORD COUNT a 5 bit indica il numero di DWORD (da
0 a 31) da trasferire dal vecchio al nuovo stack.
Nell'ACCESS BYTE:
Il campo TYPE deve valere 1100b per la 80386 e 0100b
per la 80286.
Il campo S deve valere 0 (special descriptor).
Il campo DPL è il livello di privilegio minimo richiesto al caller.
Il campo P indica se il segmento di codice da chiamare è presente in
memoria (P=1) o è stato deallocato (P=0).
Prima di autorizzare la chiamata della procedura, la CPU prende in esame
i seguenti quattro livelli di privilegio:
- Il CPL contenuto nei due bit meno significativi di CS.
- Il RPL contenuto nel selettore usato come operando della CALL.
- Il DPL del descrittore di Call Gate (DPL_SRC).
- Il DPL del descrittore del segmento di codice a cui saltare (DPL_DEST).
La chiamata è valida solo se:
max(CPL, RPL) ≤ DPL_SRC
e
DPL_DEST ≤ CPL
Se il segmento di codice a cui saltare è conforme (C=1), oltre a CALL
è possibile usare anche JMP; se, invece, C=0, si può usare anche
JMP solo se:
max(CPL, RPL) ≤ DPL_SRC
e
DPL_DEST = CPL
Se queste condizioni sono soddisfatte, la chiamata della procedura è valida e
avviene secondo il meccanismo mostrato in Figura 6.9.
L'operando Selector della CALL seleziona il descrittore di un Call
Gate nella GDT o in una LDT. Il campo SEGMENT SELECTOR del
Call Gate seleziona, nella GDT o in una LDT, il descrittore del
segmento di codice in cui è definita la procedura da chiamare. Il campo OFFSET
del Call Gate rappresenta l'entry point della procedura stessa.
6.1.8 Stack Switching
La chiamata tramite Call Gate di una procedura più privilegiata, comporta i
seguenti tre passi:
- Il CPL del caller cambia e diventa uguale al DPL del
segmento in cui è definita la procedura.
- Il controllo viene trasferito alla procedura.
- Viene selezionato un nuovo stack con lo stesso livello di privilegio
della procedura.
Eventuali parametri richiesti dalla procedura, vengono copiati dal vecchio al
nuovo stack; il numero (da 0 a 31) di DWORD da copiare viene
specificato dal campo DWORD COUNT di Figura 6.8. Se si devono copiare più
di 31 DWORD, si può sfruttare vecchio contenuto di SS:ESP salvato
nel nuovo stack; in alternativa, uno dei parametri da passare può essere un
puntatore al blocco di ulteriori dati da copiare dal vecchio stack.
Al termine della procedura, il vecchio stack viene ripristinato e il controllo
ritorna al caller.
Come è stato spiegato nel Capitolo 2, per effettuare lo stack switching, la
CPU legge i puntatori ai nuovi stack dal Task State Segment
(TSS) del task in esecuzione; nel caso della 80386, vedremo più
avanti che tali puntatori sono coppie Selector:Offset da 16+32 bit
da copiare in SS:ESP.
Il TSS fornisce appositi campi destinati a contenere i puntatori ai nuovi
stack per i livelli di privilegio 0, 1 e 2; la CPU
seleziona il nuovo stack in base al DPL del segmento che contiene la
procedura da chiamare.
Il descrittore del nuovo segmento di stack deve avere un DPL uguale al
livello di privilegio (che diventa il nuovo CPL) della procedura da
chiamare; in caso contrario, viene generata un'eccezione (Stack Fault).
La CPU si assicura che il nuovo stack abbia spazio a sufficienza per
contenere il vecchio SS:ESP, gli eventuali parametri da passare e il
vecchio CS:EIP (indirizzo di ritorno); se questo requisito non è
soddisfatto, viene generata un'eccezione.
I 16 bit di SS vengono estesi a 32 bit mediante zeri, prima di
essere inseriti nel nuovo stack; tali zeri sono di competenza della CPU
e non devono essere modificati!
In seguito vengono inseriti nel nuovo stack gli eventuali parametri richiesti
dalla procedura; infine, viene inserito il vecchio contenuto di CS:EIP,
che rappresenta l'indirizzo di ritorno (anche in questo caso, il contenuto di
CS viene esteso a 32 bit mediante zeri).
La Figura 6.10 mostra un esempio pratico relativo ai concetti appena esposti.
Nell'esempio di Figura 6.10, supponiamo che un task con CPL=3 chiami tramite
call gate una procedura con livello di privilegio 1; se tutti i requisiti
sono soddisfatti, la CPU seleziona dal TSS il puntatore al nuovo stack
con DPL=1 e copia nel nuovo stack tutto il necessario (vecchio contenuto di
SS:ESP, parametri e vecchio contenuto di CS:EIP). Al termine della
procedura, viene ripristinato il vecchio stack con DPL=3.
Si noti che il TSS riserva spazio solo ai puntatori agli stack con livelli
di privilegio 0, 1 e 2; non avrebbe senso un puntatore ad uno
stack con livello di privilegio 3 nel TSS in quanto non esistono
task che girano con livello di privilegio 4 o maggiore.
6.1.9 Ritorno da una procedura
Per il ritorno da una procedura, valgono considerazioni analoghe a quanto esposto
in precedenza per il trasferimento del controllo.
Nel caso di un ritorno di tipo NEAR, restiamo sempre all'interno dello stesso
segmento, per cui non si verifica alcun cambio di privilegio; l'indirizzo di ritorno,
costituito da un offset a 32 bit, viene estratto dal nuovo stack e copiato in
EIP, mentre il contenuto di CS rimane invariato. La CPU deve
solo verificare che il contenuto dello stesso EIP non ecceda il campo
LIMIT.
Se il ritorno è di tipo FAR, stiamo saltando ad un differente segmento di
codice; una coppia da 16+32 bit viene estratta dal nuovo stack e copiata in
CS:EIP per avere l'indirizzo di ritorno.
In teoria, non ci dovrebbero essere problemi legati ai privilegi in quanto stiamo
tornando ad un segmento da cui ha avuto origine un salto valido tramite CALL
o INT; la CPU però effettua ugualmente delle verifiche perché la
procedura chiamata potrebbe aver alterato il selettore da caricare in CS.
In particolare, la CPU si assicura che il RPL del selettore da
caricare in CS coincida con il CPL del caller (coincida cioè con il
DPL del segmento di codice in cui è presente il caller).
In generale, un ritorno di tipo FAR ci riporta ad un segmento di codice che
ha privilegi minori o al più uguali a quelli della procedura chiamata. Come al
solito, se il livello di privilegio non cambia durante il ritorno, la CPU
effettua verifiche solo sul campo LIMIT; se, invece, il ritorno comporta
una variazione del privilegio, la CPU effettua le verifiche mostrate in
Figura 6.11 (il tipo di eccezione è indicato con le abbreviazioni illustrate nei
precedenti capitoli).
Se le verifiche danno esito positivo, i registri CS, EIP, SS,
ESP vengono caricati con i corrispondenti valori estratti dal nuovo stack.
In presenza di parametri passati alla procedura chiamata, il numero di BYTE
specificato dall'istruzione RET provvede alla pulizia del vecchio stack e al
conseguente aggiornamento del contenuto di ESP.
Prima di restituire il controllo, la CPU verifica anche il contenuto dei
registri DS, ES, FS e GS, in quanto la procedura
chiamata potrebbe averli utilizzati per accedere a segmenti di dati, caricando in
essi dei selettori il cui RPL è inferiore al CPL del caller; in tal
caso, la stessa CPU carica il NULL SELECTOR nei registri di segmento
interessati. Come sappiamo, un tentativo di utilizzare un registro di segmento dati
contenente il NULL SELECTOR, provoca un'eccezione (#GP) della CPU.
Tutto ciò è necessario per evitare che al caller vengano restituiti dei registri di
segmento contenenti selettori che permettono di accedere a segmenti di dati più
privilegiati.
6.1.10 Restrizioni sul set di istruzioni
Tutte le istruzioni legate alla gestione della modalità protetta, possono essere
eseguite solo da task che girano al massimo livello di privilegio; in genere, tali
istruzioni sono di competenza del SO.
Le istruzioni riservate si suddividono in due categorie:
- istruzioni sensibili destinate alle operazioni di I/O
- istruzioni privilegiate destinate al controllo del sistema
Le istruzioni sensibili sono quelle destinate alla gestione delle operazioni
di I/O con le porte hardware e vengono descritte più avanti.
Le istruzioni privilegiate, per essere eseguite, richiedono CPL=0; se
ciò non accade, viene generata un'eccezione.
Questa istruzione pone TS=0 nel registro MSW; come abbiamo visto
nella Figura 5.12 del Capitolo 5, sulla 80386 tale registro occupa la
WORD meno significativa di CR0.
Il flag TS viene posto automaticamente a 1 dalla CPU ogni volta
che si verifica un task switch; ciò è necessario perché il task switch stesso può
verificarsi proprio mentre era iniziata la fase di decodifica ed esecuzione di una
istruzione della FPU. Una istruzione della FPU eseguita con TF=1
produce una eccezione 7 (Processor Extension Not Available); la
ISR che intercetta questa eccezione può così verificare se la FPU è in
uso al task corrente (quello conseguente al task switch) o ad un altro task. Se la
FPU era in uso ad un altro task, il suo stato deve essere salvato in modo da
poter essere ripristinato quando il nuovo task restituisce il controllo; in questo
modo si evita che il nuovo task, usando a sua volta la FPU, ne alteri lo stato
precedente.
Questa istruzione sospende l'esecuzione delle istruzioni da parte della 80386.
L'operatività della CPU può essere ripristinata dall'arrivo di una richiesta
di interruzione hardware (compresa una NMI) o da un comando RESET; nel
caso in cui arrivi una richiesta di interruzione hardware, la coppia CS:(E)IP
punta all'istruzione successiva a HLT.
L'istruzione LGDT richiede come unico operando una locazione di memoria da
6 byte (48 bit) da caricare nel GDTR; i 16 bit meno
significativi rappresentano il campo LIMIT della GDT, mentre i
32 bit più significativi rappresentano il campo BASE. Nel modo
16 bit, gli 8 bit più significativi del campo BASE vengono
ignorati (il Base Address è a 24 bit).
LGDT è un'istruzione di sistema, disponibile però anche in modalità reale in
quanto il suo scopo è predisporre l'ambiente operativo per il passaggio alla modalità
protetta. Una volta entrati in modalità protetta, eventuali modifiche della GDT
sono sconsigliate; se necessario, possono essere effettuate con LGDT solo da
un task che gira al massimo livello di privilegio.
L'istruzione LIDT è del tutto analoga a LGDT; il suo scopo è
inizializzare il registro IDTR.
L'istruzione LLDT richiede come unico operando un registro o una locazione di
memoria da 16 bit da caricare nella parte visibile del LDTR; tale
operando è un selettore che contiene l'indice del descrittore della LDT nella
GDT. La CPU accede al descrittore, legge i campi BASE e
LIMIT e li copia nella parte invisibile del LDTR; tutte le altre parti
del descrittore vengono ignorate.
LLDT è un'istruzione di sistema destinata alla modalità protetta; può essere
utilizzata solo da un task che gira al massimo livello di privilegio.
In un sistema multitasking, LLDT viene utilizzata per caricare nel LDTR
il descrittore della LDT del primo task da eseguire; ad ogni cambio di task
(task switch), la CPU provvede automaticamente ad aggiornare il LDTR.
Questa istruzione richiede come unico operando un registro o una locazione di
memoria a 16 bit il cui contenuto rappresenta la Machine Status Word
da caricare nella WORD bassa del registro CR0. Se si sta scrivendo
un programma espressamente per la 80386, si consiglia di evitare l'uso di
LMSW; al suo posto si deve ricorrere alla apposita variante di MOV
(descritta più avanti) che opera direttamente sui registri di controllo.
L'istruzione LTR richiede come unico operando un registro o una locazione di
memoria da 16 bit da caricare nella parte visibile del Task Register
(TR); tale operando è un selettore che contiene l'indice del descrittore del
Task State Segment (TSS) nella GDT. La CPU accede al
descrittore, legge i campi BASE e LIMIT e li copia nella parte
invisibile del TR; tutte le altre parti del descrittore vengono ignorate.
L'istruzione LTR è di competenza del SO e viene usata per inizializzare
il registro TR quando si entra in modalità protetta; le successive modifiche
di TR vengono gestite in automatico ad ogni task switch.
Questa forma speciale di MOV permette di effettuare operazioni di lettura o
scrittura dei registri speciali, che comprendono i registri di controllo, di debug
e di test. Uno degli operandi deve essere un Reg32; l'altro operando è uno
dei registri di controllo CR0, CR2, CR3 o uno dei registri di
debug DR0, DR1, DR2, DR3, DR6, DR7 o uno
dei registri di test TR6, TR7.
6.1.11 Restrizioni sull'uso dei puntatori
Abbiamo visto che i selettori possono essere equiparati a veri e propri puntatori
in quanto i descrittori ad essi associati contengono gli indirizzi di segmenti,
procedure, LDT, TSS, etc; un selettore di Call Gate, ad esempio,
fa riferimento ad un descrittore contenente una coppia Base:Offset che punta
all'entry point di una procedura. Da queste considerazioni appare evidente
quindi che un uso improprio dei puntatori potrebbe compromettere i meccanismi di
protezione basati sui livelli di privilegio!
Proprio per evitare questo tipo di problema, ogni puntatore che un task utilizza
per accedere ad altri segmenti deve essere sottoposto a validazione; a tale
proposito, si rendono necessari i seguenti passi:
- verificare che chi ha fornito il puntatore sia autorizzato ad accedere
al segmento
- verificare che il segmento non venga usato in modo improprio
- verificare che l'indirizzo a cui accedere nel segmento non ecceda i limiti
La 80386, come è stato illustrato in precedenza, esegue in automatico i passi
2 e 3, mentre il passo 1 richiede la collaborazione del
programmatore; lo stesso programmatore può intervenire anche sui punti 2 e
3 per prevenire eventuali condizioni di errore tali da provocare un'eccezione
della CPU.
Per la validazione dei puntatori, la 80386 rende disponibili una serie di
istruzioni illustrate nel seguito.
Questa istruzione richiede come operando sorgente il selettore di un descrittore di
cui si vuole esaminare la DWORD più significativa; come si vede in Figura
6.1, tale DWORD contiene l'ACCESS BYTE e gli attributi.
Il descrittore deve avere un campo TYPE valido e deve essere accessibile dal
task che sta eseguendo LAR; a tale proposito, considerando il CPL del
task, il RPL del selettore e il DPL del descrittore, si deve avere
CPL≤DPL e RPL≤DPL (ciò non è necessario per i segmenti conformi
di codice).
Se le condizioni sono verificate, la DWORD più significativa del descrittore
viene caricata nell'operando destinazione dopo essere stata sottoposta ad un AND
con 00FxFF00h (la x indica che quel nibble caricato in DEST è
indefinito); il flag ZF inoltre viene posto a 1.
Se le condizioni non sono verificate, si ha ZF=0, mentre l'operando destinazione
non viene modificato.
Se SRC è a 32 bit, il selettore occupa la WORD meno significativa.
Se DEST è un Reg16, solo i 16 bit meno significativi della
DWORD vengono salvati, dopo essere stati sottoposti ad un AND con
FF00h.
I segmenti di codice, dati e stack vengono sempre considerati di tipo valido da
LAR; per quanto riguarda i segmenti speciali, la Figura 6.12 illustra tutti
i dettagli.
Questa istruzione richiede come operando sorgente il selettore di un descrittore di
cui si vuole esaminare il campo LIMIT.
Il descrittore deve avere un campo TYPE valido e deve essere accessibile dal
task che sta eseguendo LSL; a tale proposito, considerando il CPL del
task, il RPL del selettore e il DPL del descrittore, si deve avere
CPL≤DPL e RPL≤DPL (ciò non è necessario per i segmenti conformi
di codice).
Se le condizioni sono verificate, il campo LIMIT del descrittore viene caricato
nell'operando destinazione e il flag ZF viene posto a 1. La granularità
del valore caricato in DEST è espressa sempre in BYTE; se la granularità
è in pagine, il campo LIMIT viene sottoposto ad uno shift a sinistra di 12
bit e ad un successivo OR con 00000FFFh per convertire il risultato in
BYTE.
Se le condizioni non sono verificate, si ha ZF=0, mentre l'operando destinazione
non viene modificato.
Se SRC è a 32 bit, il selettore occupa la WORD meno significativa.
Se DEST è un Reg16, solo i 16 bit meno significativi del campo
LIMIT vengono salvati.
I segmenti di codice, dati e stack vengono sempre considerati di tipo valido da
LSL; per quanto riguarda i segmenti speciali, la Figura 6.13 illustra tutti
i dettagli.
Questa istruzione richiede come unico operando il selettore di un descrittore di
segmento; il suo scopo è verificare se il segmento è leggibile.
Il descrittore deve essere accessibile dal task che sta eseguendo VERR; a tale
proposito, considerando il CPL del task, il RPL del selettore e il
DPL del descrittore, si deve avere CPL≤DPL e RPL≤DPL (ciò
non è necessario per i segmenti conformi di codice).
Il selettore deve puntare al descrittore di un segmento di codice o dati; non sono
consentiti i descrittori speciali.
Se le condizioni sono verificate, il flag ZF viene posto a 1; in caso
contrario si ha ZF=0.
Questa istruzione richiede come unico operando il selettore di un descrittore di
segmento; il suo scopo è verificare se il segmento è scrivibile.
Il descrittore deve essere accessibile dal task che sta eseguendo VERW; a tale
proposito, considerando il CPL del task, il RPL del selettore e il
DPL del descrittore, si deve avere CPL≤DPL e RPL≤DPL (ciò
non è necessario per i segmenti conformi di codice).
Il selettore deve puntare al descrittore di un segmento di codice o dati; non sono
consentiti i descrittori speciali.
Se le condizioni sono verificate, il flag ZF viene posto a 1; in caso
contrario si ha ZF=0.
Questa istruzione compara i campi RPL di due selettori che devono trovarsi nei
due operandi richiesti.
Indicando con RPL_SRC il campo RPL di SRC e con RPL_DEST
il campo RPL di DEST, se risulta RPL_DEST<RPL_SRC, l'istruzione
ARPL produce ZF=1 e RPL_DEST=RPL_SRC. Se invece risulta
RPL_DEST≥RPL_SRC, l'istruzione ARPL produce ZF=0, mentre
RPL_DEST non viene modificato.
ARPL viene usata per impedire che un task possa utilizzare, per errore o in
malafede, un puntatore ad un segmento con maggiori privilegi; il caso più diffuso
è quello di un normale programma che chiama una procedura di servizio del SO
che richiede uno o più puntatori come parametri.
Consideriamo il caso di un servizio del SO che gira a livello di privilegio
0 e richiede un puntatore ad un buffer in cui scrivere dei dati; un normale
programma con CPL=3 potrebbe passare a tale servizio un puntatore ad un'area
della memoria riservata allo stesso SO. In mancanza di precauzioni, il
servizio chiamato finirebbe per sovrascrivere quell'area riservata provocando un
disastro!
Per evitare queste conseguenze, il servizio può usare ARPL per confrontare
il RPL del puntatore (in DEST) con il CPL del caller (in
SRC) che ha passato il puntatore stesso; come sappiamo, il CPL del
caller si trova nel vecchio contenuto di CS salvato nello stack prima della
chiamata della procedura. Se CPL=3 e RPL=0, l'istruzione ARPL
pone RPL=CPL ottenendo RPL=3; in questo modo, il tentativo di scrivere
nel buffer (che ha DPL=0) produce un'eccezione in quanto la CPU
effettua prima i controlli illustrati in 6.1.4 o in 6.1.5.
6.2 Protezione a livello delle pagine
I meccanismi di protezione si applicano anche alle pagine; lo scopo è ugualmente
quello di impedire interferenze tra i diversi programmi in esecuzione.
Prima di eseguire una qualsiasi istruzione che comporta un accesso alla memoria,
la CPU si assicura che il task in esecuzione stia rispettando tutte le
regole di protezione; a tale proposito, vengono effettuate delle verifiche che
si concentrano su:
- tipo di pagina
- restrizioni sulla memoria indirizzabile
I campi analizzati per effettuare queste verifiche sono contenuti negli elementi
delle tabelle di primo e secondo livello, come si vede in Figura 6.14.
6.2.1 Restrizioni sulla memoria indirizzabile
Nel modello di memoria a pagine, sono previsti due soli livelli di privilegio,
indicati con 0 e 1 nel bit U/S (User/Supervisor) di
Figura 6.14.
Se U/S=0, la CPU si trova in Supervisor Level; tale livello è
riservato al SO, ad altro software di sistema (come i device drivers) e ad
aree dati protette (come le stesse tabelle di primo e secondo livello).
Se U/S=1, la CPU si trova in User Level; tale livello è
riservato ai normali programmi, compresi i rispettivi blocchi di dati e stack.
Quando la paginazione è attiva, il CPL (riferito al modello segmentato) di
un task in esecuzione viene convertito nei due livelli di privilegio previsti per
le pagine; a tale proposito, un CPL pari a 0, 1 o 2
viene associato a U/S=0 (livello Supervisor), mentre CPL=3
viene associato a U/S=1 (livello User).
Se la CPU sta operando a livello Supervisor, il dominio indirizzabile
è formato da tutte le pagine; a livello User invece, solo le pagine con
U/S=1 risultano accessibili.
6.2.2 Verifiche sul tipo di pagina
Sono previsti solo due tipi di pagina, indicati con 0 e 1 nel bit
R/W (Read/Write) di Figura 6.14.
Una pagina con R/W=0 è accessibile in sola lettura; una pagina con
R/W=1 è accessibile in lettura e scrittura.
Quando la CPU si trova a livello Supervisor, tutte le pagine sono
accessibili in lettura e scrittura (anche quelle con R/W=0); a livello
User invece, solo le pagine con U/S=1 risultano accessibili, in
lettura se R/W=0 e in lettura/scrittura se R/W=1.
Le pagine con U/S=0 non sono accessibili dal livello User, nè in
lettura, nè in scrittura.
6.2.3 Combinazioni di protezione tra i due livelli di tabelle
Nel Capitolo 5 abbiamo visto che, con la paginazione attiva, sono presenti due
livelli di tabelle. La Page Directory o "tabella di primo livello" occupa
4096 byte e al suo interno può contenere sino a 1024 elementi di
Figura 6.14, ciascuno dei quali punta ad una "tabella di secondo livello" denominata
Page Table. Ogni tabella di secondo livello occupa 4096 byte e al suo
interno può contenere sino a 1024 elementi di Figura 6.14, ciascuno dei quali
punta ad una pagina nella "RAM virtuale", denominata Page Frame,
destinata a contenere codice o dati.
Gli attributi di protezione presenti negli elementi della Page Directory,
possono differire da quelli degli elementi presenti nella Page Table
associata; la CPU esamina entrambe le tabelle e combina tra loro tali
attributi per determinare le caratteristiche della Page Frame. La Figura
6.15 illustra tutte le combinazioni di protezione possibili.
Osservando, ad esempio, la quinta riga di Figura 6.15, vediamo che il campo DIR
dell'indirizzo a 32 bit indica nella Page Directory un elemento che
punta ad una Page Table con livello di privilegio User e accesso di tipo
Read-Only; il campo PAGE dell'indirizzo a 32 bit indica nella
stessa Page Table un elemento che punta ad una Page Frame con livello di
privilegio Supervisor e accesso di tipo Read-Only. La CPU combina
queste informazioni e determina che la relativa Page Frame ha livello di
privilegio User e accesso di tipo Read-Only.
Come si vede nelle ultime quattro righe di Figura 6.15, affinché una Page Frame
sia accessibile a livello Supervisor in lettura/scrittura, deve essere presente
il livello Supervisor, sia nell'elemento della Page Directory sia in
quello della Page Table.
Esistono dei casi particolari per i quali si assume che il livello di privilegio sia
sempre 0, indipendentemente dal CPL; tali casi riguardano:
- Accesso a descrittori di GDT, IDT, LDT e TSS.
- Accesso a stack innestati durante una CALL, una eccezione o una
interruzione, che comportano un cambio di privilegio.
6.2.4 Protezione combinata tra segmenti e pagine
La paginazione, quando viene attivata, deve convivere con la segmentazione. La
CPU esamina prima la protezione per i segmenti e solo successivamente quella
per le pagine; se viene rilevata una violazione delle regole a livello dei segmenti,
viene generata un'eccezione, mentre la verifica della protezione per le pagine viene
sospesa.
Consideriamo, ad esempio, un segmento dati di grosse dimensioni, appartenente ad un
task che gira a CPL=3; per renderne più efficiente la gestione, tale segmento
può essere suddiviso in una serie di pagine, accessibili a livello User
(quindi, ogni pagina ha U/S=1). Le pagine relative ai dati a sola lettura
hanno R/W=0; quelle relative ai dati modificabili hanno R/W=1.
Se il task tenta di accedere a questi dati, la CPU si assicura innanzi tutto
che vengano rispettate le regole di protezione per il segmento; in seguito, la stessa
CPU effettua tutte le verifiche relative alla protezione delle pagine.
6.3 Multitasking
In presenza di una interruzione/eccezione (gestita tramite Interrupt Gate o
Trap Gate) o di istruzioni come JMP e CALL (gestite in modo diretto
o tramite Call Gate), un task in esecuzione cede temporaneamente il controllo ad
un'altra porzione di codice; in questi casi viene usato lo stack per salvare alcune
informazioni importanti relative al task stesso. La chiamata di una procedura, ad esempio,
comporta il salvataggio dell'indirizzo di ritorno e di eventuali parametri nello stack;
nel caso della chiamata di una ISR, oltre all'indirizzo di ritorno, nello stack
viene salvato anche il contenuto del registro EFLAGS.
La situazione cambia radicalmente nel momento in cui il trasferimento del controllo
avviene tramite TSS o Task Gate; in tal caso, infatti, il task in esecuzione
viene interrotto, il suo intero stato viene salvato nel relativo Task State Segment
e il controllo passa ad un altro task. Si parla allora di task switch.
Nel tutorial Assembly Base abbiamo visto che una procedura la cui chiamata
utilizza lo stack per salvare le necessarie informazioni (indirizzo di ritorno, eventuali
parametri e variabili locali), può essere anche ricorsiva; tale procedura può chiamare
ripetutamente se stessa, creando ad ogni chiamata una copia distinta di informazioni
nello stack (in sostanza, le informazioni create da una chiamata non vengono sovrascritte
dalla chiamata successiva). Più in generale, una procedura del genere viene definita
"rientrante" quando è in grado di gestire correttamente chiamate effettuate in
contemporanea da più task in esecuzione.
Nel caso di un task switch, invece, il controllo viene ceduto ad un task del tutto
indipendente da quello che viene interrotto; di conseguenza, affinché il task interrotto
possa essere riavviato, il suo stato completo deve essere salvato nel relativo TSS.
Lo stack non viene utilizzato in questo meccanismo, per cui i task coinvolti non possono
essere rientranti.
Ogni task attivato da un task switch può specificare una propria LDT; in questo
modo è possibile evitare che i vari task in esecuzione possano interferire tra loro.
La 80386 permette di gestire anche interruzioni ed eccezioni tramite task switch;
in questo caso, la CPU provvede a restituire correttamente il controllo al
programma che era stato interrotto.
6.3.1 Il Task State Segment (TSS)
Tutte le informazioni necessarie per ripristinare l'esecuzione di un task interrotto
da un task switch, vengono salvate come sappiamo in un'apposita struttura denominata
Task State Segment (TSS); la Figura 6.16 illustra le caratteristiche
di un TSS destinato esplicitamente alla 80386.
I bit colorati in grigio sono riservati alla CPU per usi futuri e devono essere
posti a 0.
I vari campi del TSS si dividono in dinamici e statici; i campi
dinamici vengono aggiornati dalla CPU ad ogni task switch e comprendono i
registri generali EAX, EBX, ECX, EDX, ESP,
EBP, ESI, EDI, i registri di segmento CS, SS,
DS, ES, FS, GS, il registro dei flags EFLAGS,
l'Instruction Pointer EIP e il selettore del TSS associato al task a
cui restituire eventualmente il controllo (Back Link).
I campi statici vengono inseriti al momento della creazione del TSS e non
vengono più modificati; si tratta quindi di campi a sola lettura che comprendono il
registro CR3 (indirizzo della Page Directory), il selettore per la
LDT associata al nuovo task, gli indirizzi logici da caricare in SS:ESP
per i livelli di privilegio 0, 1 e 2, il bit T e
l'indirizzo della mappa dei permessi per l'accesso alle porte di I/O.
Il bit T è il Debug Trap Bit; se T=1 la CPU genera una
eccezione ad ogni task switch associato a quel TSS.
La mappa dei permessi per l'accesso alle porte di I/O, se presente, deve
trovarsi agli indirizzi più alti dello stesso TSS; il suo offset a 16
bit all'interno del TSS viene inserito nel campo I/O Map Base.
Gli aspetti relativi al bit T e alla mappa I/O vengono trattati più
avanti e nei capitoli successivi.
6.3.2 Il descrittore del TSS
Il TSS è a tutti gli effetti un segmento dati, per cui le sue caratteristiche
devono essere specificate da un apposito descrittore; tale descrittore, la cui
struttura è illustrata in Figura 6.17, deve trovarsi rigorosamente nella GDT.
I vari campi hanno lo stesso significato già illustrato per un generico descrittore
di segmento. Il campo LIMIT deve specificare un valore minimo di 103
byte; se è presente la I/O Map, tale limite deve essere ovviamente maggiore.
Nell'ACCESS BYTE, il campo TYPE deve valere 10B1b, dove il bit
indicato con B rappresenta il Busy Bit. Se B=0 il task associato
al TSS è inattivo, mentre se B=1 è in esecuzione o fa parte di una
catena di task innestati; grazie a questo bit, la CPU è in grado di impedire
che un task possa essere rientrante (ad esempio, un task A che chiama B
che a sua volta chiama nuovamente A).
Chi effettua il task switch deve avere un CPL numericamente minore o uguale
al DPL presente nel descrittore di TSS; ponendo DPL=0, si fa in
modo che i task switch possano essere effettuati solo dal SO.
La gestione di un TSS è di competenza della CPU. Un task può avere
accesso ad un TSS ma non può effettuare operazioni di lettura o scrittura; un
modo per aggirare questa limitazione consiste nel creare un segmento dati sovrapposto
al TSS stesso.
Un tentativo di caricare il selettore di un descrittore di TSS in un registro
di segmento produce un'eccezione.
6.3.3 Il Task Register (TR)
In un sistema multitasking, possono essere presenti più task che vengono eseguiti a
rotazione; in un determinato istante quindi, solo uno di tali task è in esecuzione.
La CPU individua il TSS del task correntemente in esecuzione grazie al
Task Register (TR), la cui struttura è illustrata in Figura 6.18.
Il TR è accessibile solo in modalità protetta; nella parte visibile del
registro viene caricato il selettore che punta al descrittore (nella GDT) del
TSS associato al primo task da eseguire; la CPU accede al descrittore,
legge i campi BASE e LIMIT e li carica nella parte invisibile del
registro stesso. Ad ogni task switch, il TR viene aggiornato automaticamente
dalla CPU.
Per caricare un selettore in TR, è disponibile l'istruzione LTR.
L'unico operando richiesto da LTR contiene un selettore da caricare nella
parte visibile di TR; dopo aver eseguito questa istruzione, la CPU
provvede a portare a 1 il Busy Bit nel descrittore del TSS.
LTR è un'istruzione di sistema, disponibile in modalità protetta; può essere
eseguita solo da un task che gira a CPL=0.
Per conoscere il contenuto esclusivamente della parte visibile di TR, è
disponibile l'istruzione STR.
STR legge il contenuto visibile di TR e lo salva nel suo unico operando
a 16 bit. Questa istruzione può essere eseguita da un task che gira a qualunque
livello di privilegio.
6.3.4 Il descrittore di Task Gate
Un task che non ha privilegi sufficienti per accedere al descrittore di un TSS
nella GDT, può ugualmente effettuare un task switch indiretto attraverso il
descrittore di un Task Gate; si tratta quindi di una situazione simile alla
chiamata indiretta di una procedura tramite il descrittore di un Call Gate.
Il descrittore di un Task Gate assume la struttura illustrata in Figura 6.19.
I bit indicati con 'X' sono riservati per usi futuri e vengono ignorati dalla
80386.
Il campo TSS SELECTOR contiene un selettore che deve puntare ad un descrittore
di TSS nella GDT; il RPL di questo campo non viene utilizzato.
Un descrittore di Task Gate può trovarsi nella GDT o nella LDT
associata ad un task; inoltre, questo tipo di descrittore può essere presente anche
nella IDT per permettere la gestione di interruzioni e eccezioni tramite task
switch indiretto.
Un task è autorizzato a chiamare il descrittore di un Task Gate solo se:
max(CPL, RPL) ≤ DPL
Dove CPL è il livello di privilegio del task (caller), RPL è il livello
di privilegio del selettore che punta al descrittore di Task Gate, mentre
DPL è il livello di privilegio specificato nell'ACCESS BYTE di Figura
6.19 (livello di privilegio minimo richiesto al task che vuole effettuare il task
switch); in questo caso il DPL nel descrittore del TSS (Figura 6.17)
viene ignorato.
Questi requisiti sono necessari per evitare che un task con privilegi insufficienti
possa provocare un task switch.
6.3.5 Modalità di esecuzione di un task switch
Esistono quattro situazioni che provocano un task switch:
- Il task corrente esegue JMP o CALL che puntano al descrittore
di un TSS nella GDT.
- Il task corrente esegue JMP o CALL che puntano al descrittore
di un Task Gate nella GDT o LDT.
- Si verifica una interruzione/eccezione che punta al descrittore di un Task
Gate nella IDT.
- Il task corrente esegue IRET con NT=1 nel registro
EFLAGS.
Prima di effettuare un task switch, la CPU esegue una serie di verifiche.
Come abbiamo visto, nel caso di JMP e CALL, il DPL del
descrittore di TSS o di Task Gate deve essere maggiore o uguale
al più grande tra il CPL del task corrente e il RPL del selettore
del gate. Questa verifica non viene effettuata nel caso di un task switch relativo
a eccezioni, interruzioni e IRET.
Il descrittore di TSS deve avere LIMIT maggiore o uguale a 103
in quanto la dimensione minima di un TSS è 104 byte.
Se le precedenti verifiche danno esito positivo, la CPU accede al TSS
corrente e salva l'intero stato del task da interrompere; nel registro TR
viene caricato il selettore del descrittore del nuovo TSS, il Busy Bit
viene posto a 1, così come il bit TS (Task Switched) nel
registro CR0.
Lo stato del nuovo task viene letto dal relativo TSS e vengono inizializzati
tutti i registri.
La Figura 6.20 riassume tutte le verifiche effettuate dalla CPU.
Si tenga presente che un task switch coinvolge due task totalmente indipendenti tra
loro; non esiste alcuna relazione tra il CPL del vecchio task e quello del nuovo.
La CPU non effettua alcuna verifica sui livelli di privilegio quando deve passare
da un task all'altro; il CPL del nuovo task viene letto dai due bit meno
significativi della copia di CS memorizzata nel relativo TSS.
6.3.6 Il Back Link al TSS del vecchio task
Quando una interruzione/eccezione o un'istruzione CALL provocano un task switch,
la CPU pone NT=1 e copia il selettore del TSS corrente nel campo
Back Link del TSS del nuovo task (Figura 6.16); il nuovo task restituisce
il controllo tramite una IRET.
In presenza di una IRET con NT=1, la CPU effettua un task switch
per riattivare il vecchio task; il selettore del relativo TSS viene trovato
appunto nel campo Back Link del nuovo task.
Un task switch provoca una serie di effetti sul Busy Bit (B), sul flag
NT e sul campo Back Link (BKL); la Figura 6.21 mostra tutti i casi
che si possono presentare (tra parentesi, i valori attesi prima della modifica).
6.3.7 Uso del Busy Bit per impedire chiamate rientranti
Un task switch che porta un task A a chiamare un task innestato B, può
essere seguito da un altro task switch che porta B a chiamare un task innestato
C; a sua volta, C può effettuare un ulteriore task switch per chiamare
un task innestato D e così via.
Si viene a creare così una catena di task innestati l'uno nell'altro; ciascuno di
questi task poi può essere interrotto dall'arrivo di una interruzione o eccezione
che, se gestita tramite task switch, provoca la chiamata di una ISR anch'essa
innestata.
In questa situazione, può capitare che uno dei task innestati ne chiami un altro che
fa già parte della catena; si tratta di un tipico caso di "rientranza", che ha
l'effetto di sovrascrivere il TSS corrente con il conseguente danneggiamento
della catena stessa.
Per impedire gli effetti dannosi della rientranza, la CPU si serve del Busy
Bit; in tutti i nuovi task chiamati da un task switch e in tutti quelli che fanno
parte della catena di task innestati, viene posto B=1, Un task con B=1
che tenta con un task switch di chiamare se stesso (ricorsione) o un altro task che
fa parte di una catena (rientranza), provoca un'eccezione della CPU.
Il Busy Bit è utile anche nei sistemi multiprocessore; in tal caso viene usato
per impedire che due o più processori chiamino lo stesso task.
Se necessario, è anche possibile modificare la catena dei task innestati. Supponiamo,
ad esempio, di voler eliminare uno dei task della catena; in tal caso, la procedura
consigliata è la seguente:
- Disabilitare le interruzioni.
- Modificare il Back Link del task che ha interrotto quello da eliminare.
- Porre B=0 nel descrittore del TSS del task da eliminare.
- Riabilitare le interruzioni.
6.3.8 Spazio di indirizzamento di un task
In un sistema multitasking, ogni task può avere il proprio spazio di indirizzamento
privato grazie all'uso della LDT per il modello segmentato o del Base
Address della Page Directory (PDBR in CR3) per il modello
a pagine; in questo modo, ogni task può disporre di propri segmenti riservati o
proprie pagine riservate.
Se due o più task vogliono comunicare tra loro, possono predisporre una LDT
condivisa; in alternativa, si può anche ricorrere a segmenti condivisi tramite la
GDT.
Nei precedenti capitoli abbiamo visto che, in modalità protetta, un indirizzo logico
Selector:Offset viene convertito in un indirizzo lineare il quale, a sua volta,
viene mappato nella memoria fisica; se la paginazione non è attiva, questa è l'unica
forma di mappatura disponibile ed è la stessa per ogni task (uno stesso indirizzo
lineare corrisponde allo stesso indirizzo fisico).
Se la paginazione è attiva, si può ottenere lo stesso risultato facendo in modo che
tutti i task condividano la stessa Page Directory.
Si può ricorrere alla paginazione quando si vuole che ogni task abbia una differente
mappatura degli indirizzi lineari negli indirizzi fisici; a tale proposito, nel campo
del relativo TSS riservato a CR3 (Figura 6.16) bisogna assegnare una
diversa Page Directory ad ogni task.
Se anche le Page Tables sono differenti per ogni task e i loro elementi puntano
a differenti Page Frames, nessun indirizzo fisico verrà condiviso tra i vari
task.
Quando due o più task hanno invece la necessità di condividere dei dati, si deve fare
in modo che l'area di memoria fisica contenente i dati stessi venga condivisa; a tale
scopo, esistono tre metodi distinti.
1. I vari task che vogliono condividere dei dati, si servono di un descrittore
di segmento presente nella GDT; tale descrittore deve puntare allo stesso blocco
di memoria fisica contenente i dati.
Questo metodo è poco selettivo in quanto qualunque task può accedere alla GDT.
2. I vari task che vogliono condividere dei dati, si servono di una LDT
condivisa; i descrittori di segmento contenuti in tale LDT puntano allo stesso
blocco di memoria fisica contenente i dati.
Questo metodo è più selettivo di quello precedente in quanto permette di condividere
i dati solo tra specifici task (quelli che si servono della stessa LDT).
3. I vari task che vogliono condividere dei dati, si servono delle proprie
LDT; ciascuna LDT contiene un puntatore allo stesso descrittore
(chiamato "alias") del segmento dove sono presenti i dati.
Questo metodo è ancora più selettivo di quelli precedenti in quanto non necessita di
una LDT condivisa; la LDT privata di ogni task contiene un selettore
che punta al segmento dati comune, ma anche selettori che puntano a segmenti riservati
da non condividere.
6.4 Gestione dell'Input/Output
Nel tutorial Assembly Avanzato abbiamo visto che con le architetture
80x86 le varie periferiche hardware del PC risultano accessibili
attraverso appositi registri denominati porte di I/O (o porte hardware); a
seconda dei casi, una porta hardware può essere: di solo input, di solo output o
bidirezionale.
Esistono due metodi di indirizzamento delle porte di I/O:
- Port Mapped I/O
- Memory Mapped I/O
Con il primo metodo, la CPU utilizza uno spazio di indirizzamento separato
dalla RAM; l'accesso a tale spazio avviene tramite apposite istruzioni, come
IN e OUT. La CPU si serve di un pin apposito, denominato
M/IO#, per indicare alla logica di controllo se l'indirizzo caricato
sull'Address Bus si riferisce ad una porta hardware o alla RAM; se
M/IO#=0, un indirizzo è riferito ad una porta hardware, mentre M/IO#=1
indica che l'indirizzo si riferisce alla RAM.
Quando si utilizza questo metodo, i meccanismi di protezione si basano sul flag
IOPL e sulla I/O Permission Map (illustrata nel seguito).
Con il secondo metodo, le varie porte hardware vengono mappate direttamente nella
RAM; in questo caso, l'accesso avviene tramite le normali istruzioni che la
CPU mette a disposizione per il trasferimento e la manipolazione dei dati
(come MOV, AND, OR, XOR, etc).
Per ovvi motivi, un'area della RAM dove vengono mappate le porte hardware non deve
essere rilocabile o soggetta a swapping su disco; a tale proposito, i SO possono
servirsi del campo Available (presente nei descrittori di segmento o di pagina)
per identificare tali aree.
Quando si utilizza questo metodo, risultano disponibili gli ordinari meccanismi di
protezione offerti dalla segmentazione o dalla paginazione.
6.4.1 Port Mapped I/O
Le CPU come la 80386 mappano le porte hardware in uno spazio di
indirizzamento da 64 KiB, separato dalla RAM; l'accesso a tale spazio
avviene tramite le prime 16 linee (da A0 a A15) dell'Address
Bus.
Lo spazio di indirizzamento può essere suddiviso in 65536 porte a 8
bit, 32768 porte a 16 bit o 16384 porte a 32 bit. Per
ottenere la massima efficienza di accesso, le porte a 16 bit devono trovarsi
ad indirizzi multipli interi di 2 (0, 2, 4, ...,
65532, 65534), mentre per quelle a 32 bit l'allineamento ideale
è alla DWORD (0, 4, 8, ..., 65528, 65532);
se questi requisiti sono soddisfatti, l'accesso richiede un solo ciclo di memoria,
mentre in assenza del corretto allineamento, la CPU ha bisogno di due cicli
di memoria.
Un indirizzo di porta può essere indicato nei programmi tramite un valore immediato
di tipo BYTE; in tal caso, si possono gestire:
- Le prime 256 porte a 8 bit (0, 1, 2, ...,
254, 255)
- Le prime 128 porte a 16 bit (0, 2, 4, ...,
252, 254)
- Le prime 64 porte a 32 bit (0, 4, 8, ...,
248, 252)
In alternativa, un indirizzo di porta può essere indicato nei programmi tramite il
registro DX; in tal caso, si possono gestire:
- 65536 porte a 8 bit (0, 1, 2, ..., 65534,
65535)
- 32768 porte a 16 bit (0, 2, 4, ..., 65532,
65534)
- 16384 porte a 32 bit (0, 4, 8, ..., 65528,
65532)
Gli indirizzi di porta compresi tra 0F8h e 0FFh sono riservati alla
CPU e non devono essere riassegnati ad altre porte nei programmi.
6.4.2 Memory Mapped I/O
Le porte hardware possono essere mappate anche nella RAM; questa tecnica si
rivela vantaggiosa in particolare quando si ha a che fare con periferiche dotate di
memorie di ampie dimensioni. Nel tutorial Assembly Avanzato abbiamo visto
diversi esempi relativi all'uso di una frame window nella RAM per
accedere al BIOS, alla memoria video, alla memoria XMS, etc; nel caso,
ad esempio, della memoria video, tutte le operazioni effettuate sulla frame window si
ripercuotono immediatamente sul contenuto dello schermo.
Il vantaggio del Memory Mapped I/O sta nel fatto che si possono utilizzare
tutte le istruzioni destinate all'accesso alla RAM; è possibile quindi spostare
dati con MOV o modificare dei bit con OR, XOR, AND.
6.4.3 Istruzioni di I/O
Le istruzioni di I/O sono destinate solo al Port Mapped I/O e si
suddividono nelle seguenti due categorie:
- Istruzioni che trasferiscono un solo dato (BYTE, WORD o DWORD)
per volta
- Istruzioni che trasferiscono stringhe di dati (stringhe di BYTE, WORD
o DWORD)
Come è stato già spiegato, tutte queste istruzioni vengono eseguite ponendo M/IO#=0,
in modo che l'Address Bus metta in collegamento la CPU con le porte hardware
specificate e non con la RAM.
La prima categoria di istruzioni di I/O comprende IN e OUT; i dati
da leggere o scrivere nelle porte hardware comportano l'uso del registro accumulatore.
Viene usato AL per il trasferimento dati a 8 bit, AX per quello a
16 bit e EAX per quello a 32 bit.
Questa istruzione legge un dato dalla porta specificata da SRC e lo copia
nell'accumulatore; la dimensione in bit del dato da leggere viene dedotta dall'ampiezza
del registro DEST.
Se SRC è un Imm8, si possono gestire sino a 256 porte; se invece
si usa DX, si possono gestire sino a 65536 porte.
Questa istruzione legge un dato dall'accumulatore e lo scrive nella porta specificata;
la dimensione in bit del dato da scrivere viene dedotta dall'ampiezza del registro
SRC.
Se DEST è un Imm8, si possono gestire sino a 256 porte; se invece
si usa DX, si possono gestire sino a 65536 porte.
La seconda categoria di istruzioni di I/O è conosciuta anche come Block I/O
Instructions e comprende INS e OUTS; il trasferimento dati comporta
l'uso di DS:ESI come sorgente e ES:EDI come destinazione. I registri
puntatori ESI e EDI sono obbligatori; per quanto riguarda i registri di
segmento, è consentito il segment override solo su DS.
Dopo ogni singolo dato trasferito, i registri puntatori ESI e EDI vengono
aggiornati in base al flag DF; se DF=0, i puntatori vengono incrementati,
mentre con DF=1 vengono decrementati. L'entità dell'incremento o decremento
rispecchia l'ampiezza (BYTE, WORD o DWORD) dei dati trasferiti.
Con INS e OUTS è obbligatorio l'uso di DX per specificare la porta
a cui accedere.
Questa istruzione legge un dato dalla porta specificata in DX e lo copia nella
locazione di memoria o registro DEST; la destinazione è ES:DI se l'attributo
Address Size è 16 bit e ES:EDI se l'attributo Address Size è
32 bit.
INS può essere preceduta dal prefisso REP; in tal caso, il trasferimento
dati viene ripetuto per il numero di volte contenuto in CX nel modo 16 bit
o ECX nel modo 32 bit.
La forma esplicita di INS richiede che sia specificata l'ampiezza del dato da
trasferire; per un dato di tipo WORD, ad esempio, possiamo scrivere:
ins word ptr [es:edi], dx
Considerando il fatto che è predefinito l'uso di DX come SRC e di
ES:EDI come DEST, vengono rese disponibili anche le forme implicite di
INS, rappresentate da INSB (8 bit), INSW (16 bit)
e INSD (32 bit).
Questa istruzione legge un dato dalla locazione di memoria o registro SRC e lo
scrive nella porta specificata in DX; la sorgente è SI se l'attributo
Address Size è 16 bit e ESI se l'attributo Address Size
è 32 bit. Il registro di segmento predefinito per la sorgente è DS, ma
è consentito il segment override.
OUTS può essere preceduta dal prefisso REP; in tal caso, il trasferimento
dati viene ripetuto per il numero di volte contenuto in CX nel modo 16 bit
o ECX nel modo 32 bit.
La forma esplicita di OUTS richiede che sia specificata l'ampiezza del dato da
trasferire; per un dato di tipo BYTE, ad esempio, possiamo scrivere:
outs dx, byte ptr [ds:esi]
Considerando il fatto che è predefinito l'uso di DS:ESI come SRC e di
DX come DEST, vengono rese disponibili anche le forme implicite di
OUTS, rappresentate da OUTSB (8 bit), OUTSW (16 bit)
e OUTSD (32 bit).
6.4.4 Protezione per le operazioni di I/O
Come è stato spiegato in precedenza, il Memory Mapped I/O è soggetto ai
meccanismi di protezione esposti in 6.1 per i segmenti e in 6.2 per
le pagine; quando invece si utilizza il Port Mapped I/O, risultano disponibili
due meccanismi di protezione, rappresentati dal campo IOPL in EFLAGS e
dalla I/O Permission Map nel TSS del task.
Come per la 80286, anche con la 80386 vengono definite "sensibili" (al
campo IOPL di EFLAGS) le seguenti istruzioni:
- IN, INS, INSB, INSW, INSD
- OUT, OUTS, OUTSB, OUTSW, OUTSD
- CLI, STI
Un task è autorizzato ad usare le istruzioni sensibili solo se:
CPL ≤ IOPL
Generalmente, si pone IOPL=0 (o al massimo 1) in modo che solo il
software di sistema possa usare le istruzioni sensibili; in questo caso, i normali
programmi che girano a CPL=3 non hanno le autorizzazioni necessarie. Un task
con CPL maggiore di IOPL che tenta di usare le istruzioni sensibili,
provoca una eccezione della CPU.
Ogni task ha la propria copia di EFLAGS e quindi anche del campo IOPL;
il SO può quindi fare in modo che solo determinati task possano usare le
istruzioni sensibili, assegnando a ciascuno di essi un opportuno IOPL.
La 80386 fornisce anche una nuova funzionalità che permette di rendere ancora
più selettivo l'accesso alle porte di I/O da parte di un task; si tratta della
I/O Permission BitMap che eventualmente deve trovarsi nel TSS del task
stesso.
Se questa mappa è presente, la sua posizione è indicata da un offset a 16 bit
specificato nel campo I/O Map Base di Figura 6.16; la mappa deve trovarsi
nella parte finale del TSS, per cui lo spazio che occupa è delimitato
superiormente dal campo LIMIT del TSS stesso.
Come si evince dal nome, la I/O Permission BitMap è una mappa di bit, dove ogni
bit corrisponde ad un numero di porta; se il bit vale 0 l'accesso a quella porta
è abilitato, mentre se vale 1 è interdetto. Grazie a questa funzionalità quindi,
un task che ha CPL≤IOPL può accedere solamente alle porte hardware indicate
dalla mappa.
La Figura 6.22 illustra la parte finale di un TSS contenente la I/O
Permission BitMap.
Le porte a 16 bit sono rappresentate nella mappa da due bit consecutivi; quelle
a 32 bit da quattro bit consecutivi. Una ipotetica porta 20 a 32
bit, ad esempio, viene rappresentata da quattro bit consecutivi a partire dalla
posizione 20 nella mappa; come si vede in Figura 6.23, tale porta occupa il
quinto, sesto, settimo e ottavo bit del terzo BYTE.
Se un task tenta di accedere alla porta di Figura 6.23, la CPU verifica prima
di tutto se CPL≤IOPL; se la verifica dà esito positivo, vengono controllati
i quattro bit corrispondenti sulla mappa e solo se tali bit valgono tutti 0 è
consentito l'accesso alla porta stessa.
Una porta a 16 bit non allineata alla WORD avrà lo stesso disallineamento
anche nella mappa; analogamente per le porte a 32 bit non allineate alla
DWORD. Può capitare allora che i bit di tali porte si trovino a cavallo di due
BYTE nella mappa; si può pensare, ad esempio, ad una porta 21 a 32
bit, che in Figura 6.23 verrebbe mappata negli ultimi tre bit del terzo BYTE
e nel primo bit del quarto BYTE. Il dato più piccolo a cui la CPU può
accedere è il BYTE, per cui in casi del genere sarebbe necessario leggere due
BYTE consecutivi. Se la situazione appena descritta si presenta per le porte con
gli indirizzi più alti nella mappa, c'è il rischio che la CPU tenti di accedere ad
un BYTE oltre il limite del TSS; per evitare che ciò produca un'eccezione,
la mappa stessa deve terminare con un BYTE aggiuntivo i cui bit devono valere tutti
1 (Figura 6.22).
Non è necessario che la mappa rappresenti tutte le porte; se, ad esempio, ci interessano
solo le prime 80 porte, la mappa deve avere una dimensione di 10 byte
(80 bit), più il BYTE finale. Le porte non elencate nella mappa vengono
considerate interdette.
Considerando che il campo I/O Map Base occupa 16 bit e può quindi
contenere un valore compreso tra 0 e 65535, la mappa deve trovarsi ad un
offset massimo pari a DFFFh (57343); infatti, se volessimo mappare tutte
le 65536 porte avremmo bisogno di 8192 byte e sommando 8192 a
57343 si ottiene 65535 (a cui si deve aggiungere il BYTE finale).
Se I/O Map Base è maggiore o uguale al campo LIMIT del TSS, la
I/O Permission BitMap viene considerata assente; in tal caso, un tentativo da
parte del task di accedere alle porte hardware provoca un'eccezione, anche se
CPL≤IOPL.
6.5 Interruzioni ed eccezioni
Per una descrizione generale delle interruzioni ed eccezioni vale quanto esposto nella
sezione 2.3 del Capitolo 2.
Abbiamo visto che le interruzioni e le eccezioni vengono identificate tramite un numero
detto vettore; la Non Maskable Interrupt (NMI) e le varie eccezioni
risultano associate a vettori compresi tra 0 e 31.
Alcuni di questi 32 vettori sono inutilizzati e non devono essere usati in
quanto riservati; i SO destinati alla 80386 non devono riassegnare tali
vettori perché ciò porterebbe a compromettere la compatibilità con le future
CPU.
Tutti i restanti vettori, dal 32 al 255, sono destinati alle interruzioni
mascherabili; tali vettori in genere vengono gestiti tramite un controller esterno,
come il PIC 8259A.
La Figura 6.24 elenca i vettori relativi a interruzioni ed eccezioni per la 80386.
6.5.1 Tipi di eccezioni
Le eccezioni (o interruzioni software) sono eventi sincroni generati dalla CPU
in seguito a violazioni dei meccanismi di protezione o direttamente dai programmi
tramite istruzioni come INT; si suddividono nelle seguenti tre categorie:
Una eccezione è di tipo fault quando viene individuata dalla CPU
durante l'esecuzione di una istruzione (o prima che l'istruzione venga eseguita);
in tal caso, la relativa ISR avrà come indirizzo di ritorno quello della
stessa istruzione che ha causato l'eccezione.
Un esempio di fault è la divisione per 0 prodotta dall'istruzione
DIV o IDIV; in tal caso, viene chiamata la ISR associata al
vettore n. 0 (Divide Error) di Figura 6.24 e l'indirizzo di ritorno
è quello della stessa divisione.
La ISR chiamata è tenuta a risolvere il problema in quanto al termine deve
restituire il controllo alla stessa istruzione che ha prodotto l'eccezione; in caso
contrario, si verifica chiaramente un loop infinito!
Se la ISR risolve il problema, il task interrotto dall'eccezione può essere
riavviato; si dice allora che il task è restartable (riavviabile).
Una fault si rivela molto utile quando un task tenta di accedere ad un dato
in un segmento non presente in memoria; in tal caso, viene chiamata la ISR
associata al vettore n. 11 (Segment Not Present) di Figura 6.24. La
stessa ISR può così provvedere a caricare in memoria il segmento richiesto e
a riavviare il task che era stato interrotto.
Una eccezione è di tipo trap quando viene individuata dalla CPU dopo
l'esecuzione di una istruzione; in tal caso, la relativa ISR avrà come
indirizzo di ritorno quello dell'istruzione successiva a quella che ha causato
l'eccezione.
Un esempio di trap è l'overflow dovuto ad una addizione o sottrazione tra
numeri interi con segno; una successiva istruzione INTO, se trova OF=1
nel registro EFLAGS, chiama la ISR associata al vettore n. 4
(INTO Detected Overflow) di Figura 6.24. Al termine della ISR,
l'esecuzione riprende dall'istruzione immediatamente successiva alla INTO.
Un task interrotto da una trap, quando riottiene il controllo, può quindi
proseguire normalmente l'esecuzione.
Una eccezione è di tipo abort quando è conseguenza di gravi problemi, come
errori hardware o valori illegali nelle tabelle di sistema (GDT, LDT,
Page Directory, etc).
Il task interrotto da un abort non è riavviabile.
6.5.2 Tipi di interruzioni hardware
Le interruzioni hardware sono eventi asincroni generati dall'hardware del computer;
il loro comportamento viene influenzato da determinate condizioni e dallo stato di
appositi flags del registro EFLAGS.
La NMI è una interruzione hardware non mascherabile, in quanto viene generata
in caso di gravi problemi nell'hardware del computer; vista la sua importanza, tale
interruzione giunge direttamente alla CPU tramite un pin dedicato (NMI).
L'arrivo di una NMI rappresenta una situazione molto delicata, per cui la
CPU chiama la relativa ISR e mette in attesa altre eventuali NMI
finché la ISR stessa non restituisce il controllo tramite una IRET; in
questo modo si impedisce il verificarsi di pericolose chiamate sovrapposte.
Le altre interruzioni hardware giungono alla CPU tramite il pin INTR e
vengono definite mascherabili in quanto la loro gestione può essere abilitata o meno
tramite il flag IF (Interrupt Enable in Figura 6.25); se IF=1 la
CPU elabora le interruzioni mascherabili, mentre se IF=0 le ignora.
Il flag IF, come sappiamo, può essere modificato tramite le istruzioni
CLI e STI, ma solo da un task con CPL≤IOPL; in alternativa,
un task può modificare IF con PUSHF e POPF, sempre con le stesse
limitazioni.
Un task switch (anche tramite IRET) inizializza il registro EFLAGS del
task a cui cedere il controllo, per cui può essere usato per modificare IF; un
Interrupt Gate, a sua volta, modifica direttamente IF in quanto chiama
una ISR ponendo IF=0.
Il Resume Flag è importante quando si vogliono evitare chiamate in loop ad una
ISR in fase di debugging. Supponiamo, ad esempio, di aver posizionato una INT
3 (Breakpoint) su una istruzione che tenta di accedere a dei dati in un
segmento non presente in memoria; la ISR associata alla INT 3 restituisce
il controllo alla stessa istruzione dove è presente il breakpoint e se RF=0,
viene generata una nuova eccezione di debug. Se invece la ISR termina dopo aver
posto RF=1, il breakpoint viene ignorato e la CPU può generare l'eccezione
di segmento non presente (#NP).
Una situazione particolarmente delicata si viene a creare quando si sta inizializzando
lo stack con istruzioni del tipo:
Il verificarsi di una interruzione tra queste due istruzioni, provoca la chiamata
della relativa ISR, con salvataggio dell'indirizzo di ritorno e del contenuto
di EFLAGS in uno stack che ancora non è stato inizializzato!
Per evitare questa pericolosa situazione, in presenza di una MOV o POP
con destinazione SS, la CPU disabilita le interruzioni mascherabili e
diverse eccezioni, in attesa che termini l'esecuzione dell'istruzione successiva (che,
presumibilmente, dovrebbe essere una MOV con destinazione ESP).
Un procedimento alternativo vivamente raccomandato consiste nell'uso dell'istruzione
LSS, con sorgente Mem16:Mem32 e destinazione SS:ESP; se si segue
questa strada, la coppia SS:ESP viene inizializzata in un solo passaggio ed
eventuali richieste di interruzione vengono servite solo dopo l'esecuzione della stessa
LSS.
6.5.3 Ordine di priorità per interruzioni ed eccezioni simultanee
Se in uno stesso momento diverse interruzioni ed eccezioni sono in attesa, la
CPU soddisfa le richieste in un ordine prestabilito; in particolare,
come si vede in Figura 6.26, le interruzioni mascherabili devono lasciare la
precedenza alle eccezioni e alla NMI.
6.5.4 La Interrupt Descriptor Table (IDT)
In presenza di una interruzione o eccezione, il corrispondente vettore viene usato
come un indice nella Interrupt Descriptor Table (IDT); il descrittore
presente a tale indice punta alla relativa ISR che può essere una normale
procedura o un altro task (task switch).
Come accade per la GDT, anche la IDT è unica; l'elemento di indice
0 della IDT non è riservato (a differenza di quanto accade con la
GDT) e può quindi contenere un normale descrittore.
La 80386 riconosce un massimo di 256 vettori di interruzione, per cui
la dimensione minima di una IDT completa è di 256*8=2048 byte; in
genere però, in questa tabella vengono inseriti solo i descrittori per i vettori
che si prevede di gestire.
Un tentativo di accedere ad un vettore oltre i limiti della IDT pone la
80386 in modalità shutdown; in tale modalità, la CPU sospende
l'esecuzione delle istruzioni e può essere riattivata solo dall'arrivo di una
NMI o da un segnale di RESET.
La IDT può trovarsi in qualsiasi area della memoria; la sua posizione viene
individuata dalla CPU mediante l'apposito registro di sistema Interrupt
Descriptor Table Register (IDTR), la cui struttura è illustrata in Figura
6.27.
Il campo BASE indica il Base Address a 32 bit da cui parte in
memoria la IDT. Il campo LIMIT è espresso in BYTE e per una
tabella con N descrittori, deve valere chiaramente almeno (N*8)-1. Si
noti che la dimensione massima della IDT è largamente inferiore 64 KiB,
per cui per il campo LIMIT sono sufficienti 16 bit.
L'IDTR viene inizializzato attraverso l'istruzione di sistema LIDT
(Load Interrupt Descriptor Table Register).
L'istruzione LIDT richiede come unico operando una locazione di memoria da
6 byte (48 bit); i 16 bit meno significativi rappresentano il
campo LIMIT, mentre i 32 bit più significativi rappresentano il campo
BASE. Nel modo 16 bit, gli 8 bit più significativi del campo
BASE vengono ignorati (il Base Address è a 24 bit).
LIDT è un'istruzione di sistema, disponibile però anche in modalità reale
in quanto il suo scopo è predisporre l'ambiente operativo per il passaggio alla
modalità protetta. Una volta entrati in modalità protetta, eventuali modifiche
della IDT sono sconsigliate; se necessario, possono essere effettuate con
LIDT solo da un task che gira al massimo livello di privilegio (come il
SO).
Se vogliamo conoscere il contenuto dell'IDTR, dobbiamo usare l'istruzione
di sistema SIDT (Store Interrupt Descriptor Table Register).
Questa istruzione legge 6 byte dall'IDTR e li copia nella locazione
di memoria indicata dall'operando Mem. I primi 16 bit rappresentano
il campo LIMIT, mentre i successivi 32 bit contengono il campo
BASE; nel modo 16 bit, il BYTE più significativo del campo
BASE viene posto a 0.
SIDT è un'istruzione di sistema destinata alla modalità protetta; può
essere usata da un task che gira a qualunque livello di privilegio.
6.5.5 Tipi di descrittore per la IDT
La IDT può contenere esclusivamente i seguenti tre tipi di descrittore:
- Task Gate Descriptor
- Interrupt Gate Descriptor
- Trap Gate Descriptor
Per il descrittore di Task Gate (Figura 6.19) valgono tutte le considerazioni
esposte in 6.3.4.
I descrittori di Interrupt Gate e Trap Gate vengono usati per gestire una
interruzione o eccezione tramite una normale procedura (ISR); la loro struttura
assume l'aspetto illustrato in Figura 6.28.
I bit indicati con 'X' non vengono usati, mentre quelli posti a 0 sono
riservati.
Il campo SELECTOR contiene il selettore del segmento di codice dove viene
definita la ISR da chiamare, mentre il campo OFFSET contiene il
relativo offset (entry point) a 32 bit della procedura stessa.
Nell'ACCESS BYTE, il bit S deve valere 0 per indicare un descrittore
speciale.
Il bit P indica se il descrittore contiene informazioni valide (P=1) o meno
(P=0).
I due bit del DPL hanno lo stesso significato che abbiamo visto per i Call
Gates; infatti, anche gli Interrupt Gates e i Trap Gates permettono di
saltare ad un altro segmento di codice con identico o maggiore livello di privilegio (e
con C=0).
I quattro bit riservati a TYPE assumono i seguenti valori:
- TYPE = 1110b - Interrupt Gate Descriptor
- TYPE = 1111b - Trap Gate Descriptor
Un vettore che punta al descrittore di un Task Gate nella IDT, provoca
un task switch per la gestione dell'interruzione o eccezione; se invece il descrittore
si riferisce ad un Interrupt Gate o ad un Task Gate, l'interruzione o
eccezione viene gestita tramite la chiamata ad una procedura (ISR), con lo
stesso meccanismo illustrato in Figura 6.9 per il Call Gate.
La differenza fondamentale rispetto al Call Gate sta nel fatto che la chiamata
di una ISR comporta il salvataggio nello stack del contenuto di EFLAGS
e dell'indirizzo di ritorno (valore corrente di CS:EIP); alcuni tipi di
eccezione salvano nello stack anche un codice di errore.
Un'altra differenza importante è che una ISR termina con IRET; questa
istruzione ripristina il contenuto di CS:EIP e EFLAGS, usando i dati
salvati in precedenza nello stack.
Un Interrupt Gate o un Trap Gate chiamano una ISR ponendo il
Trap Flag (TF) a zero, dopo che EFLAGS è stato salvato nello
stack; ciò è necessario per evitare che in fase di debugging venga generata una
interruzione hardware per ogni istruzione eseguita dalla stessa ISR (cosa
che provocherebbe un loop infinito). Lo stato originale di TF viene poi
ripristinato da IRET.
Un Interrupt Gate chiama una ISR ponendo IF=0; in questo modo
le altre interruzioni mascherabili vengono messe in attesa, fino al termine della
ISR stessa. Lo stato originale di IF viene poi ripristinato da
IRET.
Un Trap Gate chiama una ISR lasciando inalterato IF.
6.5.6 Meccanismo di protezione per interruzioni ed eccezioni
La regola generale è che una interruzione o eccezione non può provocare il
trasferimento del controllo ad una ISR definita in un segmento di codice meno
privilegiato.
Per evitare che questa regola venga violata, un primo metodo consiste nel definire
le ISR in un segmento di codice conforme (C=1); in questo caso, sappiamo
infatti che la ISR stessa si adatta al CPL del caller. Per motivi di
sicurezza, una ISR definita in un segmento di codice conforme dovrebbe usare
solo dati presenti nello stack; come è stato spiegato in 6.1.6, l'accesso da
un segmento di codice conforme a segmenti di dati con elevata protezione creerebbe
una situazione di vulnerabilità.
In alternativa, si possono definire le ISR in segmenti di codice con
DPL=0; tali ISR vengono eseguite comunque, indipendentemente dal
CPL del caller.
6.5.7 Codici di errore
Alcune eccezioni sono associate ad un codice di errore che fornisce informazioni
utili sul problema che si è verificato; tale codice assume la struttura mostrata in
Figura 6.29 e viene inserito dalla CPU in cima allo stack prima della chiamata
di una ISR.
I 16 bit più significativi sono riservati e hanno lo scopo di tenere il
contenuto dello stack allineato alla DWORD.
Il campo EXT in posizione 0 vale 0 se si è verificata un'eccezione
mentre veniva eseguita un'istruzione all'indirizzo CS:EIP (tale indirizzo viene
salvato nello stack prima del codice di errore); si ha invece EXT=1 se l'eccezione
è legata al verificarsi di un evento esterno al programma (ad esempio, una interrupt
hardware).
Se il campo IDT in posizione 1 vale 0, l'eccezione è dovuta ad un
problema presente in un segmento elencato in una normale tabella (GDT o
LDT); in tal caso, il campo TI indica il tipo di tabella. Se IDT=1,
allora il riferimento è alla IDT; in tal caso, il campo TI deve essere
ignorato.
Se IDT=0, allora il campo TI in posizione 2 indica il tipo di
tabella; TI=0 indica la GDT, mentre TI=1 indica la LDT.
Il campo INDEX (bit da 3 a 15) individua il descrittore nella
tabella (GDT, IDT o LDT) selezionata dai campi precedenti.
6.5.8 Vettori riservati per le eccezioni
I vettori di interruzione da 0 a 31, per convenzione, sono riservati; i
primi 16 di essi sono sempre presenti e hanno le caratteristiche illustrate in
Figura 6.24.
Questa eccezione si verifica quando si usano le istruzioni DIV e IDIV
con denominatore 0 o, più in generale, quando tali due istruzioni producono
un risultato troppo grande per essere contenuto nell'operando destinazione.
La coppia CS:EIP salvata nello stack punta all'istruzione DIV o
IDIV che ha prodotto l'eccezione; la ISR associata deve provvedere
a gestire anche tale situazione per evitare di innescare un loop infinito. Le
stesse considerazioni valgono per tutte le eccezioni legate a problemi critici.
Nessun codice di errore.
- Vettore 1: Debug Exception
Questa eccezione viene generata in seguito al verificarsi delle seguenti condizioni:
- Breakpoint all'indirizzo di una istruzione (fault)
- Breakpoint all'indirizzo di un dato (trap)
- General detect fault (fault)
- Single-step (trap)
- Breakpoint su un task switch (trap)
Ponendo, ad esempio, TF=1 nel registro EFLAGS (Figura 6.25) si sta
chiedendo alla CPU di generare una INT 1 dopo ogni istruzione eseguita;
questa interruzione viene largamente usata dai debuggers per eseguire i programmi in
modalità "passo passo" (single step).
La coppia CS:EIP salvata nello stack punta all'istruzione successiva a quella
che ha provocato l'eccezione.
La CPU non genera alcun codice di errore, ma la ISR associata al vettore
1 può analizzare la situazione consultando i registri di debug (illustrati nei
capitoli successivi).
- Vettore 2: Non Maskable Interrupt (NMI)
Questa interrupt viene generata quando sul PC si è verificato qualche grave
problema hardware, come un errore di parità in memoria, un livello insufficiente
della tensione elettrica nei circuiti, etc; data la delicatezza della situazione,
la NMI viene inviata direttamente alla CPU tramite l'omonimo pin.
L'istruzione INT 3 viene utilizzata dai debuggers per inserire dei punti
di interruzione (breakpoints) nei programmi; la ISR chiamata può così
permettere di esaminare la situazione in quei punti, attraverso l'analisi del
contenuto dei vari registri della CPU.
Per rendere possibile questo tipo di utilizzo, la INT 3 ha un opcode
(CCh) formato da un solo byte, che può essere quindi posizionato
all'inizio di qualsiasi istruzione, senza il rischio di sovrascrivere
l'istruzione successiva. Il debugger salva l'opcode dell'istruzione in cui
posizionare il breakpoint e sostituisce il suo primo byte con CCh;
quando il breakpoint viene rimosso, l'opcode dell'istruzione sovrascritta
viene ripristinato.
La coppia CS:EIP salvata nello stack punta al BYTE immediatamente
successivo all'opcode CCh.
Nessun codice di errore.
- Vettore 4: INTO Detected Overflow
Nel caso generale, un overflow prodotto da una determinata operazione tra numeri
interi con segno, viene segnalato dalla CPU ponendo a 1 il bit OF
del registro EFLAGS (Figura 6.25); se si vuole gestire questa situazione tramite
una ISR, si può eseguire l'istruzione INTO immediatamente dopo l'operazione
stessa. Se INTO trova OF=1, provvede a generare una INT 4.
La coppia CS:EIP salvata nello stack punta all'istruzione immediatamente successiva
alla INTO.
Nessun codice di errore.
- Vettore 5: BOUND Range Exceeded
L'istruzione BOUND richiede un operando di tipo Reg16 e uno di tipo
Mem16:Mem16 oppure un operando di tipo Reg32 e uno di tipo
Mem32:Mem32; il primo operando contiene l'offset dell'elemento di un vettore
a cui vogliamo accedere, mentre il secondo contiene l'offset del primo elemento e
quello dell'ultimo. Se l'offset in Reg16 (o Reg32) non rientra nei
limiti indicati, viene generata una INT 5.
La coppia CS:EIP salvata nello stack punta alla stessa istruzione BOUND
che ha generato l'eccezione; ciò è necessario per scongiurare un accesso ad un'area
della memoria che non appartiene al vettore.
Nessun codice di errore.
- Vettore 6: Invalid Opcode
In presenza di una istruzione associata ad un opcode non valido, la CPU
genera una INT 6; ciò accade nel momento in cui l'istruzione stessa sta
per essere eseguita e non quando viene caricata nella coda di prefetch.
Questa eccezione viene generata anche per una istruzione associata ad operandi non
consentiti; ad esempio, una JMP che usa un registro come operando destinazione
o una LES che specifica un registro come sorgente. La INT 6 viene
generata anche quando si usa il prefisso LOCK con istruzioni non compatibili.
La coppia CS:EIP salvata nello stack punta all'istruzione associata
all'opcode non valido.
Nessun codice di errore.
- Vettore 7: Coprocessor Not Available
Questa eccezione viene generata quando si tenta di eseguire un'istruzione della
FPU su un PC che però non è dotato di coprocessore matematico; in
tal caso si ha EM=1 nel registro CR0, per indicare che il coprocessore
deve essere emulato via software. Un programma che intercetta questa eccezione, può
chiamare l'opportuna procedura che simula via software la funzione matematica
richiesta.
La INT 7 viene generata anche quando si esegue una WAIT o una
istruzione della FPU (ESC instruction) con MP=1 e TS=1
in CR0.
La coppia CS:EIP salvata nello stack punta all'istruzione che ha generato
l'eccezione.
Nessun codice di errore.
Può capitare che, mentre la CPU sta elaborando un'eccezione, ne arrivi una
seconda; normalmente, in casi del genere la seconda eccezione viene elaborata
dopo la prima, ma ciò non sempre è possibile. Consideriamo, ad esempio, una
eccezione di segmento non presente (INT 11); se la ISR per gestire
tale eccezione si trova a sua volta in un segmento non presente, si verifica una
condizione di Double Fault e nessuna delle due eccezioni può essere elaborata.
Per stabilire quando generare una Double Fault, la CPU suddivide le
interruzioni ed eccezioni di Figura 6.24 nelle tre seguenti categorie:
- Benign Exception/Interrupt: 1, 2, 3, 4, 5, 6, 7, 16
- Contributory Exception: 0, 9, 10, 11, 12, 13
- Page Fault: 14
Due eventi di tipo "benign" oppure uno di tipo "benign" e uno "contributory", vengono
gestiti in successione e quindi non provocano un Double Fault; due eventi di
tipo "contributory" non possono essere gestiti in successione e quindi provocano un
Double Fault.
Un evento di tipo "benign" o "contributory" seguito da un Page Fault possono
essere gestiti in successione e quindi non provocano un Double Fault; lo stesso
vale per un Page Fault seguito da un evento "benign". Se però un Page Fault
è seguito da un evento "contributory" o da un altro Page Fault, viene generata
una Double Fault.
L'istruzione che ha creato il problema non è riavviabile.
Se una nuova eccezione si verifica mentre è in fase di gestione una Double Fault,
la 80386 avvia la fase di "shutdown"; l'esecuzione delle istruzioni viene sospesa
e la CPU può essere riattivata solo dall'arrivo di una NMI o da un segnale
di RESET.
Per gestire in modo adeguato una Double Fault, la ISR deve essere chiamata
tramite un Task Gate; il back link del TSS associato alla ISR punta
al TSS del task che ha prodotto l'eccezione.
La coppia CS:EIP salvata nello stack è indefinita; la CPU provvede ad
inserire nello stack anche un codice di errore che vale sempre 0.
- Vettore 9: Coprocessor Segment Overrun
La INT 9 segnala che un'istruzione del coprocessore matematico 80387
ha superato il campo LIMIT durante la fase di lettura o scrittura in un
segmento di dati; questo tipo di interruzione è tipico dei sistemi dove la
FPU è separata dalla CPU, per cui le verifiche sul rispetto delle
regole di protezione risultano indipendenti per i due dispositivi.
Nei sistemi basati sulla 80386, il coprocessore matematico 80387 è
un dispositivo esterno; di conseguenza, la INT 9 è un evento asincrono, come
accade per le interruzioni hardware. Per lo stesso motivo, la CPU non è in
grado di sapere quale istruzione della FPU ha provocato l'interruzione; per
conoscere tale informazione, è necessario consultare gli appositi registri della
stessa FPU (si veda il Capitolo 17 del tutorial Assembly Avanzato).
Il task che è stato interrotto dalla INT 9 può anche non essere quello che
ha eseguito l'istruzione della FPU che ha provocato l'interruzione; in tal
caso, il task stesso è riavviabile.
La coppia CS:EIP salvata nello stack punta alla stessa istruzione che ha
generato l'eccezione.
Nessun codice di errore.
- Vettore 10: Invalid Task State Segment
Può capitare che, in seguito ad un task switch, il selettore presente nel Task
Gate punti ad un TSS non valido; in tal caso, la CPU genera una
INT 10.
La CPU inserisce nello stack anche un codice di errore, con il bit in EXT
che indica se l'eccezione è dovuta o meno ad un evento esterno; la Figura 6.30 mostra i
casi che rendono non valido un TSS e i relativi codici di errore.
Per gestire in modo adeguato una Invalid TSS Exception, la ISR deve
essere chiamata tramite un Task Gate; la stessa ISR deve occuparsi della
corretta impostazione del Busy Bit nel nuovo TSS.
Se il problema si è verificato prima del task switch, la coppia CS:EIP salvata
nello stack punta alla stessa istruzione che ha generato l'eccezione; in caso contrario,
punta alla prima istruzione del nuovo task.
- Vettore 11: Segment Not Present
La INT 11 viene generata quando si tenta di accedere ad un segmento che ha
P=0; ciò accade in particolare quando si prova a caricare tale segmento in
CS, DS, ES, FS e GS, mentre se il registro è
SS viene generato uno Stack Fault.
Un altro caso riguarda il tentativo di caricare, con LLDT, una LDT che
ha P=0; se ciò però accade durante un task switch, viene generata una INT
10.
Un ulteriore caso si ha quando si tenta di usare un gate il cui descrittore ha
P=0.
Il codice di errore inserito nello stack ha la stessa forma di quelli illustrati in
Figura 6.29; se EXT=1, il problema è dovuto ad un segmento referenziato dalla
IDT.
La INT 11 viene usata dai SO per implementare la memoria virtuale basata
sui segmenti; la ISR chiamata da questa eccezione deve provvedere a caricare in
memoria un segmento con P=0 in quanto spostato su disco.
Bisogna prestare attenzione al fatto che, se la INT 11 avviene durante un task
switch, il contenuto dei registri DS, ES, FS e GS potrebbe
essere non usabile, in quanto modificato dallo stesso task switch; il compito di gestire
correttamente questa situazione spetta alla ISR.
La coppia CS:EIP salvata nello stack punta alla stessa istruzione che ha generato
l'eccezione; tale istruzione è riavviabile, purché la ISR risolva il problema
prima di restituire il controllo.
Questa eccezione viene generata in caso di violazione dei limiti dello stack (ad
opera di istruzioni come PUSH, POP, ENTER, LEAVE), ma
anche quando si tenta di caricare in SS il selettore di un segmento non
valido (P=0).
Il bit EXT nel codice di errore indica se il problema è stato causato da un
evento interno (programma) o esterno (interruzione); se l'evento è interno, il codice
di errore fa riferimento al segmento responsabile dell'eccezione, mentre per un evento
esterno il codice di errore è 0.
Per gestire questo tipo di eccezione è raccomandato l'uso di un Task Gate.
Bisogna prestare attenzione al fatto che, se la INT 12 avviene durante un task
switch, il contenuto dei registri DS, ES, FS e GS potrebbe
essere non usabile, in quanto modificato dallo stesso task switch; il compito di gestire
correttamente questa situazione spetta alla ISR.
La coppia CS:EIP salvata nello stack punta alla stessa istruzione che ha generato
l'eccezione; tale istruzione è riavviabile, purché la ISR risolva il problema
prima di restituire il controllo.
- Vettore 13: General Protection
Tutte le violazioni delle regole di protezione che non ricadono nei casi illustrati in
precedenza, vengono trattate come General Protection Fault e generano una INT
13; nel seguito vengono elencati gli eventi principali.
- Violazione del campo LIMIT nell'uso di segmenti referenziati da CS,
DS, ES, FS e GS.
- Violazione del campo LIMIT nell'uso di una tabella dei descrittori.
- Trasferimento del controllo verso un segmento non eseguibile.
- Tentativo di scrittura su un segmento di codice o dati Read Only.
- Tentativo di lettura da un segmento Execute Only.
- Tentativo di caricare in SS il selettore di un segmento Read Only.
- Tentativo di caricare in SS, DS, ES, FS o GS il
selettore di un descrittore speciale (S=0).
- Tentativo di caricare in DS, ES, FS o GS il selettore di
un segmento Execute Only
- Tentativo di caricare in SS il selettore di un segmento eseguibile.
- Tentativo di accedere alla memoria con DS, ES, FS o GS
che contengono il NULL SELECTOR.
- Tentativo di task switch verso un task con B=1 (Busy).
- Tentativo di violare i livelli di privilegio.
- Eccedere il limite dei 15 byte per la lunghezza di una istruzione.
- Caricare in CR0 PG=1 (Page Enabled) e PE=0
(Protection Disabled).
- Tentativo di gestire una interruzione nella modalità virtuale 8086 con una
ISR con livello di privilegio diverso da 0.
Il codice di errore inserito nello stack fornisce tutti i dettagli necessari per
identificare il problema.
Se l'eccezione è dovuta al tentativo di usare un descrittore non valido, il codice
di errore contiene il selettore del descrittore stesso; in tutti gli altri casi
vale 0 (NULL SELECTOR).
Bisogna prestare attenzione al fatto che, se la INT 13 avviene durante un
task switch, il contenuto dei registri DS, ES, FS e GS
potrebbe essere non usabile, in quanto modificato dallo stesso task switch; il compito
di gestire correttamente questa situazione spetta alla ISR.
La coppia CS:EIP salvata nello stack punta alla stessa istruzione che ha
generato l'eccezione.
La Page Fault viene generata se PG=1 in CR0 e la CPU
individua un problema nel tentativo di convertire un indirizzo lineare in un
indirizzo fisico; si presentano i due casi elencati nel seguito.
- L'elemento della Page Directory o della Page Table a cui si
tenta di accedere ha P=0.
- Il task che tenta di accedere ad una pagina non ha privilegi sufficienti.
La CPU chiama la relativa ISR dopo aver inserito nello stack un codice
di errore la cui struttura è illustrata in Figura 6.31.
Il bit U/S indica se l'eccezione si è verificata con la CPU in
Supervisor Mode (U/S=0) o in User Mode (U/S=1).
Il bit W/R indica se l'eccezione si è verificata in un tentativo di lettura
(W/R=0) o di scrittura (W/R=1).
Il bit P indica se l'eccezione è stata causata da una pagina non presente
(P=0) o da privilegi insufficienti (P=1).
La CPU inoltre inserisce in CR2 l'indirizzo lineare a 32 bit
dell'istruzione che ha provocato l'eccezione; la ISR chiamata può usare questa
informazione per individuare i corrispondenti elementi della Page Directory e
della Page Table.
Una Page Fault può verificarsi durante un task switch; tale evento può essere
dovuto ad una delle cause elencate nel seguito.
- Salvataggio dello stato del vecchio task nel relativo TSS.
- Accesso alla GDT per leggere il descrittore del TSS del nuovo
task.
- Lettura del TSS del nuovo task.
- Lettura della LDT del nuovo task.
Negli ultimi due casi l'eccezione si verifica nel nuovo task.
Una Page Fault può verificarsi nel bel mezzo delle due istruzioni:
In tal caso, la relativa ISR verrebbe chiamata con l'uso di uno stack non
inizializzato correttamente; come è stato spiegato in precedenza, per evitare un
problema del genere è vivamente raccomandato l'uso di LSS per caricare in
un solo passaggio una coppia Selector:Offset in SS:ESP.
Per la chiamata della ISR associata ad una Page Fault, è raccomandato
l'uso di un Task Gate.
La coppia CS:EIP salvata nello stack punta alla stessa istruzione che ha
generato l'eccezione (nel vecchio o nuovo task).
- Vettore 16: Coprocessor Error
Questa eccezione viene generata quando la CPU riceve un segnale dal
coprocessore matematico 80387 attraverso il pin ERROR#. Se EM=0
(nessuna emulazione) in CR0, la CPU testa il pin ERROR# ogni
volta che incontra una istruzione ESC o una WAIT.
Bibliografia
Intel 80386 Hardware Reference Manual
(disponibile su Internet)
Intel 80386 Programmer's Reference Manual
(disponibile su Internet)
Intel ASM386 Assembly Language Reference
(disponibile su Internet)