Modalità Protetta

Capitolo 3: Esempi pratici per la Modalità Protetta 80286


In questo capitolo vengono presentati degli esempi che mettono in pratica i concetti teorici esposti nei capitoli 1 e 2; prima però è necessario analizzare alcuni aspetti relativi al controllo e inizializzazione della CPU 80286 e alle istruzioni "privilegiate".

3.1 Controllo e inizializzazione del sistema. Istruzioni privilegiate

La CPU 80286 si serve di appositi registri per il controllo del sistema e per la gestione della modalità protetta; il contenuto di tali registri può essere alterato attraverso una serie di nuove istruzioni, eseguibili solo a determinati livelli di privilegio.

3.1.1 Il registro FLAGS

La Figura 3.1 mostra la struttura del registro FLAGS, che sulla 80286 introduce i due nuovi campi NT e IOPL. Il campo IOPL è formato da due bit e permette di rappresentare i soliti 4 livelli di privilegio; il suo scopo è quello di stabilire il massimo valore numerico del CPL al di sopra del quale il task corrente non è più autorizzato ad eseguire determinate istruzioni.
Un task che gira con livello di privilegio CPL può eseguire le istruzioni IN, INS, INSB, INSW, OUT, OUTS, OUTSB, OUTSW, STI e CLI, se e solo se:
CPL ≤ IOPL
Un task che prova ad eseguire CLI o STI avendo CPL maggiore (numericamente) di IOPL, produce una eccezione 13; in questo caso, il flag IF rimane inalterato. Analogo discorso se si tenta di modificare IF eseguendo POPF, direttamente o tramite IRET. Il flag NT viene posto a 1 quando una istruzione CALL o INT specifica un selettore che punta ad un task gate; il task corrente viene quindi interrotto e il controllo passa ad un nuovo task innestato nel primo (nested task). Il vecchio e il nuovo TSS vengono marcati come "busy" (B=1); nel back link del nuovo TSS viene caricato un selettore che punta al vecchio TSS.
Al termine di un task innestato, una IRET che trova NT=1 restituisce il controllo al vecchio task attraverso un task switch; per i task non innestati si ha NT=0, per cui la IRET si comporta come in modalità reale. Osserviamo che in relazione a SAHF (Store AH register into FLAGS), non ci sono problemi in quanto tale istruzione influisce solo sugli 8 bit meno significativi del registro FLAGS.

3.1.2 Il registro Machine Status Word

Il MSW rappresenta in ogni istante la configurazione e lo stato della 80286 (e non del task in esecuzione); la Figura 3.2 illustra la struttura di questo registro. Il contenuto del MSW in genere viene caricato in fase di inizializzazione della modalità protetta attraverso l'apposita istruzione LMSW (Load Machine Status Word Register); se si vuole conoscere il contenuto del MSW si può ricorrere all'istruzione SMSW (Store Machine Status Word Register).

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 situazione è tipica dei sistemi dove la CPU è separata dalla FPU, per cui le due unità si servono di meccanismi di protezione indipendenti; il flag TS quindi ha anche lo scopo di proteggere un task da un errore causato da un task differente mentre eseguiva una istruzione del coprocessore.
La ISR che intercetta l'eccezione 7 ha il compito di riportare TS a 0 attraverso l'apposita istruzione CLTS (Clear Task Switched Flag).

Il flag EM permette di stabilire se una determinata funzione del coprocessore matematico deve essere emulata via software. Se EM=1 e MP=0 (funzione non presente e coprocessore non presente), il tentativo di eseguire una istruzione della FPU produce una eccezione 7; la ISR che intercetta tale eccezione può così chiamare la procedura che deve emulare via software quella istruzione.

Il flag MP indica la presenza o meno della FPU; se MP=1, la FPU è presente. Se MP=1 e TS=1, il tentativo di eseguire una qualsiasi istruzione della FPU produce l'eccezione 7; come abbiamo visto, la ISR che intercetta tale eccezione può così verificare se la FPU è in uso al task corrente o ad un altro task.

