Intel 80286: fra protezione della memoria e… call gate!

Non poche volte m’è capitato di trovarmi di fronte a delle novità particolarmente difficili da digerire e che ho velocemente marchiato come roba “inutile”, di cui se ne può fare a meno, o addirittura frutto della mente deviata di qualcuno che non aveva niente di meglio da fare.

Tipica posizione questa di persone poco capaci, svogliate e/o che non vogliono staccarsi dalle immutabili “certezze” acquisite, magari difendendole a spada tratta a ogni occasione. Col tempo si matura la consapevolezza che le difficoltà vanno affrontate e risolte, con un po’ di impegno, buona volontà, ma soprattutto la giusta apertura mentale.

E’ il caso dell’80286 di Intel: microprocessore a 16 bit che ha introdotto, con la modalità protetta, una profonda rivoluzione nell’architettura x86, di cui però inizialmente non riuscivo a percepire le potenzialità e addirittura consideravo un’autentica assurdità, abituato com’ero al semplice e lineare modello offerto dalla Motorola col 68000 e compagnia bella…

In effetti le differenze rispetto a quest’ultima (e a tante altre architetture prive del concetto di segmentazione della memoria che, quindi, offrono un accesso diretto, o per meglio dire lineare a essa) sono notevoli, mentre confrontato all’8086 sono più di sostanza che di forma, ben “nascoste” nei famigerati segmenti, che con questa nuova CPU hanno cambiato nome in selettori, mantenendo invariato però l’utilizzo.

Più precisamente, l’80286 introduce una nuova modalità di funzionamento del microprocessore chiamata protetta, etichettando il normale funzionamento dei precedenti processori (8086 e 80186) come modalità reale. In modalità protetta la CPU cambia radicalmente il funzionamento dei segmenti (diventati, appunto, selettori) per l’accesso alla memoria (ho già parlato di segmentazione in precedenza, quando ho introdotto l’8086, per cui non mi soffermerò sull’argomento), che hanno assunto la seguente forma:

Il valore a 16 bit è stato suddiviso in tre gruppi di informazioni. Partendo da destra verso sinistra, abbiamo il livello di privilegio del selettore, a quale tabella esso fa riferimento, e l’indice nella tabella.

Il livello di privilegio riguarda la modalità richiesta per l’uso del selettore, fra le quattro messe a disposizione dall’80286. Già, perché, a differenza di tanti altri microprocessori che offrono un meccanismo a due livelli (modalità utente, usata per le applicazioni, e supervisore, usata per kernel, driver e librerie di sistema), Intel ha pensato bene di aggiungerne un altro paio per definire in maniera più granulare (quindi “fine”) i meccanismi di protezione per l’accesso a particolari risorse.

A prima vista sembra la solita, inutile, complicazione ma, riflettendoci bene, è così scocciante per un programmatore? No, perché non siamo tenuti a impostare a manina ogni singolo selettore per le nostre applicazioni, visto che è il compilatore utilizzato che si fa carico di tutto. Al più potremmo essere interessati se dovessimo lavorare al codice di sistema, ma anche qui, a meno di non lavorare con le primitive del kernel, scrivendo driver e librerie di sistema non siamo tenuti a occuparci di queste informazioni.

In buona sostanza, la flessibilità ottenuta da questo meccanismo non comporta conseguenze di cui tenere conto quando si lavora normalmente (tranne per gli sviluppatori del kernel). Ma non sono tutte rose e fiori, purtroppo. Per rappresentare i quattro livelli di privilegio sono necessari due bit, per cui si riduce il numero di selettori utilizzabili da un’applicazione, che ammontano a 8192 per i selettori locali e altrettanti per quelli globali (quindi 16384 in tutto), e ciò comporta un grosso limite al numero di risorse allocabili da un’applicazione e dal sistema.

Il secondo campo di un selettore indica a quale insieme appartiene: se locale all’applicazione (LDT, Local Descriptor Table) o globale al sistema (GDT, Global Descriptor Table). Nelle tabelle si fa riferimento alla parola “descrittore” perché ogni elemento riporta (“descrive”) le informazioni associate al selettore in questione:


Senza soffermarci troppo (perché la spiegazione richiederebbe non poco tempo e conoscenze tecniche), è sufficiente dire che viene specificato l’indirizzo base della memoria fisica a cui si riferisce il selettore (Base), la sua dimensione (Limit, in byte), il tipo di segmento (Type, per codice, stack e dati) e alcuni flag accessori che sono utili per lo più al kernel (ad esempio per implementare la memoria virtuale).

