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: 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: 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: Ogni volta che accediamo in memoria, la CPU si assicura che un segmento non venga usato in modo improprio; in particolare: 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 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: 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: 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: 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: 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: 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: 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: 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:

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

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: 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: In alternativa, un indirizzo di porta può essere indicato nei programmi tramite il registro DX; in tal caso, si possono gestire: 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: 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: 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: 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: 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. Questa eccezione viene generata in seguito al verificarsi delle seguenti condizioni: 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). 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. 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. 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. 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. 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: 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. 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. 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. 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. 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). 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)