La Figura 3.3 elenca le configurazioni raccomandate per i primi tre bit del registro MSW; tutte le restanti configurazioni sono da evitare in quanto possono dare luogo ad anomalie (in particolare, se è previsto l'uso delle istruzioni della FPU, uno dei due bit MP o EM deve essere posto a 1, ma non entrambi).
Ricordiamo che le istruzioni della FPU sono chiamate Escape Instructions (o ESC Instructions) in quanto i loro opcode iniziano tutti per 11011b, che coincide con il codice ASCII del tasto ESC. Il flag PE indica la modalità operativa della 80286. Se PE=0, la CPU è in configurazione di compatibilità con la modalità reale; se PE=1, la CPU è in modalità protetta. Si tenga presente però che PE=1 indica solo che sono attive tutte le funzionalità della 80286 per il supporto della modalità protetta; come abbiamo visto nel capitolo 1, per accedere fisicamente a tutta la RAM disponibile in tale modalità è necessario abilitare la linea A20 dell'Address Bus.

Un aspetto curioso è che i progettisti della 80286 hanno previsto un modo per entrare in modalità protetta, ma non per uscirne; l'unica possibilità di tornare in modalità reale, come vedremo nel seguito, è un reset della CPU.

3.1.3 Istruzioni privilegiate

Si definiscono "privilegiate" tutte quelle istruzioni eseguibili solamente da un task che gira a CPL=0; se questo requisito non è soddisfatto, il tentativo di eseguire tali istruzioni produce una eccezione 13 con codice di errore 0.
Si tratta di istruzioni destinate a modificare il contenuto delle tabelle dei descrittori e dei registri di sistema; in questo capitolo e in quelli precedenti abbiamo già incontrato alcune di esse, come LGDT, LLDT, LIDT, LTR, LMSW e CLTS.

Le istruzioni LGDT e LIDT vengono usate in fase di inizializzazione della modalità protetta, per cui sono necessariamente disponibili anche in modalità reale; il loro scopo è quello di caricare nei rispettivi registri, GDTR e IDTR, i descrittori della GDT e della IDT. Eventuali modifiche successive della GDT o della IDT con tali istruzioni, possono essere effettuate solo da un task (generalmente, il SO) che gira con CPL=0.
Le corrispondenti istruzioni SGDT e SIDT possono essere eseguite a qualunque livello di privilegio.

L'istruzione LLDT può essere usata solo quando si è già in modalità protetta, da un task che gira a CPL=0; in caso contrario, viene generata l'eccezione 13.
LLDT ha lo scopo di inizializzare il LDTR (parte visibile) con il selettore che punta al descrittore (nella GDT) della LDT del primo task da eseguire; gli aggiornamenti successivi di tale registro vengono gestiti in automatico dalla CPU ad ogni task switch.
La corrispondente istruzione SLDT può essere eseguita a qualunque livello di privilegio.

Si tenga presente che LLDT aggiorna il LDTR, ma non ha alcun effetto sul TSS del relativo task e sui registri di segmento; per una corretta gestione di un task switch, i passaggi da compiere sono i seguenti: Per il TR valgono considerazioni del tutto analoghe. L'istruzione LTR può essere usata solo da un task che gira a CPL=0 ed ha lo scopo di caricare nel TR (parte visibile) il selettore che punta al descrittore (nella GDT) del TSS del primo task da eseguire; successivamente, lo stesso TR viene aggiornato in automatico ad ogni task switch.
La corrispondente istruzione STR può essere eseguita a qualunque livello di privilegio.

LTR provvede anche a porre a 1 il busy bit nel descrittore del nuovo TSS; il compito di aggiornare i registri di segmento spetta invece al software di sistema.

L'istruzione LMSW ha il compito di inizializzare il MSW, per cui è disponibile anche in modalità reale; eventuali modifiche successive di tale registro con LMSW sono possibili solo da parte di un task che gira a CPL=0.
La corrispondente istruzione SMSW può essere eseguita a qualunque livello di privilegio.

L'istruzione CLTS ha il compito di riportare a 0 il flag TS nel MSW; può essere eseguita solo da un task che gira a CPL=0.

L'istruzione HLT (halt) fa in modo che la 80286 interrompa l'esecuzione delle istruzioni; la CPU entra in una modalità denominata HALT state e può essere riesumata solo dal ricevimento di una interrupt o da un comando di reset. Questa istruzione può essere eseguita solo da un task che gira a CPL=0.

L'istruzione POPF può modificare il campo IOPL del registro FLAGS solo se viene eseguita da un task che gira a CPL=0.

3.1.4 Inizializzazione della CPU 80286

Quando si accende un PC o si invia un segnale RESET all'omonimo pin, la 80286 compie una serie di operazioni il cui scopo è quello di definire lo stato iniziale della CPU; terminate queste operazioni, la linea A20 dell'Address Bus viene disabilitata e si entra nella modalità operativa reale.
Lo stato iniziale dei vari registri è il seguente: Nel registro FLAGS quindi, tutti i bit sono azzerati, tranne quello in posizione 1 che però non viene usato dalla 80286. IF=0 fa si che vengano disabilitate le interruzioni mascherabili; ciò è importantissimo in quanto si evita che una eventuale interrupt chiami la relativa ISR, con conseguente utilizzo dello stack, che però non è stato ancora allocato (SP, come viene spiegato più avanti, è indefinito).

Nel registro MSW tutti i bit vengono posti a 1, tranne i primi quattro che valgono 0; come è stato spiegato in precedenza, se si vuole garantire la compatibilità con le CPU di classe superiore, è fondamentale preservare il contenuto dei bit dalla posizione 4 alla 15.
Abbiamo quindi PE=0 (modalità reale), MP=0 (coprocessore assente) e EM=0 (nessuna emulazione); si ha poi TS=0 per indicare l'assenza di task innestati.

La IDT coincide chiaramente con la IVT della modalità reale in quanto ha una dimensione di 1024 byte e parte dall'indirizzo assoluto 000000H (che in modalità reale corrisponde a 00000H); viene quindi riservata un'area sufficiente a contenere 256 vettori di interruzione da 16+16 bit ciascuno.

La coppia CS:IP in modalità reale viene interpretata come indirizzo logico F000H:FFF0H, che corrisponde all'indirizzo fisico a 20 bit FFFF0H; nel rispetto della convenzione adottata sulle piattaforme hardware 80x86, si tratta dell'ultimo paragrafo del primo MiB di RAM indirizzabile dalla 8086, dove in genere è presente una JMP che salta ad un'area della memoria contenente il codice di inizializzazione del sistema.
In modalità protetta, la parte invisibile di CS fa riferimento ad un segmento di codice con indirizzo base FF0000H e LIMIT pari a FFFFH (64 KiB); sommando il Base Address a IP=FFF0H si ottiene l'indirizzo a 24 bit FFFFF0H, che anche in questo caso è l'ultimo paragrafo dei 16 MiB fisici indirizzabili dalla 80286 (in genere, tale indirizzo è mappato in una EPROM).

I registri di segmento DS, ES e SS, sia in modalità reale sia in quella protetta, referenziano i primi 64 KiB della RAM; il registro SP è da considerare indefinito.

3.1.5 Inizializzazione della Modalità Protetta

Per una corretta inizializzazione della modalità protetta, si raccomanda di procedere con i seguenti passi: Il passo 4 è importantissimo dato che, una volta posto PE=1, le istruzioni presenti nella coda di prefetch della 80286 non sono più valide in quanto destinate alla modalità reale; eseguendo allora una JMP intrasegmento, si obbliga la CPU a svuotare la stessa coda e a riempirla con le nuove istruzioni per la modalità protetta.

Per il passo 6, se non si intende utilizzare alcuna LDT, si deve caricare il NULL Selector nel LDTR.

3.2 Esempi pratici

La modalità protetta rappresenta una sfida piuttosto impegnativa per gli sviluppatori, in quanto si tratta di gestire aspetti tipici della programmazione di sistema, che richiedono quindi una adeguata conoscenza anche dell'hardware del computer; ciò è dovuto al fatto che, come è stato già spiegato in precedenza, con la CPU in tale stato operativo non possiamo più beneficiare di tutto ciò che è destinato alla modalità reale, come i servizi forniti dal DOS e dal BIOS o le ISR per l'elaborazione delle interruzioni predefinite.
Non possiamo allocare memoria, né gestire l'I/O su file e neppure interagire con le varie periferiche del PC; se abbiamo bisogno di tali funzionalità, dobbiamo scrivere tutto il codice necessario per riprogrammare l'hardware che controlla gli hard disk, la tastiera, il mouse, etc.
In sostanza, la gestione completa della modalità protetta comporta la scrittura di un intero sistema operativo!

Questi tutorial hanno uno scopo puramente didattico, per cui gli esempi illustrati devono essere necessariamente semplici e comprensibili; anche così però, come vedremo nel seguito, il codice da scrivere presenta tutte le caratteristiche di un "mini sistema operativo".

Per semplificarci la vita, prendiamo spunto da una tecnica impiegata da alcuni vecchi SO, che consiste nell'utilizzare il DOS come "trampolino di lancio" della modalità protetta; in questo modo, possiamo scrivere dei programmi di esempio che assumono la seguente struttura: Per rispettare questa struttura, facciamo in modo che il nucleo (kernel) del nostro programma risieda interamente nel primo MiB di RAM; una volta entrati poi in modalità protetta, possiamo anche decidere se sfruttare ulteriore memoria presente nei 16 MiB indirizzabili dalla 80286.

Osserviamo ora che, quando il DOS carica un programma in memoria, procede alla rilocazione delle componenti Seg e Offset; noi però vogliamo che gli offset partano da zero e non vengano rilocati in quanto, in modalità protetta, un indirizzo virtuale è formato da una coppia Selector:Offset, dove Offset è uno spiazzamento compreso tra 0000h e FFFFh, da sommare al Base Address. Ricordando quanto è stato esposto nel Capitolo 12 del tutorial Assembly Base, in modalità reale gli offset partono da zero e non subiscono rilocazione quando il segmento di appartenenza è allineato al paragrafo (16 byte); tutto ciò che dobbiamo fare allora consiste nel definire segmenti di programma con quel tipo di allineamento.
Con NASM possiamo scrivere, ad esempio:
CODESEGM SEGMENT ALIGN=16 PUBLIC USE16 CLASS=CODE
In versione MASM:
CODESEGM SEGMENT PARA PUBLIC USE16 'CODE'
Per definire poi i descrittori dei vari segmenti di programma, abbiamo bisogno dei rispettivi Base Address e ciò significa convertire a 24 bit gli indirizzi fisici associati alle componenti Seg; tale compito è semplicissimo in quanto sappiamo che, in modalità reale, l'indirizzo fisico a 20 bit associato si ottiene moltiplicando Seg per 16 (shift di 4 bit verso sinistra). Se, ad esempio, CODESEGM=03BFh, si ha:
03BFh * 16 = 03BF0h
Questo risultato va poi esteso a 24 bit con degli zeri a sinistra, in modo da ottenere 003BF0h.

In relazione al campo LIMIT, la situazione più semplice riguarda il caso in cui abbiamo a che fare con segmenti di lunghezza fissa; ad esempio, se per un segmento dati DSEGM abbiamo definito una costante DSEGM_SIZE, otteniamo subito:
LIMIT = DSEGM_SIZE - 1
Negli altri casi possiamo servirci di una etichetta posta alla fine del segmento; se, alla fine di un segmento di codice di nome CODESEGM scriviamo:
end_codesegm:
allora si ha:
LIMIT = (offset end_codesegm) - 1
Ci servono anche gli ACCESS BYTE per i tre tipi di segmenti di programma, codice, dati e stack (Expand Down); a tale proposito possiamo definire le tre costanti seguenti: Tutto ciò ci torna utile nel momento in cui vogliamo scrivere procedure che visualizzano stringhe o numeri nella memoria video; ricordando che alla modalità video testo a colori è riservata un'area da 32 KiB a partire dall'indirizzo logico B800h:0000h, si tratta di definire un apposito descrittore con Base Address 0B8000h, LIMIT pari a 32768-1 byte e ACCESS BYTE di tipo ACCBYTE_DATA.

Nel caso invece della memoria oltre il primo MiB, possiamo definire direttamente dei descrittori di segmento con Base Address a 24 bit come, ad esempio, 13FBC0h; ciò è possibile in quanto stiamo accedendo ad un'area della RAM al di fuori del controllo del DOS. Appare evidente però che, per gestire al meglio una simile situazione, sarebbe opportuno scrivere un apposito memory manager che si occupi di allocare e deallocare i blocchi di memoria di cui abbiamo bisogno.

Una volta chiariti questi aspetti, vediamo in dettaglio i vari passi da compiere per preparare la CPU al passaggio in modalità protetta e al ritorno in modalità reale. Facciamo riferimento ad un primo esempio estremamente semplice, che si basa sull'uso della sola GDT; tutto il codice gira a CPL=0, i descrittori di segmento hanno tutti DPL=0 e i relativi selettori specificano RPL=0.

3.2.1 Identificazione del tipo di CPU

Il requisito minimo per gli esempi che seguono è la presenza di una 80286; la prima cosa da fare consiste quindi nell'identificare il tipo di CPU di cui disponiamo. Il metodo seguito si basa sul fatto che i vari tipi di CPU della famiglia 80x86 possono supportare o meno determinate caratteristiche; si tratta quindi di procedere per esclusione.
Nel nostro caso, come risulta da quanto esposto nel Capitolo 16 del tutorial Assembly Base, sfruttiamo il fatto che in presenza dell'istruzione:
PUSH SP
le 8086, 8088, 80186 e 80188 salvano nello stack SP-2; in sostanza, SP viene prima decrementato di 2 e il valore così ottenuto viene salvato nello stack.
Questo comportamento non è stato ritenuto corretto dai progettisti di CPU, per cui a partire dalla 80286 viene salvato il vecchio SP nello stack; quindi, prima viene salvato nello stack il valore corrente di SP e poi lo stesso SP viene decrementato di 2.
Possiamo scrivere allora il seguente codice: Non è necessario effettuare ulteriori verifiche, in quanto gli esempi presentati nel seguito girano perfettamente anche su CPU 80386 o superiori; l'importante è rispettare tutti gli aspetti esposti in questo e nei precedenti capitoli, in relazione alla compatibilità verso l'alto della modalità protetta 80286 (in particolare, tutti i campi riservati nei descrittori e nei registri come FLAGS e MSW, devono essere posti a zero).

3.2.2 Disabilitazione delle interruzioni mascherabili e della NMI

Ricordiamo che, una volta entrati in modalità protetta, non possiamo più utilizzare le ISR predefinite che gestiscono la NMI e le interruzioni hardware inviate dai PIC alla CPU attraverso il pin INTR; tali ISR vengono installate in fase di avvio del PC dal BIOS e dal DOS e sono destinate alla modalità reale. Se arrivasse quindi una IRQ mentre siamo in modalità protetta, verrebbe chiamata la relativa ISR con conseguente violazione dei meccanismi di protezione.
Per evitare questa situazione, è necessario definire una IDT e installare le proprie ISR per le IRQ e per le eccezioni generate dalla 80286; il secondo esempio presentato in questo capitolo illustra proprio tale aspetto.

Le interruzioni mascherabili, come sappiamo, vengono disabilitate con la semplice istruzione:
cli
Abbiamo visto che tale istruzione può essere eseguita senza restrizioni in modalità reale, mentre in modalità protetta ciò è consentito solo ai task che girano con CPL≤IOPL.

In relazione alla NMI, la situazione è più delicata in quanto il suo stato può essere gestito via software solo su certi computer, mentre su altri ciò non è possibile; in più, la documentazione su tale aspetto è piuttosto scarsa e su Internet si possono reperire procedure di cui non è garantito il funzionamento.
Nel caso generale, sui PC di classe AT si sfrutta il fatto che gli indirizzi per accedere ai 128 byte della CMOS attraverso la porta 70h (Capitolo 4 del tutorial Assembly Avanzato), richiedono al massimo 7 bit; si è deciso allora di utilizzare il bit più significativo per gestire lo stato della linea NMI. Se il bit in posizione 7 viene posto a 1, la NMI viene disabilitata; se lo stesso bit vale 0, la NMI viene abilitata. Una volta che il bit 7 è stato modificato, si deve subito effettuare una lettura della porta 71h (porta di I/O della CMOS); questo passaggio è necessario per evitare che il Real Time Clock (RTC) venga lasciato in uno stato indefinito.
Possiamo scrivere allora il seguente codice: Come è stato spiegato in precedenza, il funzionamento di questa procedura non è garantito; si tenga anche presente che su certi PC la porta 70h è accessibile in sola scrittura, per cui risulta impossibile ottenere lo stato della CMOS.

3.2.3 Salvataggio dello stato dei PIC Master e Slave

Anche se abbiamo disabilitato le interruzioni mascherabili, prima di entrare in modalità protetta è buona norma salvare lo stato dei due PIC, Master e Slave; a tale proposito, dobbiamo effettuare una lettura dalle rispettive porte 21h e A1h.
Come riporta il Capitolo 3 del tutorial Assembly Avanzato, un comando inviato ad un PIC dopo la sua inizializzazione, viene trattato come Operational Command Word (OCW); nel nostro caso, inviamo un OCW1 ad entrambi i PIC in modo da avere come risposta lo stato (masked/unmasked) delle varie IRQ, memorizzato nell'Interrupt Mask Register (IMR). Supponendo allora di aver definito le due variabili picm_state e pics_state, di tipo BYTE, possiamo scrivere il seguente codice:

3.2.4 Abilitazione della linea A20 dell'Address Bus

In modalità reale, sfruttando gli indirizzi logici Seg:Offset, nessuno ci impedisce di scrivere FFFFh:FFFFh; l'unico MiB accessibile in tale modalità è formato però da tutti gli indirizzi fisici a 20 bit compresi tra 00000h e FFFFFh. L'ultimo BYTE della RAM (quello di indice FFFFFh) può essere rappresentato con l'indirizzo logico normalizzato FFFFh:000Fh; se proviamo ad avanzare di 1 otteniamo FFFFh:0010h, che corrisponde all'indirizzo fisico a 21 bit 100000h. Con l'Address Bus a 20 linee della 8086, il bit più significativo viene troncato e si ottiene 00000h; questo fenomeno, detto wrap around, si ripete per tutti gli indirizzi logici compresi tra FFFFh:0010h e FFFFh:FFFFh.
Con gli Address Bus a 24 e più linee delle CPU 80286 e superiori, tali indirizzi logici sono perfettamente leciti, per cui il wrap around non si verifica; si presenta allora il problema di come garantire la compatibilità verso l'enorme quantità di programmi destinata alla modalità reale 8086 (alcuni di tali programmi ricorrono in modo esplicito al fenomeno del wrap around).
Il problema è stato risolto sfruttando un pin rimasto libero nel chip 8042 Keyboard Controller; si tratta del pin P21 visibile in Figura 3.4. Attraverso P21, è possibile modificare lo stato della linea A20 dell'Address Bus; se P21=1, la linea A20 è abilitata, mentre P21=0 tiene la linea A20 sempre a 0.
Tenendo la linea A20 a 0, il wrap around è garantito in quanto tutti gli indirizzi logici compresi tra FFFFh:0010h e FFFFh:FFFFh produrranno indirizzi fisici a 21 bit, con il bit più significativo (quello in posizione 20) che vale sempre 0. Per garantire la compatibilità verso la 8086, tutte le CPU della famiglia 80x86 vengono inizializzate in modalità reale; la linea A20 viene quindi tenuta a 0 attraverso il pin P21 dell'8042.
Noi abbiamo la necessità di portare la 80286 in modalità protetta, con la possibilità di accedere a tutti i 16 MiB di RAM disponibili, per cui dobbiamo riprogrammare in modo opportuno l'8042; in pratica, si tratta di modificare il bit in posizione 1 di Figura 3.4, in modo da avere P21=1.
Come risulta dal Capitolo 13 del tutorial Assembly Avanzato, il comando da inviare alla porta di input per richiedere la modifica della Output Port di Figura 3.4 è D1h; il successivo comando per abilitare la linea A20 (P21=1) è DFh.
Dobbiamo ricordare che, prima di scrivere nella porta di input dell'8042, bisogna assicurarsi che il buffer di input sia vuoto; a tale proposito, si deve attendere che il bit in posizione 1 dello Status Register si sia portato a 0. Prima di leggere dalla porta di Output dell'8042, bisogna assicurarsi che il buffer di output sia pieno; a tale proposito, si deve attendere che il bit in posizione 0 dello Status Register si sia portato a 1. Supponendo allora di avere a disposizione due apposite procedure wait_for_write e wait_for_read, possiamo scrivere il seguente codice: Considerando il fatto che stiamo parlando di hardware piuttosto datato, il codice appena illustrato potrebbe non essere compatibile con tutti i PC. Un metodo alternativo consiste nel riprogrammare l'8042 in modo da modificare direttamente il bit in posizione 1 di Figura 3.4; tale metodo è disponibile nel codice sorgente presentato più avanti.

3.2.5 Predisposizione della 80286 per il ritorno in modalità reale

All'inizio del capitolo si è accennato al fatto che la 80286 è stata progettata per rendere semplice l'ingresso in modalità protetta, mentre non si è pensato ad un modo per tornare in modalità reale; tale possibilità è stata ritenuta superflua in quanto, in origine, questa nuova CPU era destinata principalmente all'ambito professionale dei sistemi multi-utente, multitasking, real-time, comunicazioni, etc.
La situazione però ha subito una svolta imprevista quando la IBM ha deciso di impiegare la 80286 per i suoi nuovi PC di classe AT; a quel punto, da più parti è arrivata la richiesta di studiare un metodo rapido ed efficiente per semplificare la scrittura di SO destinati alla modalità protetta, ma capaci di eseguire anche i programmi per la modalità reale, che continuavano ad invadere il mondo del software.
La soluzione escogitata si appoggia ancora una volta sull'8042 ed è divenuta famosa per la sua struttura a dir poco contorta; nel seguito viene illustrata in dettaglio anche perché rappresenta un pezzo di storia del mondo dei PC.

Come si vede in Figura 3.4, il pin P20 della Output Port del chip 8042 risulta associato ad un System Reset; attraverso tale pin è possibile anche inviare un impulso all'ingresso RESET della CPU.
Nel Capitolo 13 del tutorial Assembly Avanzato, si può notare che i comandi da F0h a FFh per l'8042 prendono il nome di Pulse Output Port, in quanto permettono di inviare un impulso per circa 6 microsecondi da specifici pin nelle posizioni da 0 a 3 della Output Port di Figura 3.4; i pin da cui inviare gli impulsi devono essere specificati ponendo a 0 i corrispondenti bit nelle posizioni da 0 a 3 del comando stesso. In particolare, il comando FEh (11111110b) indica che vogliamo inviare un impulso che dal pin P20 (in posizione 0) della porta di output del controller, giunge all'ingresso RESET della CPU; la conseguenza è che la 80286 si resetta come spiegato in 3.1.4 e ritorna in modalità reale.
Questa non è la soluzione ideale in quanto, dopo il reset, l'esecuzione riprende da CS:IP=F000h:FFF0h e cioè, dall'indirizzo fisco a 20 bit FFFF0h (ultimo paragrafo dell'unico MiB indirizzabile in modalità reale); come sappiamo, a tale indirizzo è presente un JMP FAR verso il codice di inizializzazione del sistema. A noi interessa invece una soluzione più sofisticata, che renda possibile scrivere un programma (o un intero SO), capace di saltare dalla modalità protetta a quella reale e viceversa, senza dover ogni volta riavviare totalmente il computer; tale soluzione esiste e si basa sull'uso della BIOS Data Area (BDA) e della CMOS.
Nel Capitolo 2 del tutorial Assembly Avanzato è stato spiegato che la BDA è un'area di memoria da 256 byte, posizionata a partire dal paragrafo 0040h. All'offset 0067h della BDA è presente un campo da 16+16 bit denominato CS:IP for 80286 return from protected mode; in tale campo bisogna salvare l'indirizzo logico Seg:Offset da cui si vuole far ripartire un programma dopo il reset della CPU. Se abbiamo definito un'etichetta real_mode come destinazione per il ritorno in modalità reale, possiamo scrivere allora il seguente codice: Come si può notare, nel rispetto delle convenzioni che già conosciamo, la componente Offset deve precedere Seg.

A questo punto bisogna dire al BIOS che, dopo il reset, in CS:IP devono essere caricati i valori che abbiamo salvato nella BDA; per fare ciò dobbiamo servirci della CMOS. Nel Capitolo 4 del tutorial Assembly Avanzato possiamo vedere che all'offset 0Fh della CMOS è presente un campo da 1 byte denominato IBM - Reset Code; in tale campo possiamo inserire un codice che indica il tipo di reset da effettuare. Nel nostro caso, dobbiamo inserire il codice 05h; in questo modo, il BIOS sa che dopo il reset deve caricare in CS:IP la coppia di valori salvata nella BDA all'indirizzo 0040h:0067h.
Possiamo scrivere quindi: Durante la fase di reset, il BIOS provvede a cancellare il codice che abbiamo inserito all'offset 0Fh della CMOS; ciò è necessario per evitare che ad ogni riavvio del computer si ripeta il procedimento appena descritto.

In sostanza, stiamo effettuando un reset "a caldo", che non comporta la cancellazione del contenuto della RAM; il nostro programma resta quindi in memoria e la sua esecuzione può riprendere dall'indirizzo da noi specificato.

La tecnica appena illustrata funziona perfettamente, ma non può essere certo definita una soluzione efficiente; su un PC basato sulla 80286, un simile procedimento può richiedere parecchi secondi. Si può dire anzi che, insieme alla segmentazione a 64 KiB, questo modo contorto di saltare dalla modalità protetta a quella reale e viceversa, è la causa principale che ha decretato l'insuccesso della 80286 nel mondo degli sviluppatori di SO.

3.2.6 Salvataggio dello stack per la modalità reale

Prima di passare in modalità protetta, è consigliabile salvare la coppia SS:SP correntemente in uso in modalità reale; a tale proposito, basta servirsi di due variabili di tipo WORD in cui memorizzare SS e SP.
In teoria, questo passaggio non sarebbe necessario, purché però non vengano commessi errori nella gestione dello stack; in ogni caso, si tratta di poche istruzioni che rappresentano una garanzia in più.

3.2.7 Predisposizione della Global Descriptor Table

Qualunque programma destinato ad accedere alla modalità protetta, è tenuto a predisporre almeno la GDT; come è stato spiegato nei precedenti capitoli, la IDT e le LDT sono invece facoltative. Appare chiaro però che, senza la IDT non possiamo gestire interruzioni ed eccezioni; analogamente, senza le LDT non possiamo beneficiare dei meccanismi di protezione tra i vari task e, soprattutto, non è garantito l'isolamento tra task e SO.

Predisporre la GDT è semplicissimo. Definiamo un blocco dati destinato a contenere i vari descrittori, di segmento e speciali; ogni descrittore, come sappiamo, ha un'ampiezza di 8 byte. Il descrittore di indice 0 (simbolicamente, GDT[0]), deve essere il NULL descriptor.
Scriviamo un'apposita procedura destinata ad inserire i vari descrittori nella GDT; i parametri richiesti per i segmenti di programma sono: sd_size (dimensione segmento), sd_seg16 (paragrafo da cui parte il segmento), sd_accbyte (ACCESS BYTE del segmento) e sd_index (indice del descrittore nella GDT).
Il blocco dati in cui inserire la GDT è puntato da DS:BX; la posizione in cui inserire un descrittore nella GDT si ottiene moltiplicando sd_index per 8. Possiamo scrivere allora il seguente codice: Osserviamo che sd_seg16*16 ci dà i primi 16 bit (da 0 a 15) del Base Address. Nella moltiplicazione, il nibble alto di sd_seg16*16 sconfina da sinistra; isolando tale nibble in AH otteniamo i restanti 8 bit (da 16 a 23) del Base Address. Nel caso, ad esempio, di AX=B800h, l'istruzione
shl ax, 4
ci dà AX=8000h (primi 16 bit del Base Address). Ponendo nuovamente AX=B800h (e quindi AH=B8h), l'istruzione
shr ah, 4
ci dà AH=0Bh (8 bit più significativi del Base Address).
Alla fine otteniamo Base Address = 0B8000h.

Le considerazioni appena esposte si riferiscono ai segmenti di programma definiti nel primo MiB di memoria; in tal caso, è necessario procedere nel modo descritto per determinare il Base Address a 24 bit. Per i segmenti di programma definiti nella memoria oltre il primo MiB, abbiamo già a disposizione il Base Address a 24 bit, per cui i calcoli precedenti non sono necessari; per inserire questo tipo di segmenti nella GDT, utilizziamo quindi una apposita procedura.

Gli ACCESS BYTE per i segmenti di programma di tipo CODE, DATA e STACK, come è stato già esposto in precedenza, hanno la seguente struttura: Per comodità, riserviamo GDT[1] al descrittore della stessa GDT; in questo modo, possiamo esplorare il contenuto della tabella.
GDT[2] viene riservato invece al segmento B800h in cui risulta mappata la memoria video in modo testo; tale segmento viene usato da tutte le procedure che mostrano stringhe e numeri sullo schermo, per cui il relativo selettore deve essere disponibile globalmente.

In base a come abbiamo riempito la GDT, definiamo anche i selettori associati ai vari segmenti; abbiamo, ad esempio: e così via.

Una volta predisposta la GDT, possiamo caricare il relativo descrittore nel GDTR, con l'istruzione LGDT; come sappiamo, tale istruzione è disponibile anche in modalità reale in quanto il suo scopo è preparare l'ambiente operativo per l'ingresso in modalità protetta.

3.2.8 Ingresso in modalità protetta

Una volta effettuate tutte le necessarie inizializzazioni, possiamo finalmente abilitare il supporto per la modalità protetta della 80286.
Innanzi tutto, poniamo a 1 il bit PM nella MSW attraverso l'istruzione LMSW; anche LMSW è disponibile in modalità reale per le stesse ragioni già illustrate per LGDT. Possiamo scrivere, ad esempio: A questo punto entriamo in modalità protetta attraverso un salto FAR intrasegmento, verso un indirizzo virtuale Selector:Offset; la componente Offset punta all'etichetta da cui parte il nostro codice, mentre la componente Selector è il selettore del segmento di programma contenente il codice stesso.
Per evitare che l'assembler effettui ottimizzazioni indesiderate sull'istruzione JMP FAR, inseriamo direttamente il relativo opcode EAh; se abbiamo definito un'etichetta protected_mode all'interno di un segmento di codice CODESEGM con selettore SEL_CODESEGM, possiamo scrivere quindi: (si può anche ricorrere ad un salto FAR indiretto, con opcode FFh e indirizzo di tipo m16:m16)

Il salto deve essere FAR in quanto dobbiamo aggiornare, non solo il registro IP, ma anche CS; le istruzioni precedenti fanno in modo che il vecchio contenuto CODESEGM di CS venga sostituito con il selettore dello stesso CODESEGM.
L'altro aspetto di fondamentale importanza è che, in presenza di una qualsiasi istruzione di salto, la CPU svuota la prefetch queue e la riempie con le nuove istruzioni presenti a partire dalla destinazione del salto stesso; nel nostro caso, le vecchie istruzioni per la modalità reale vengono eliminate e sostituite con quelle per la modalità protetta, presenti a partire dall'etichetta protected_mode.

Appena entrati in modalità protetta, dobbiamo svolgere un compito importantissimo che consiste nell'inizializzare DS, ES e SS con dei selettori validi; possiamo scrivere, ad esempio: In modalità reale non esistono meccanismi di protezione, per cui nei registri di segmento possiamo caricare anche dei valori casuali; in modalità protetta ciò causerebbe una eccezione di segmento non valido!
Consideriamo, ad esempio, una procedura che usa ES dopo averne preservato il contenuto con PUSH; al termine di tale procedura, lo stesso ES viene ripristinato con l'istruzione POP. Se ES non era stato inizializzato con un selettore valido prima della chiamata della procedura, PUSH inserisce nello stack un valore casuale; di conseguenza, POP estrae tale valore e lo carica in ES provocando, appunto, una eccezione di segmento non valido.

Si noti che, se non si hanno esigenze particolari, non è necessario inizializzare anche SP; tale lavoro viene effettuato infatti dal DOS grazie alla presenza del segmento STACKSEGM con attributo di combinazione STACK.
Se non ci sono stati errori nella gestione dello stack in modalità reale, quando entriamo in modalità protetta abbiamo SP=STACK_SIZE; inoltre, il segmento STACKSEGM è stato definito come Expand Down, per cui SP viene decrementato ad ogni PUSH.

Un ulteriore compito da svolgere appena entrati in modalità protetta consiste nell'impostare i campi NT e IOPL del registro FLAGS; nel nostro caso, non sono previsti task switch e tutto il codice gira a CPL=0, per cui dobbiamo porre NT=0 e IOPL=0. Possiamo scrivere quindi: Questa operazione deve essere eseguita dal kernel in quanto, come sappiamo, solo un task che gira a CPL=0 può modificare IOPL.

3.2.9 Ritorno in modalità reale

Al termine del nostro programma, dobbiamo tornare in modalità reale in modo da restituire correttamente il controllo al DOS. Avendo già predisposto tutto il necessario, come spiegato in precedenza non dobbiamo fare altro che inviare il comando FEh alla porta di input 64h dell'8042. Possiamo scrivere quindi: Con i moderni emulatori e macchine virtuali, queste istruzioni vengono eseguite in modo rapidissimo; con un PC basato su una vera 80286, il reset della CPU avviene in modo estremamente lento, per cui può essere necessario aggiungere il seguente loop infinito: L'istruzione HLT, come sappiamo, costringe la CPU a sospendere l'esecuzione delle istruzioni; il loop è necessario in quanto gli effetti di HLT possono essere annullati dall'arrivo di una interrupt (in tal caso, la CPU verrebbe riattivata).

Il comando FEh resetta quindi la 80286 e l'esecuzione del nostro programma riprende dall'indirizzo da noi impostato in precedenza; a questo punto, la 80286 è di nuovo in modalità reale.
Ciò che dobbiamo fare ora consiste nel ripetere in ordine inverso le operazioni illustrate in precedenza.

Carichiamo DATASEGM in DS, ripristiniamo SS:SP e lo stato dei PIC, Master e Slave.
Disabilitiamo la linea A20 inviando all'8042 il comando DDh; ripristiniamo la NMI con le istruzioni: Infine, riattiviamo la gestione delle interruzioni mascherabili con l'istruzione STI.

3.2.10 Configurazione degli emulatori DOS e delle macchine virtuali

Gli esempi presentati in questo capitolo sono stati testati con successo con l'emulatore DOSBox-X e con MS-DOS 6.22 installato nella macchina virtuale 86Box; l'emulatore DOSBox invece non supporta la CPU 80286, per cui si blocca durante la fase di reset descritta in precedenza. Prima di procedere, dobbiamo quindi configurare in modo opportuno l'ambiente operativo che intendiamo usare.
Bisogna ricordare che quando il nostro programma passa in modalità protetta, può svolgere una serie di operazioni totalmente al di fuori del controllo del DOS, dei vari DOS extender, gestori della memoria espansa, etc; è importantissimo quindi disattivare tutto ciò che ha a che vedere con XMS, EMS e HMA.

Nel caso di MS-DOS installato in 86Box, dall'interno della macchina virtuale dobbiamo editare (con EDIT) il file C:\CONFIG.SYS e commentare (con REM) le seguenti righe: 86Box è capace di emulare in modo estremamente accurato l'hardware dei vecchi PC basati sulle CPU dalla 8088 ai primi modelli di Pentium; in ogni caso, non è necessario impostare una macchina virtuale 80286, anche perché ciò equivale a fare un viaggio indietro nel tempo sino al 1982, con tutte le complicazioni che ne derivano in termini di hardware e SO dell'epoca.
Per gli esempi presentati in questo capitolo è stata scelta la seguente configurazione hardware:

DOSBox-X è un emulatore DOS, per cui tutte le impostazioni hardware e software sono disponibili nel relativo file di configurazione dosbox-x.conf; dopo aver editato tale file, dobbiamo impostare su false il supporto XMS, EMS e HIMEM.
Nella sezione [cpu], per abilitare il supporto al reset della CPU tramite 8042 dobbiamo impostare:
core = normal
Per evitare che l'emulatore richieda LIMIT=FFFFh per tutti i segmenti, dobbiamo impostare:
segment limits = false
Come modello di CPU possiamo scegliere:
cputype = 486
Nella sezione [keyboard] è importantissimo impostare:

3.2.11 Programma di esempio con GDT

Mettendo assieme tutti i concetti appena esposti, possiamo scrivere un semplice programma che ha unicamente lo scopo di mostrare come entrare in modalità protetta e come tornare in modalità reale; a tale proposito, come è stato già anticipato, utilizziamo solo la GDT e facciamo girare tutto il codice a CPL=0. Tutti i segmenti di codice, dati e stack hanno quindi DPL=0; analogamente, tutti i selettori specificano RPL=0.

Uno dei (pochi) pregi della segmentazione a 64 KiB della modalità protetta 80286 è che viene favorito uno stile di programmazione modulare; per sfruttare questo aspetto, suddividiamo il nostro programma in un modulo principale che rappresenta il kernel, un modulo che contiene tutte le procedure che operano sull'hardware e un ulteriore modulo riservato alla GDT.

Facciamo anche ricorso ad apposite macro per tutte quelle procedure che richiedono parametri; in questo modo possiamo scrivere codice più semplice e chiaro, simile a quello dei linguaggi di alto livello.
Il riempimento della GDT, ad esempio, assume un aspetto del genere: e così via.

Si noti che STACKSEGM è un segmento di tipo Expand Down; di conseguenza, per il campo LIMIT dobbiamo specificare l'offset minimo indirizzabile.

Per ulteriori dettagli, si può fare riferimento ai commenti presenti nel codice sorgente.

La Figura 3.5 mostra il codice sorgente del modulo principale KRNL16A.ASM, contenente il kernel del nostro esempio.

Selezione codice sorgente: La Figura 3.6 mostra il codice sorgente del modulo HRDW16A.ASM, che contiene le procedure per l'accesso all'hardware. La Figura 3.7 mostra il codice sorgente del modulo GDT16A.ASM, che contiene la GDT e le procedure per la sua gestione. I moduli HRDW16A.ASM e GDT16A.ASM sono disponibili solo in versione NASM; gli object file che si ottengono sono perfettamente linkabili al modulo KRNL16A.ASM per MASM. Se lo si ritiene opportuno, possono essere facilmente convertiti in versione MASM, grazie alla elevata compatibilità tra i due assembler; sono richiesti MASM 6.11 e NASM 2.15.5 o superiore.

Codice sorgente per NASM: KRNL16A NASM

Codice sorgente per MASM: KRNL16A MASM

La Figura 3.8 mostra l'output del programma su DOSBox-X. Il programma visualizza una serie di informazioni, compreso l'indirizzo di ingresso in modalità protetta; in Figura 3.8 si può notare che il registro CS contiene il valore 0018h, che è proprio il selettore del segmento CODESEGM (INDEX=0000000000011b, TI=0b, RPL=00b, in esadecimale danno 0018h).

In seguito, grazie al selettore SEL_GDTDATA, vengono visualizzati alcuni descrittori di segmento presenti nella GDT.

Sfruttando poi il fatto che siamo in modalità protetta e la linea A20 è attiva, una stringa viene letta da DATASEGM e copiata con REP MOVSB in un segmento dati HMEMSEGM che si trova oltre il primo MiB, all'indirizzo a 24 bit 1F0000h (2031616); successivamente, tale stringa viene visualizzata sullo schermo (e quindi copiata da HMEMSEGM a 0B8000h).
Osserviamo come, con la modalità protetta attiva (PM=1 nel MSW), tutte le istruzioni si comportino di conseguenza; REP MOVSB usa sempre DS:SI come sorgente e ES:DI come destinazione, ma stavolta DS e ES contengono i selettori di due segmenti di dati che possono trovarsi in un'area qualunque dei 16 MiB indirizzabili dalla 80286!

La chiamata delle varie procedure avviene in modo formalmente identico alla modalità reale; non è necessario l'uso dei Call Gate in quanto, anche saltando da un segmento all'altro, non si verifica alcun cambio di privilegio.

Possiamo anche notare che la programmazione in modalità protetta è del tutto simile a quella in modalità reale; anzi, per certi versi è anche più semplice. Le procedure print_string, print_hexnum e print_binnum possono essere usate in entrambe le modalità operative; in modalità reale inviano l'output sul video, al segmento B800h, mentre in modalità protetta viene usato il selettore del segmento stesso, con Base Address a 24 bit 0B8000h. Eseguendo DOSBox-X da un terminale, abbiamo la possibilità di leggere l'output diagnostico generato dall'emulatore; in particolare, notiamo le seguenti due righe: Effettivamente, come si vede in Figura 3.8, l'indirizzo di ritorno in modalità reale (da noi memorizzato nella BDA), è proprio 0850h:0396h!

3.2.12 Programma di esempio con GDT e IDT

L'esempio appena presentato ha richiesto la scrittura di una quantità notevole di codice; bisogna considerare però che la quasi totalità del lavoro svolto si riferisce a impostazioni e reimpostazioni dell'hardware, comuni a tutti i programmi che operano in modalità protetta. Vediamo infatti ora un nuovo esempio denominato KRNL16B, che riutilizza il 100% del codice scritto per KRNL16A.

KRNL16B introduce una funzionalità fondamentale, che consiste nella gestione delle interruzioni; in questo modo abbiamo la possibilità di vedere ciò che accade in presenza di violazioni (volontarie o involontarie) dei meccanismi di protezione della 80286.
In base a quanto è stato esposto nel precedente capitolo, i primi 32 vettori di interruzione (da 00h a 1Fh) sono riservati alla 80286 per la gestione delle interruzioni software (comprese quindi le eccezioni); per questo motivo, la IDT deve avere una dimensione minima pari a 32*8 byte. Il nostro obiettivo è quello di gestire anche le richieste di interruzione hardware (IRQ) generate dai due PIC; si tratta quindi di aggiungere alla IDT ulteriori 8+8=16 vettori di interruzione, per un totale di 48.
Osservando la Figura 3.10 del Capitolo 3, tutorial Assembly Avanzato, possiamo notare che si presenta un problema, dovuto al fatto che le 8 IRQ gestite dal PIC Master, vengono associate ai vettori che vanno da 08h a 0Fh; si verifica quindi una sovrapposizione con i corrispondenti vettori riservati alla 80286. Ciò che dobbiamo fare consiste quindi nel riprogrammare il PIC Master per eliminare tale sovrapposizione. Per evitare di lasciare buchi nella IDT, associamo le IRQ del PIC Master ai vettori che vanno da 32 a 39 (da 20h a 27h); per lo stesso motivo, riprogrammiamo anche il PIC Slave associando le sue IRQ ai vettori che vanno da 40 a 47 (da 28h a 2Fh).
Il codice da scrivere è lo stesso presentato nel già citato Capitolo 3 e consiste nell'inviare in sequenza i 4 comandi di inizializzazione ICW; a tale proposito, si consiglia di rileggere la parte relativa alla programmazione dei PIC.
Definiamo innanzi tutto le costanti relative alle porte dei PIC: A questo punto, indicando con MPIC_BASE_TYPE il Base Type 20h del PIC Master e con SPIC_BASE_TYPE quello (28h) del PIC Slave, possiamo scrivere: Ci serve anche il codice necessario per abilitare solamente le IRQ che intendiamo gestire. Nel nostro caso, vogliamo intercettare i codici di scansione generati dalla tastiera in seguito alla pressione dei tasti, per cui dobbiamo abilitare la sola IRQ1 (che abbiamo associato al vettore n. 21h); come sappiamo, tale IRQ è connessa al chip 8042 Keyboard Controller.
Ciò che dobbiamo fare consiste nell'inviare un comando OCW1 per modificare i bit desiderati nell'IMR del PIC; abbiamo visto che, se un bit vale 0, la corrispondente IRQ è abilitata (unmasked), mentre se vale 1 è disabilitata (masked). Ponendo allora in AL la bitmask per il PIC Master e in AH quella per il PIC Slave, possiamo scrivere: Inserendo questo codice in una procedura chiamata PIC_mask, se vogliamo abilitare la sola IRQ1 ci possiamo servire delle seguenti istruzioni: Chiaramente, bisogna ricordare che prima di effettuare queste impostazioni, lo stato dell'IMR dei PIC deve essere salvato, come abbiamo fatto nell'esempio KRNL16A.

Come al solito, una volta tornati in modalità reale, dobbiamo ripetere in ordine inverso tutte le impostazioni precedentemente effettuate per l'ingresso in modalità protetta; in particolare, è di vitale importanza riprogrammare i PIC in modo da ripristinare i Base Type, che sono 08h per il PIC Master e 70h per il PIC Slave.

Per la gestione della IDT ci serviamo di un apposito modulo denominato IDT16B.ASM; il lavoro da svolgere è del tutto simile a quello già visto per la GDT.
Dobbiamo ricordare che nella IDT possiamo inserire solo descrittori di Interrupt Gate, Trap Gate o Task Gate; per i relativi ACCESS BYTE ci serviamo delle seguenti costanti: Come si può notare, abbiamo DPL=0 in tutti i descrittori, per cui anche le relative ISR gireranno a CPL=0.
Per semplicità, ci serviamo solo di Interrupt Gate, anche se, in certi casi, tale scelta non è corretta; nel precedente capitolo infatti abbiamo visto che determinate eccezioni devono essere gestite tramite Task Gate.

Come sappiamo, un descrittore di Interrupt Gate deve contenere, oltre all'ACCESS BYTE, l'offset della ISR da chiamare e il selettore che punta al descrittore del segmento di codice contenente la ISR stessa; tale descrittore viene inserito nella GDT. Indicando con ics_sel il selettore e facendo puntare DS:BX alla IDT, per una generica ISR denominata isr_n possiamo scrivere: Per evitare di ripetere 32 volte questo codice, nell'esempio che segue vengono definiti 32 puntatori alle ISR e si ricorre ad un loop che effettua 32 iterazioni.

Per i 16 vettori associati alle IRQ viene usata la stessa tecnica; è importante ricordare che, in questo caso, le relative ISR devono inviare sempre l'EOI ai PIC di competenza.

Le ISR associate ai 32 vettori riservati alla 80286 si limitano a mostrare un messaggio di errore; solo alcune mostrano anche l'indirizzo di ritorno e l'eventuale codice di errore.
Lo stesso discorso vale per le ISR associate ai 16 vettori dei PIC; a noi interessa intercettare solo la IRQ1 proveniente dalla tastiera, per cui la relativa ISR mostra gli scancode generati dalla pressione o dal rilascio dei tasti.

Una volta riempita la IDT, carichiamo nell'IDTR il relativo descrittore tramite l'istruzione LIDT; a tale proposito, utilizziamo una procedura che richiede un parametro il cui valore può essere 0 o 1. Il valore 0 indica che ci stiamo riferendo alla IDT per la modalità protetta; il valore 1 invece ripristina la IVT per la modalità reale (come indicato in 3.1.4).

Per ulteriori dettagli, si può fare riferimento ai commenti presenti nel codice sorgente.

La Figura 3.9 mostra il codice sorgente del modulo principale KRNL16B.ASM, contenente il kernel del nostro esempio.

Selezione codice sorgente: La Figura 3.10 mostra il codice sorgente del modulo HRDW16B.ASM, che contiene le procedure per l'accesso all'hardware. La Figura 3.11 mostra il codice sorgente del modulo IDT16B.ASM, che contiene la IDT e le procedure per la sua gestione. La Figura 3.12 mostra il codice sorgente del modulo GDT16B.ASM, che contiene la GDT e le procedure per la sua gestione. I moduli HRDW16B.ASM, IDT16B.ASM e GDT16B.ASM sono disponibili solo in versione NASM; gli object file che si ottengono sono perfettamente linkabili al modulo KRNL16B.ASM per MASM. Come al solito, questi moduli possono essere facilmente convertiti in versione MASM.

Codice sorgente per NASM: KRNL16B NASM

Codice sorgente per MASM: KRNL16B MASM

La Figura 3.13 mostra l'output del programma su MS-DOS 6.22 installato nella macchina virtuale 86Box. Il programma KRNL16B, una volta entrato in modalità protetta, permette di generare tre tipi di eccezione; la prima è una "division by zero" che si può ottenere con il codice: La CPU chiama il vettore 00h e inserisce nello stack return CS:IP che punta all'istruzione (DIV BX) responsabile del problema; è compito della ISR gestire questa situazione per evitare un loop infinito.
Nel nostro caso, l'opcode di DIV BX occupa 2 byte, per cui nella ISR incrementiamo return IP di 2; si può notare che dopo PUSH BP, troviamo return IP a BP+2.
Si tenga presente che il codice appena illustrato è un semplice esempio e funziona solo se l'eccezione è stata provocata da una istruzione con opcode da 2 byte; in caso contrario, il programma si blocca. In una situazione reale, in genere il SO chiude il task che ha provocato il problema.

Un altro caso è la "interrupt on overflow" che possiamo ottenere con il codice: La CPU chiama il vettore 04h e inserisce nello stack return CS:IP che punta all'istruzione successiva a INTO; si tratta quindi di una situazione che non presenta problemi in quanto generata da una eccezione non critica.

La Figura 3.13 mostra infine il caso di una "general protection exception" che possiamo ottenere, ad esempio, caricando in ES un selettore non valido, come 1234h; la CPU chiama il vettore 0Dh e inserisce nello stack return CS:IP che punta alla stessa istruzione che ha provocato il problema e anche un codice di errore (nel nostro caso, si tratta proprio del selettore 1234h).
La ISR deve provvedere ad estrarre il codice di errore dallo stack e a gestire correttamente il return CS:IP; anche in questo caso, il nostro esempio funziona solo se l'istruzione che ha creato il problema ha un opcode da 2 byte. Prima di lasciare la modalità protetta, il programma KRNL16B avvia un loop dal quale si esce premendo il tasto [ESC]; come si può notare, la CPU associa correttamente la IRQ1 al vettore 21h come da noi stabilito attraverso la riprogrammazione dei PIC.

Una volta tornati in modalità reale, dobbiamo svolgere un compito estremamente importante che consiste nel ripristinare i Base Type predefiniti 08h e 70h dei due PIC, così come lo stato (salvato in precedenza) dei rispettivi IMR; infine, nell'IDTR dobbiamo caricare il descrittore della IVT, con Base Address 000000h e LIMIT 1024-1.
Per verificare la correttezza di queste reimpostazioni, vediamo in Figura 3.13 che viene chiesto di premere un tasto per terminare il programma; questa volta, la CPU associa la IRQ1 alla ISR predefinita (vettore 09h) installata dal BIOS (o dal DOS) per la modalità reale.

3.2.13 Programma di esempio con GDT, LDT, IDT e Task Switch

Vediamo un ultimo esempio KRNL16C il quale, ancora una volta, riutilizza il 100% del codice scritto in precedenza. Formalmente, KRNL16C appare del tutto simile a KRNL16B; la differenza sostanziale sta nel fatto che stavolta, il kernel cede il controllo ad un task che gira a CPL=3!

Il kernel rappresenta il task principale, denominato TASK0, mentre in un nuovo modulo TSK116C viene inserito il codice di un secondo task indicato con TASK1. TASK0 usa solo la GDT, per cui ha bisogno di un TSS i cui campi Back Link e LDT Selector puntano al NULL Selector; TASK1 invece ha bisogno di un TSS e di una LDT, la quale specifica i descrittori dei segmenti privati di codice dati e stack usati (lo spazio di indirizzamento virtuale privato del task stesso).
TASK1 non è innestato in TASK0, per cui il campo Back Link del suo TSS punta al NULL Selector; il campo LDT Selector invece punta alla LDT.
TASK1 gira a CPL=3, per cui deve disporre di un apposito stack con DPL=3; inoltre, dobbiamo anche definire uno stack con DPL=0 per l'accesso ai servizi forniti dal kernel (che girano in questo caso a CPL=0).

Il riempimento della LDT è del tutto simile a quanto già visto per la GDT e la IDT; nel nostro caso, dobbiamo inserire i descrittori per i segmenti di codice, dati, stack con DPL=0 e stack con DPL=3. Si tratta di normali segmenti di programma, i cui descrittori hanno quindi una struttura che già conosciamo; se DS:BX punta alla LDT, per il segmento privato di dati (TSK1DATA), ad esempio, possiamo scrivere: Osserviamo che in questo caso, gli ACCESS BYTE per codice, dati e stack a CPL=3 specificano DPL=3, mentre per lo stack a CPL=0 si ha DPL=0. Analogamente, per il campo RPL nei selettori degli stessi segmenti si ha: In relazione ai TSS la situazione è più delicata; in questo caso infatti dobbiamo riempire tutti i campi essenziali per un corretto Task Switch.
Per TASK0 dobbiamo riempire i campi Back Link, LDT Selector e SS:SP per CPL=0; gli altri campi (stato corrente di TASK0) vengono riempiti in automatico dalla 80286 quando viene ceduto il controllo a TASK1.
Per TASK1 invece è essenziale riempire i campi Back Link, LDT Selector, SS:SP per CPL=0 e tutti i registri che puntano ai segmenti di programma privati con DPL=3, come SS:SP, CS:IP (entry point del task), DS e ES. Ovviamente, nei registri di segmento vanno inseriti i relativi selettori.
Sempre in relazione al TSS di TASK1, è importante inizializzare anche il campo FLAGS; nel nostro caso, dobbiamo porre IF=1 in modo da abilitare le interruzioni mascherabili.
Se DS:BX punta al TSS, ldt_sel è il selettore della LDT e l'entry point del task è rappresentato dall'etichetta task1_start, abbiamo quindi (Figura 2.13 del Capitolo 2): Anche in questo caso, quando viene restituito il controllo a TASK0, la CPU provvede a salvare lo stato corrente di TASK1 negli appositi campi del TSS associato.

Bisogna ricordare che i TSS e le LDT devono essere gestite dal kernel; di conseguenza, i relativi descrittori vanno inseriti obbligatoriamente nella GDT.
Trattandosi di normali segmenti di dati, i descrittori vengono inseriti nella GDT con lo stesso procedimento usato per i generici segmenti di programma; è necessario solo ricordare che gli ACCESS BYTE devono specificare S=0, mentre il campo TYPE deve valere 0010b per le LDT e 0001b per i TSS.

L'esempio KRNL16C illustra anche l'utilizzo di un Call Gate, tramite il quale TASK1 può chiamare un servizio fornito dal kernel; in questo caso si tratta di una procedura kprint_string che visualizza una stringa sullo schermo.
Il descrittore di Call Gate viene inserito nella GDT (perché è un servizio fornito dal kernel) tramite un'apposita procedura; in questo caso, l'ACCESS BYTE deve specificare S=0 e TYPE=0100b.
Siccome kprint_string richiede 5 parametri di tipo WORD, nel campo WORD COUNT del descrittore del Call Gate dobbiamo inserire il valore 5.

L'elenco dei vari servizi forniti dal kernel si trova in un include file chiamato KRNL16C.INC, che deve essere incluso in TSK116C.ASM.

Per ulteriori dettagli, si può fare riferimento ai commenti presenti nel codice sorgente.

La Figura 3.14 mostra il codice sorgente del modulo principale KRNL16C.ASM, contenente il kernel (TASK0) del nostro esempio.

Selezione codice sorgente: La Figura 3.15 mostra il codice sorgente del modulo TSK116C.ASM, contenente il TASK1 del nostro esempio, il TSS e la LDT.

Selezione codice sorgente: La Figura 3.16 mostra il codice sorgente del modulo HRDW16C.ASM, che contiene le procedure per l'accesso all'hardware. La Figura 3.17 mostra il codice sorgente del modulo IDT16C.ASM, che contiene la IDT e le procedure per la sua gestione. La Figura 3.18 mostra il codice sorgente del modulo GDT16C.ASM, che contiene la GDT e le procedure per la sua gestione. I moduli HRDW16C.ASM, IDT16C.ASM e GDT16C.ASM sono disponibili solo in versione NASM; gli object file che si ottengono sono perfettamente linkabili al modulo KRNL16C.ASM per MASM. Questi moduli possono essere facilmente convertiti in versione MASM.

Codice sorgente per NASM: KRNL16C NASM

Codice sorgente per MASM: KRNL16C MASM La Figura 3.19 mostra l'output del programma su MS-DOS 6.22 installato nella macchina virtuale 86Box. KRNL16C, dopo le solite impostazioni dell'hardware, procede con il riempimento di LDT, TSS, IDT e GDT; come si può notare, nella GDT vengono inseriti anche i descrittori di TSS0, TSS1, LDT1 e Call Gate per la procedura kprint_string.
Dopo l'ingresso in modalità protetta, si passa al caricamento dei registri LDTR e TR con le informazioni relative al primo task da eseguire; nel nostro caso si tratta ovviamente di TASK0.
TASK0 non usa la LDT per cui nel LDTR dobbiamo caricare il NULL Selector; in TR dobbiamo invece mettere il selettore del TSS0.

A questo punto TASK0 effettua un salto diretto a TASK1; la CPU provvede a salvare in TSS0 lo stato dello stesso TASK0 e ad aggiornare in automatico LDTR e TR. Come sappiamo, la destinazione del salto è il selettore (SEL_TSS1SEGM) che punta a TSS1, mentre la componente Offset viene ignorata; con NASM si potrebbe scrivere:
jmp SEL_TSS1SEGM:0000h
La sintassi varia però da un assembler all'altro.
Un metodo che funziona sempre è il ricorso al codice macchina della precedente istruzione; possiamo scrivere: Quando TASK1 riceve il controllo, svolge un compito del tutto simile a quello di KRNL16B; oltre a provocare delle eccezioni a scelta, mostra una stringa e poi attende che l'utente prema il tasto [ESC].
La parte interessante è la stampa della stringa, attraverso un Call Gate che permette di chiamare la procedura kprint_string; si tratta solo di un esempio che ha lo scopo di mostrare come i vari task possano richiedere dei servizi al kernel.
In questo caso abbiamo TASK1 che gira a CPL=3 e richiede un servizio a TASK0 che invece gira a CPL=0; come sappiamo, devono verificarsi allora le seguenti condizioni:
EPL = max(TASK1_CPL, GATE_SEL_RPL) ≤ GATE_DESC_DPL
e
TASK0_DPL ≤ TASK1_CPL
Quindi, il caller deve essere più privilegiato del gate, mentre il segmento di destinazione (dove si trova kprint_string) deve essere più privilegiato del caller.

Anche per la chiamata di kprint_string tramite selettore di Call Gate (SEL_KPRINTSTR), ci possono essere differenze sintattiche tra i vari assembler; con NASM possiamo scrivere:
   call     SEL_KPRINTSTR:0000h           ; call gate (offset ignorato)
Questa sintassi non viene accettata da MASM; ricorrendo allora al codice macchina, abbiamo: Dopo la pressione del tasto [ESC], TASK1 restituisce il controllo a TASK0 sempre tramite salto diretto a TSS0; come al solito, la CPU provvede a salvare in TSS1 lo stato di TASK1 e ad aggiornare LDTR e TR.


Con il programma KRNL16C si possono fare numerosi esperimenti per testare i meccanismi di protezione della 80286; a tale proposito, si consiglia di commentare prima il codice che genera le eccezioni nel file TSK116C.ASM.

Se nel descrittore del Call Gate mettiamo DPL=0 anziché DPL=3, la stringa non viene stampata e si ottiene questa eccezione: Il codice di errore in binario diventa 0000000001010000b; si ha quindi EXT=0 (nessun evento esterno), IDT=0 (la IDT non è coinvolta), TI=0 (il problema è nella GDT), INDEX=10 (indice nella GDT). Ma all'indice 10 della GDT troviamo proprio il descrittore del Call Gate!

Se creiamo TSS1 con una dimensione inferiore a 44 byte, il Task Switch fallisce e vengono mostrati questi messaggi: Il codice di errore in binario diventa 0000000001001000b; si ha quindi EXT=0 (nessun evento esterno), IDT=0 (la IDT non è coinvolta), TI=0 (il problema è nella GDT), INDEX=9 (indice nella GDT). Ma all'indice 9 della GDT troviamo proprio il descrittore di TSS1!
Come spiegato nel precedente capitolo, questo tipo di eccezione deve essere gestito tramite un Task Gate; la relativa ISR, oltre ad estrarre il codice di errore dallo stack, deve anche provvedere a reimpostare in modo corretto il TSS coinvolto.

Un altro esperimento consiste nell'omettere per TASK1 lo stack per CPL=0; in questo caso si può constatare che il Task Switch verso TASK1 fallisce!

Un ulteriore esperimento consiste nel tenere IF=0 nel campo FLAGS del TSS di TASK1; la conseguenza è che TASK1 si blocca in un loop infinito in quanto le interruzioni mascherabili sono disabilitate e quindi non può essere rilevata la pressione del tasto [ESC]!

Se nel modulo TSK116C.ASM proviamo ad inserire istruzioni che accedono alle porte hardware, otteniamo una General Protection Exception con codice di errore 0000h; ciò accade perché, come sappiamo, un task può usare tali istruzioni solo se ha CPL≤IOPL, ma in questo caso TASK1 ha CPL=3 e IOPL vale 0!

Bibliografia

Intel 80286 Hardware Reference Manual
(disponibile su Internet)

Intel 80286 Programmer's Reference Manual
(disponibile su Internet)

iAPX 286 Operating Systems Writer's Guide
(disponibile su Internet)