E’ importante, però, mettere in chiaro che l’esecuzione è fortemente condizionata dai valori presenti nei descrittori. Giusto per fare un esempio, il codice potrà essere eseguito esclusivamente su selettori nel cui tipo sia stato indicato, appunto, che si tratta di codice, pena l’interruzione dell’esecuzione e il sollevamento di un’eccezione. Da ciò si capisce anche il perché la modalità venga chiamata “protetta”: esistono dei granitici meccanismi di protezione delle risorse con cui si sta lavorando.

Il terzo e ultimo campo specifica l’indice del selettore (dalla tabella di quelli locali o globali, come detto qui sopra) che si vuole utilizzare. Quindi il codice, lo stack e i dati di un’applicazione saranno allocati in opportuni e ben precisi selettori, che verranno combinati con un offset per accedere alla locazione di memoria effettiva, come riportato nel seguente schema:


Lo schema è simile a quanto avveniva coi segmenti, dove l’indirizzo finale veniva generato da un’opportuna combinazione del segmento e dell’offset. Qui l’offset ha lo stesso significato, ma cambia quello del segmento, poiché il selettore da solo non è sufficiente a determinare immediatamente l’indirizzo base da sommare all’offset per ricavare l’indirizzo fisico desiderato.

Dal selettore è necessario individuare intanto la tabella dei descrittori da utilizzare, e sfruttando l’indice si ottiene finalmente accesso alle informazioni del descrittore. Da qui si controlla innanzitutto se il livello di privilegio presente è “accettabile” (generando un’eccezione in caso contrario), si confronta l’offset col limite per vedere se lo si sta superando (generando anche qui un’eccezione, nel caso), e finalmente se tutti i controlli sono stati soddisfatti si preleva l’indirizzo base sommandolo all’offset per generare l’indirizzo fisico per accedere alla memoria.

Un meccanismo abbastanza complicato e che ai tempi definivo, appunto, contorto, ma che ha ben ragione di esistere e devo ammettere che sono rimasto molto affascinato dal sistema di protezione che viene messo in piedi utilizzandolo. In ogni caso si tratta di operazioni che vengono effettuate internamente dalla CPU per cui, per quanto “astruso”, i programmatori possono benissimo ignorarlo.

Ma il sistema di protezione ideato da Intel non finisce qui. I selettori, in particolare sono quelli “globali”, sono stati usati per implementare altri sofisticati meccanismi chiamati gate che regolano in maniera rigida l’esecuzione di interrupt (interrupt gate), processi (task gate) e le chiamate al sistema operativo (call gate).

Anche qui, senza scendere nei dettagli, nel caso in cui si verifica un’interruzione o un’eccezione (sia esso esterna, cioé provocata da una periferica, o interna, provocata dal codice), viene prelevato un opportuno selettore della GDT (l’interrupt gate, appunto) che specifica in maniera precisa quale codice dev’essere eseguito, e le necessarie restrizioni.

I task gate vengono, invece, utilizzati per memorizzare lo stato di un processo e semplificare il passaggio dall’uno all’altro (i programmatori del kernel ringrazieranno sicuramente).

Infine per le call gate il meccanismo è analogo, ma il relativo gate offre in più la possibilità di specificare se e quanti parametri passare dall’applicazione al sistema operativo, sfruttando i relativi stack per la copia di questi valori. Questo meccanismo è, però, stato sostituito nei microprocessori più moderni da apposite istruzioni per richiamare molto più velocemente le API del s.o..

Già, perché, e questa è sicuramente la nota dolonte che ne ha frenato l’uso, tutti questi meccanismi hanno un costo molto elevato in termini di velocità di esecuzione. In particolare quello dei gate è estremamente oneroso in termini di cicli di clock consumati per completare l’operazione, visto che si arrivava a superare anche i 200 (sì, proprio duecento).

Per questo motivo i programmatori hanno snobbato per parecchio tempo la modalità protetta, preferendole la modalità reale. Giusto il tempo necessario a Intel per migliorarne l’implementazione, grazie anche al fatto che la tecnologia consentiva di integrare più transistor nei chip da dedicare a questo scopo.

Da lì in poi si è andato ad affermare l’utilizzo dei famigerati DOS extender che permettevano di lanciare applicazioni in modalità protetta. Il limite dei 640KB del DOS era, infatti, ancora lì, e la memoria oltre il MB degli 8086 era decisamente troppo allettante per non pensare di sfruttarla. Sì, perché con l’80286 si poteva arrivare a indirizzare ben 16MB di RAM e fino a 4GB di memoria virtuale!

Press ESC to close