Modalità Protetta

Capitolo 5: Modalità Protetta 80386 - Gestione della Memoria


Il supporto fornito dalla 80386 per la scrittura dei sistemi operativi, è strutturato come estensione di quello per la 80286, in modo da garantire la compatibilità verso il basso; molti dei concetti esposti nei precedenti capitoli, risultano quindi pienamente validi anche con questo nuovo modello di CPU.

Le funzionalità fornite dalla 80386 per la programmazione di sistema includono: Tutte queste funzionalità sono supportate attraverso apposite istruzioni di sistema e registri di sistema, di cui si parlerà al momento opportuno. In questo capitolo ci occupiamo della gestione della memoria; gli altri aspetti vengono illustrati nei capitoli successivi.

5.1 Gestione della memoria - Modello segmentato

Il meccanismo fondamentale per la gestione della memoria è la segmentazione. Ogni programma viene suddiviso in vari blocchi, tra loro indipendenti, denominati segmenti; ogni segmento, nel caso più semplice, è destinato a contenere un tipo specifico di informazione, come codice, dati o stack.

Le caratteristiche complete di un segmento vengono specificate attraverso un blocco di dati da 8 byte (64 bit) denominato segment descriptor; la Figura 5.1 illustra la struttura di un descrittore di segmento.

Figura 5.1 - Segment Descriptor (S = 1)


Come si può notare, per garantire la compatibilità verso il basso, le prime tre WORD coincidono pienamente con quelle destinate alla 80286; la WORD più significativa viene invece utilizzata per le estensioni dedicate alla 80386.

Il campo Base Address viene esteso a 32 bit attraverso gli 8 bit nelle posizioni dalla 56 alla 63.

Il campo LIMIT viene esteso a 20 bit attraverso i 4 bit nelle posizioni dalla 48 alla 51; in questo caso entrano in gioco anche il bit G (Granularity) in posizione 55 e il bit D/B in posizione 54 (descritto più avanti). Per il momento, supponiamo di avere D/B=1, che è il valore di default per la 80386 e indica che la CPU sta operando nel modo 32 bit.
Se G=0, la granularità del campo LIMIT è in BYTE; con 20 bit possiamo esprimere un campo LIMIT compreso tra 00000h e FFFFFh, valori che rappresentano l'offset massimo in BYTE. Gli offset all'interno del segmento vanno da 0 a LIMIT; la dimensione del segmento stesso può variare quindi da 1 byte (il solo offset 0) a 220 byte (1 MiB).
Se G=1, la granularità del campo LIMIT è in pagine da 4 KiB ciascuna (4096 byte); con 20 bit possiamo esprimere un campo LIMIT compreso tra 00000h e FFFFFh, valori che sommati a 1 rappresentano il numero di pagine disponibili. Gli offset all'interno del segmento vanno da 0 a (LIMIT+1)*4096-1; la dimensione del segmento stesso può variare quindi da 4096 byte (una sola pagina) a
220 * 4096 = 220 * 212 = 220+12 = 232 byte = 4 GiB
Si tenga presente che la granularità in pagine del campo LIMIT non ha nulla a che vedere con la paginazione della memoria.

Qualunque tentativo di accedere ad un offset che sconfina dagli estremi indicati, provoca una eccezione della CPU.

Il bit AV (Available), in posizione 52, viene ignorato dalla 80386 e può essere eventualmente usato dai SO; il bit in posizione 53 è riservato per usi futuri e deve valere rigorosamente 0.

Il bit D/B in posizione 54 è importante per la compatibilità con la 80286 e il suo significato varia a seconda del tipo di segmento. Per i segmenti di dati, questo bit viene indicato con B e prende il nome di Big bit. Se B=0, un segmento di dati non può avere una dimensione superiore a 64 KiB; se B=1, valgono le considerazioni esposte in precedenza per il campo LIMIT.
Per i segmenti di codice e stack, questo bit viene indicato con D e prende il nome di Default bit. In un segmento di codice, se D=0 la dimensione predefinita degli operandi e degli indirizzamenti è 16 bit (modo 16 bit); se D=1, il default è 32 bit (modo 32 bit). Per lo stack, se D=0 viene usato SP per gli indirizzamenti e la dimensione del segmento non può superare 64 KiB; se D=1, viene usato ESP per gli indirizzamenti e per la dimensione del segmento valgono le considerazioni esposte in precedenza per il campo LIMIT.

Nota importante

Si può notare che nei programmi scritti per la 80286, la WORD più significativa del descrittore di Figura 5.1 vale 0; quando tali programmi girano su una 80386, si ha quindi D/B=0, G=0, gli 8 bit più significativi del Base Address che valgono 0 e i 4 bit più significativi di LIMIT che valgono 0.
Nel seguito, salvo diverse indicazioni, assumeremo sempre D/B=1.

L'ACCESS BYTE, nelle posizioni da 40 a 47, rimane sostanzialmente invariato rispetto alla 80286.

Il bit P (Present) indica la disponibilità del segmento. Se P=1, il segmento è usabile in quanto si trova in memoria e contiene informazioni valide; se P=0, il segmento non è usabile in quanto non si trova in memoria (è stato deallocato per liberare spazio) o contiene informazioni non valide. Quando P=0, il SO può utilizzare per i propri scopi tutti i campi del descrittore di Figura 5.1, tranne gli 8 bit riservati all'ACCESS BYTE.
Qualunque tentativo di accedere ad un segmento con P=0, provoca una eccezione della CPU.

I due bit DPL indicano il livello di privilegio, 0, 1, 2 o 3, del segmento; il livello di privilegio più alto è 0 mentre il più basso è 3.
Grazie al DPL, è possibile attivare un meccanismo di protezione che stabilisce le regole di accesso ai vari segmenti.

Il bit S in posizione 44 vale 1 per indicare che il descrittore si riferisce ad un segmento di programma; i descrittori speciali o di sistema hanno invece S=0.

I 4 bit riservati a TYPE (compreso il bit A), per i segmenti di codice assumono il significato illustrato in Figura 5.2.

Figura 5.2 - ACCESS BYTE per i segmenti di codice (E = 1)


Il bit E (Executable) per i segmenti di codice vale sempre 1.
Il bit C (Conforming) permette di allentare le regole di protezione quando si salta verso un segmento con livello di privilegio maggiore; il suo funzionamento è stato descritto in dettaglio nel Capitolo 2.
Il bit R (Readable) permette di stabilire se il segmento di codice, oltre che eseguibile, è anche leggibile. Ponendo R=1, autorizziamo l'accesso in lettura ad eventuali dati memorizzati in un segmento di codice; l'accesso in scrittura è invece proibito (per ovvi motivi di sicurezza) per cui tali dati non sono modificabili (dati costanti).
Il bit A (Accessed) viene posto automaticamente a 1 dalla CPU ogni volta che accediamo ad un segmento. I SO pongono A=0, per poi verificare se la CPU ha riportato tale bit a 1; in questo modo è possibile raccogliere dati statistici sulla frequenza di utilizzo di un segmento. Un segmento che per un certo tempo non viene usato (A=0), può essere deallocato dal SO per fare posto ad altre informazioni.

I 4 bit riservati a TYPE (compreso il bit A), per i segmenti di dati assumono il significato illustrato in Figura 5.3.

Figura 5.3 - ACCESS BYTE per i segmenti di dati (E = 0)


Il bit E (Executable) per i segmenti di dati vale sempre 0.
Il bit ED (Expand Down) permette di ribaltare il significato del campo LIMIT per i segmenti di dati. Normalmente si ha ED=0 per indicare che i dati devono essere disposti tra l'offset minimo 0 e il massimo consentito dal campo LIMIT; qualunque tentativo di accedere ad un offset superiore al massimo, provoca una eccezione della CPU.
Se ED=1, i dati devono essere disposti tra l'offset minimo (LIMIT+1 se G=0, (LIMIT+1)*4096 se G=1) e il massimo consentito (220-1 se G=0, 232-1 se G=1); ciò permette di creare segmenti la cui dimensione può essere incrementata in fase di esecuzione, riducendo opportunamente il valore LIMIT. Qualunque tentativo di accedere ad un offset inferiore al minimo, provoca una eccezione della CPU.
Chiaramente, ED=0 è l'impostazione predefinita per i segmenti di dati; anche per i segmenti di stack, se non si ha la necessità di incrementarne la dimensione, non è necessario impostare ED=1.
Il bit W (Writable) permette di stabilire se il segmento di dati, oltre che in lettura, è accessibile anche in scrittura; un segmento di dati con W=0 contiene informazioni accessibili solo in lettura (dati costanti). Per i segmenti di stack si ha obbligatoriamente W=1; in caso contrario, la CPU genera una eccezione.
Il bit A ha lo stesso significato già illustrato per i segmenti di codice.


Tutti i descrittori di segmento e i descrittori speciali vengono sistemati in apposite tabelle denominate descriptor tables; esistono due tipi principali di tabelle dei descrittori: La GDT è una tabella unica, attraverso la quale il SO può controllare vari aspetti importanti, come la gestione del multitasking o la condivisione di servizi (ad esempio, librerie di procedure) con tutti i programmi in esecuzione.
Un qualunque programma destinato a girare in modalità protetta, deve predisporre almeno la GDT, mentre le altre tabelle sono facoltative.

La LDT è una tabella analoga alla GDT ma riservata ad un singolo task; ogni task in esecuzione ha la propria LDT.
All'interno della LDT sono presenti, in particolare, i descrittori di tutti i segmenti usati dal relativo task; tali segmenti formano lo spazio di indirizzamento privato del task stesso. In un sistema multitasking, grazie alle LDT, più task possono essere in esecuzione senza interferire tra loro; ogni task infatti ha il proprio codice eseguibile in segmenti differenti da quelli degli altri task.
Se necessario, più task possono condividere uno stesso segmento di dati; a tale proposito, nelle relative LDT devono specificare un descrittore che punta al segmento condiviso. Eventualmente, è persino consentito a due o più task di condividere la stessa LDT.

L'accesso alle tabelle dei descrittori avviene tramite un apposito selector (selettore) la cui struttura è mostrata in Figura 5.4.

Figura 5.4 - Selettore in modalità protetta


Il campo INDEX ha un'ampiezza di 13 bit e rappresenta un indice nella tabella dei descrittori; con 13 bit possiamo gestire sino a 213=8192 descrittori.

Il campo TI (Table Indicator) occupa 1 bit e definisce il tipo di tabella a cui si riferisce il campo INDEX; il valore 0 indica la GDT, mentre il valore 1 indica una LDT.

Il campo RPL ha un'ampiezza di 2 bit e rappresenta il Requested Privilege Level (livello di privilegio desiderato); come per il DPL, si va dal valore 0 (privilegio massimo) a 3 (privilegio minimo). Come abbiamo visto nei precedenti capitoli, nel caso dei selettori di segmenti di codice e stack, il campo RPL coincide con il CPL; per i selettori di segmenti di dati, questo stesso campo viene impiegato per mitigare le conseguenze di un uso improprio dei puntatori negli indirizzamenti.


Il fatto che il campo INDEX abbia un'ampiezza di 13 bit, fa si che una tabella possa contenere sino a 213=8192 descrittori (di segmento e speciali); la dimensione massima della tabella stessa è pari quindi a:
213 * 8 = 213 * 23 = 213+3 = 216 = 65536 byte = 64 KiB

La CPU deve sapere a quale indirizzo di memoria è presente la GDT; a tale proposito, in genere si utilizza un apposito descrittore. La GDT però è unica, per cui tutte le necessarie informazioni vengono inserite direttamente in un apposito registro di sistema denominato Global Descriptor Table Register (GDTR); la Figura 5.5 illustra tutti i dettagli.

Figura 5.5 - Global Descriptor Table Register (GDTR)


Il campo BASE indica il Base Address a 32 bit da cui parte in memoria la GDT. 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 GDT è 64 KiB, per cui per il campo LIMIT sono sufficienti 16 bit.

Il GDTR viene inizializzato attraverso l'istruzione di sistema LGDT (Load Global Descriptor Table Register).
LGDT - Load Global Descriptor Table Register

LGDT  Mem16:Mem32
L'istruzione LGDT 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).
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 (come il SO).

Nota importante

LGDT e LIDT (illustrata nei capitoli successivi), sono le uniche due istruzioni in grado di operare direttamente su un indirizzo lineare; il campo BASE richiesto come operando, infatti, è un indirizzo lineare a 32 bit e non un indirizzo logico Selector:Offset.

Se vogliamo conoscere il contenuto del GDTR, dobbiamo usare l'istruzione di sistema SGDT (Store Global Descriptor Table Register).
SGDT - Store Global Descriptor Table Register

SGDT  Mem
Questa istruzione legge 6 byte dal GDTR 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.
SGDT è un'istruzione di sistema destinata alla modalità protetta; può essere usata da un task che gira a qualunque livello di privilegio.

Nota importante

Il descrittore di indice 0 nella GDT è riservato e non può esse usato; viene anche definito NULL DESCRIPTOR. Il selettore del NULL DESCRIPTOR prende il nome di NULL SELECTOR e ha valore 0 che equivale a INDEX=0, TI=0 (GDT) e RPL=0.
Eventualmente, è possibile caricare il NULL SELECTOR nei registri di segmento DS, ES, FS e GS; un tentativo di utilizzare tali registri contenenti valore 0, provoca però una eccezione della CPU. Questo aspetto in genere viene sfruttato per inizializzare con il NULL SELECTOR i registri di segmento (dati) momentaneamente non usati, in quanto è proibito caricare in essi valori casuali.



Ogni task in esecuzione ha la propria LDT; le caratteristiche di ogni LDT vengono specificate in un apposito descrittore speciale, la cui struttura generale è illustrata in Figura 5.6.

Figura 5.6 - Special Descriptor (S = 0)


La principale differenza rispetto ai descrittori di segmento è il bit S che deve valere 0; inoltre, a seconda del valore assunto dal campo TYPE, la struttura di un descrittore speciale può variare notevolmente. Per i descrittori di LDT si ha TYPE=0011b.
Tutti i descrittori di LDT devono trovarsi tassativamente nella GDT.

In un sistema multitasking possono essere presenti più task che il SO fa girare a rotazione; in questo modo, l'utente ha l'impressione che i vari task stiano girando tutti contemporaneamente. In realtà quindi, in un determinato istante solo uno dei task è effettivamente in esecuzione. La CPU deve sapere a quale indirizzo di memoria è presente la LDT del task correntemente in esecuzione; a tale proposito, tutte le necessarie informazioni vengono inserite in un apposito registro di sistema denominato Local Descriptor Table Register (LDTR); la Figura 5.7 illustra tutti i dettagli.

Figura 5.7 - Local Descriptor Table Register (LDTR)


Solamente la parte relativa alla componente Selector è visibile ai programmi; la restante parte, destinata a contenere le componenti Base Address e LIMIT, è nascosta in quanto riservata alla CPU. Il campo BASE indica il Base Address a 32 bit da cui parte in memoria la LDT. 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 LDT è 64 KiB, per cui per il campo LIMIT sono sufficienti 16 bit.

Il LDTR viene inizializzato attraverso l'istruzione di sistema LLDT (Load Local Descriptor Table Register).
LLDT - Load Local Descriptor Table Register

LLDT  Reg16/Mem16
L'istruzione LLDT richiede come unico operando un registro o una locazione di memoria da 16 bit; 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 nel 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 genere, si tratta del SO).
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.

Se vogliamo conoscere il contenuto del LDTR, dobbiamo usare l'istruzione di sistema SLDT (Store Local Descriptor Table Register).
SLDT - Store Local Descriptor Table Register

SLDT  Reg16/Mem16
Questa istruzione legge unicamente i 16 bit del campo Selector nel LDTR e li copia nel registro o nella locazione di memoria a 16 bit specificata come operando.
SLDT è un'istruzione di sistema destinata alla modalità protetta; può essere usata da un task che gira a qualunque livello di privilegio.


Una volta che il LDTR è stato inizializzato, qualunque riferimento alla LDT del task correntemente in esecuzione non necessita più di un accesso al relativo descrittore nella GDT; infatti, tutte le informazioni di cui la CPU ha bisogno, sono già presenti nello stesso LDTR e possono essere quindi utilizzate via hardware alla massima velocità possibile.
Le stesse considerazioni valgono anche per i registri di segmento. Supponiamo, ad esempio, di voler associare ad ES un segmento dati il cui descrittore è presente nella LDT del task in esecuzione; se AX contiene il selettore di tale descrittore, possiamo scrivere:
   mov   es, ax
Se ora vogliamo trasferire in EAX un dato a 32 bit che si trova all'offset EBX di tale segmento, ci serviamo dell'istruzione:
   mov   eax, es:[ebx]
Teoricamente, la CPU si serve del selettore in ES per accedere al descrittore del segmento dati nella LDT; dal descrittore ricava il Base Address del segmento, gli somma l'offset EBX, accede all'indirizzo lineare così ottenuto, legge un dato a 32 bit e lo copia in EAX. Chiaramente, se per ogni indirizzamento fosse necessario un procedimento così contorto, si andrebbe incontro ad un vero e proprio disastro in termini di prestazioni.
Per ovviare a questo inconveniente, si segue la stessa tecnica già illustrata per il LDTR; anche i registri di segmento quindi sono dotati di una parte nascosta, come viene mostrato dalla Figura 5.8.

Figura 5.8 - Registri di segmento completi della 80386


Solamente la parte relativa alla componente Selector di ogni registro di segmento è visibile ai programmi; la parte restante, destinata a contenere i campi BASE, LIMIT e gli attributi del segmento (ACCESS BYTE, granularità, etc), è invisibile in quanto riservata alla CPU.
Ogni volta che carichiamo un nuovo selettore in un registro di segmento, la CPU accede al relativo descrittore nella tabella specificata, legge i campi BASE, LIMIT, ATTRIBUTES e li carica nella parte nascosta del registro stesso; da questo momento in poi, ogni indirizzamento che coinvolge quel segmento, non necessita più di un accesso alla tabella dei descrittori.
Nel caso quindi dell'istruzione:
   mov   eax, es:[ebx]
la CPU ha già pronto il Base Address (nella parte nascosta di ES) e lo somma all'offset in EBX per ottenere l'indirizzo lineare a cui accedere; tutti i necessari calcoli avvengono via hardware e quindi alla massima velocità possibile.

Nota importante

Si può notare che le parti nascoste dei vari registri di sistema, rappresentano una vera e propria memoria cache ad alte prestazioni, grazie alla quale la 80386 può gestire via hardware, alla massima velocità, gli aspetti appena illustrati.

La Figura 5.9 mostra un esempio relativo ad un programma che sfrutta il modello di memoria segmentato; si assume che le parti nascoste dei registri di segmento siano state già caricate dalla CPU con i vari campi BASE, LIMIT e attributi.

Figura 5.9 - Modello di memoria segmentato


Ovviamente, un programma può disporre di decine di segmenti; in un determinato momento della fase di esecuzione, sino a sei di essi possono essere gestiti attraverso altrettanti registri di segmento.

In base a quanto è stato esposto in precedenza, gli accessi in memoria nel modello segmentato, avvengono attraverso indirizzi logici Selector:Offset, che vengono convertiti dalla CPU in indirizzi lineari tramite la somma:
Base Address + Offset
(dove il Base Address viene letto dalla parte nascosta del registro di segmento coinvolto).

In assenza di paginazione della memoria, l'indirizzo lineare così ottenuto viene associato dalla CPU ad un indirizzo fisico nella RAM.

5.2 Gestione della memoria - Modello Flat

La 80386 mette a disposizione anche un modello di memoria estremamente semplice, il cui scopo è quello di agevolare la conversione a 32 bit di programmi scritti per CPU che non supportano la segmentazione. Nel caso ad esempio della CPU 8080, un programma è formato da codice, dati e stack che condividono uno stesso spazio di indirizzamento da 64 KiB; non esiste distinzione inoltre tra indirizzi logici, lineari e fisici.

La situazione appena descritta può essere simulata sulla 80386 scrivendo un programma il quale, attraverso la sola GDT, definisce tre descrittori per codice, dati e stack, che puntano tutti allo stesso segmento da 4 GiB, caratterizzato da D/B=1, G=1, BASE=0 e LIMIT=FFFFFh; si ottiene così il risultato mostrato dalla Figura 5.10.

Figura 5.10 - Modello di memoria flat


Questa struttura può essere ulteriormente semplificata riducendo a due il numero di descrittori, uno per il codice e uno per i dati e lo stack; ovviamente, è compito del programmatore fare in modo che i vari blocchi non si sovrappongano tra loro (in particolare, ESP deve essere posizionato ad un indirizzo di memoria sufficientemente lontano dal blocco dati e dal blocco codice).

Non esiste quindi alcuna impostazione hardware che permetta alla 80386 di attivare il modello flat; si tratta semplicemente di usare il modello segmented come indicato dalla Figura 5.10.

Il modello flat è particolarmente indicato quando si ha la necessità di far girare un singolo programma; in un caso del genere, si tende anche a rinunciare ai vari meccanismi di protezione. Impostando il campo LIMIT al valore massimo possibile, si evitano le eccezioni dovute al superamento dei limiti del segmento; altre eccezioni possono essere evitate, in particolare, assegnando il valore 0 al livello di privilegio di tutti i selettori di segmento, descrittori di segmento e descrittori delle pagine (vedere più avanti).

Un programma che gira nel modello flat viene eseguito con più velocità ed efficienza; infatti, i registri di segmento vengono inizializzati una sola volta e in modo definitivo, tutti gli indirizzamenti sono di tipo NEAR e, soprattutto, vengono evitati molti controlli che la CPU usualmente effettua a causa dei meccanismi di protezione.

Nei precedenti capitoli abbiamo visto che la modalità protetta si rivela molto utile per la ricerca degli errori nei programmi; qualunque comportamento anomalo viene subito segnalato dalla CPU attraverso un'eccezione.
Se si hanno esigenze di questo genere, è possibile anche impostare un modello di memoria flat con protezione. In pratica, lo schema rimane come illustrato in Figura 5.10, tranne per il campo LIMIT che deve specificare l'offset massimo per i vari blocchi di codice, dati e stack, come in Figura 5.9; in questo modo, come sappiamo, la CPU può segnalare eventuali tentativi di invadere aree di memoria riservate.

Anche per il modello flat, in assenza di paginazione della memoria, l'indirizzo lineare calcolato dalla CPU viene associato ad un indirizzo fisico nella RAM.

5.3 Gestione della memoria - Paginazione

Abbiamo visto che la 80286, quando opera in modalità protetta, è in grado di simulare una quantità di memoria ben superiore ai 16 MiB fisicamente indirizzabili tramite il suo Address Bus a 24 linee; ciò è possibile grazie ad un livello di astrazione denominato memoria virtuale. Un programma viene obbligato ad accedere alla RAM attraverso indirizzi logici del tipo Selector:Offset, dove Offset è uno spiazzamento calcolato rispetto al Base Address del segmento associato a Selector; il risultato è che il programma stesso vede la memoria come un insieme di segmenti, ciascuno dei quali può raggiungere una dimensione massima di 64 KiB. Considerando che la GDT e la LDT del programma permettono di gestire sino a 214 descrittori di segmento e ipotizzando segmenti tutti da 64 KiB, con questa tecnica è possibile simulare sino a 1 GiB di memoria virtuale; ovviamente, tutto questo spazio di indirizzamento viene mappato solo in piccola parte nella RAM, mentre la parte più consistente si trova su disco.
Se l'indirizzo logico fa riferimento ad un segmento presente in RAM, la CPU somma Offset a Base Address e ottiene un indirizzo lineare a 24 bit; tale indirizzo ricade sicuramente nei 16 MiB gestibili dalla 80286 e quindi rappresenta anche l'indirizzo fisico da caricare nell'Address Bus. Se l'indirizzo logico fa riferimento ad un segmento che era stato spostato su disco, il programma viene interrotto e si ottiene un'apposita eccezione; i SO possono intercettare tale eccezione, ricaricare il segmento in RAM e riavviare il programma.

Questo stesso livello di astrazione è presente anche sulla 80386; considerando che in questo caso i segmenti possono raggiungere la dimensione massima di 4 GiB, i 214 selettori di segmento gestibili con la GDT e la LDT permettono di simulare ben 64 TiB di memoria virtuale, contro i soli 4 GiB di RAM indirizzabili dall'Address Bus a 32 bit della CPU!
La situazione diventa però problematica nel momento in cui si ha la necessità di spostare segmenti di programma dalla RAM al disco o viceversa. Con la 80286 questi spostamenti avvengono in modo molto efficiente grazie al fatto che un segmento non supera i 64 KiB; nel caso invece della 80386, per segmenti relativamente piccoli si ha lo stesso grado di efficienza, ma l'eventuale presenza di segmenti di enormi dimensioni rende proibitivo questo modo di procedere.

Per ovviare a questo inconveniente, la 80386 offre la possibilità di attivare un secondo livello di astrazione denominato paging (paginazione della memoria); questa funzionalità è supportata via hardware dalla CPU, per cui il suo uso comporta un calo minimo delle prestazioni.
Un indirizzo logico del modello segmentato, se non si verificano eccezioni, viene convertito dalla CPU in un indirizzo lineare; senza la paginazione, tale indirizzo lineare diventa direttamente un indirizzo fisico che ricade all'interno dei 4 GiB di RAM gestibili dalla 80386. Con la paginazione attiva, un programma vede gli stessi 4 GiB in forma virtuale, suddivisi in tanti piccoli blocchi da 4096 byte (4 KiB) ciascuno, denominati pagine; nel caso generale, solo alcune pagine si trovano in RAM, mentre buona parte di esse risiede su disco. Un indirizzo lineare quindi ricade in una di queste pagine e solo in una seconda fase viene convertito in un indirizzo fisico, attraverso un meccanismo analogo a quello del modello segmentato. Se l'indirizzo lineare si riferisce ad una pagina in RAM, si ottiene subito l'indirizzo fisico; in caso contrario, si ottiene un'apposita eccezione (Page Fault), che permette al SO di caricare in memoria la pagina richiesta.
Per capire i vantaggi della paginazione, consideriamo un programma che deve gestire strutture dati di diversi terabyte, distribuite in segmenti di grosse dimensioni; chiaramente, non essendoci spazio a sufficienza nella RAM, buona parte di tali dati deve necessariamente risiedere su disco. Se ora abbiamo bisogno di elaborare una parte dei dati, senza la paginazione siamo costretti a caricare in RAM l'intero segmento che li contiene; con la paginazione attiva invece, possiamo suddividere il segmento stesso in tante pagine e caricare nella RAM solo quelle che ci interessano.
Appare evidente poi che, grazie alle loro ridotte dimensioni, le pagine se necessario possono essere spostate con notevole efficienza, dalla RAM al disco o viceversa.

Per gestire la paginazione della memoria, la 80386 si serve di quattro nuovi registri di sistema a 32 bit denominati Control Registers e indicati con CR0, CR1, CR2 e CR3.
Si tenga presente che CR1, pur facendo parte della 80386, risulta inaccessibile in quanto riservato per usi futuri (Figura 5.11).

Figura 5.11 - Control Register 1 (CR1)


La paginazione della memoria può essere attivata o disattivata tramite il registro CR0, la cui struttura è illustrata in Figura 5.12.

Figura 5.12 - Control Register 0 (CR0)


Come si può notare, la WORD meno significativa di CR0 incorpora il registro MSW (Machine Status Word) della 80286. Per ragioni di compatibilità, la 80386 supporta le istruzioni LMSW (Load Machine Status Word) e SMSW (Store Machine Status Word); tali istruzioni operano esclusivamente sui primi 16 bit di CR0.
Quando si scrive un programma per la 80386, si raccomanda vivamente di non usare LMSW e SMSW; al loro posto si deve impiegare la variante di MOV che opera direttamente sui registri di controllo.

Nota importante

Il significato dei bit PE, MP, EM e TS è stato già illustrato in dettaglio nel Capitolo 3. Nel caso della 80386 è importante sottolineare una notevole semplificazione introdotta dal bit PE (Protection Enable) rispetto alla 80286. PE=1 porta la CPU in modalità protetta; in questo modo vengono attivati tutti i meccanismi di protezione per i segmenti e per le pagine. PE=0 riporta la CPU direttamente in modalità reale; non è più necessario quindi il procedimento contorto richiesto dalla 80286!
Si tenga presente che ponendo PE=0 attraverso l'istruzione LMSW, la 80386 non viene riportata in modalità reale; per fare ciò è necessario servirsi dell'istruzione MOV con operando destinazione CR0.

Il bit R (Reserved) è riservato alla 80386; quando si modifica CR0, il contenuto di tale bit deve essere rigorosamente preservato!

I bit nelle posizioni dalla 5 alla 30 sono riservati e in genere vengono posti a 0.

Il bit PG (Paging) consente di attivare o disattivare la paginazione. PG=1 attiva la paginazione, mentre PG=0 la disattiva.

Se la paginazione è disattivata (PG=0), l'indirizzo lineare calcolato dalla CPU equivale all'indirizzo fisico della RAM da caricare nell'Address Bus; se invece la paginazione è attiva (PG=1), entra in gioco un secondo livello di astrazione, con i 32 bit dell'indirizzo lineare che vengono suddivisi come illustrato in Figura 5.13.

Figura 5.13 - Indirizzo lineare con PG=1


Per capire il significato dei tre campi DIR, PAGE e OFFSET, bisogna considerare che con la paginazione attiva, deve essere predisposta in memoria almeno una tabella denominata Page Directory o "tabella di primo livello"; tale tabella occupa esattamente una pagina (4096 byte) e al suo interno può contenere sino a 1024 elementi da 32 bit (4 byte), ciascuno dei quali punta ad una "tabella di secondo livello" denominata Page Table.
Ogni tabella di secondo livello occupa esattamente una pagina e al suo interno può contenere sino a 1024 elementi da 32 bit (4 byte), ciascuno dei quali punta ad una pagina nella "RAM virtuale", denominata Page Frame, destinata a contenere codice o dati.

Il campo DIR occupa 10 bit e rappresenta un indice all'interno della Page Directory; con 10 bit possiamo rappresentare 210=1024 indici, compresi tra 0 e 1023.
Il campo PAGE occupa 10 bit e rappresenta un indice all'interno della Page Table selezionata da DIR; con 10 bit possiamo rappresentare 210=1024 indici, compresi tra 0 e 1023.
Il campo OFFSET occupa 12 bit e rappresenta un offset all'interno della Page Frame selezionata da PAGE; con 12 bit possiamo rappresentare 212=4096 offset, compresi tra 0 e 4095.

Si noti che una Page Directory permette di gestire sino a 1024 Page Tables; ogni Page Table permette di gestire sino a 1024 Page Frames, ciascuna delle quali occupa 4096 byte. Con questa tecnica si può quindi coprire l'intero spazio di indirizzamento da 4 GiB della 80386; abbiamo infatti:
1024 * 1024 * 4096 = 210 * 210 * 212 = 210+10+12 = 232 byte = 4 GiB
In un sistema multitasking si può decidere di predisporre una sola Page Directory da condividere tra tutti i task in esecuzione oppure si può avere una Page Directory per ogni task in esecuzione; in questo secondo caso, ogni task in esecuzione è in grado di vedere sino a 4 GiB virtuali, parte dei quali si trova su disco. In alternativa, è anche possibile scegliere una via di mezzo; ad esempio, alcuni task condividono la stessa Page Directory, mentre altri task usano ciascuno una propria Page Directory.

L'indirizzo in RAM della Page Directory correntemente in uso viene memorizzato nel registro CR3, che per questo motivo viene anche chiamato Page Directory Base Register (PDBR). La struttura di CR3 è illustrata in Figura 5.14.

Figura 5.14 - Control Register 3 (CR3)


I 12 bit meno significativi di CR3 sono riservati e vengono considerati implicitamente tutti di valore 0; un tentativo di modificarli attraverso MOV non produce alcun effetto.
Questa situazione è legata al fatto che le pagine (e quindi anche le tabelle, che sono a loro volta delle pagine) sono disposte in memoria con allineamento alla pagina, per cui partono tutte da indirizzi multipli interi di 4096; un indirizzo di questo genere ha appunto i 12 bit meno significativi che valgono 0. La CPU quindi legge solo i 20 bit più significativi di CR3 e aggiunge ad essi 12 zeri a destra per ottenere l'indirizzo lineare a 32 bit della Page Directory.

Sia le tabelle di primo livello sia quelle di secondo livello, contengono elementi la cui struttura è illustrata in Figura 5.15.

Figura 5.15 - Elemento di una tabella


I bit colorati in grigio sono riservati alla CPU e devono valere 0; in ogni caso, si raccomanda di preservare sempre il loro contenuto.

Il campo PAGE FRAME ADDRESS specifica i 20 bit più significativi del Base Address di una tabella o di una pagina; siccome le tabelle e le pagine sono allineate ad indirizzi multipli interi di 4096, i 12 bit meno significativi dello stesso Base Address valgono implicitamente 0.
Nel caso di una Page Directory, il Base Address si riferisce ad una Page Table. Nel caso di una Page Table, il Base Address si riferisce ad una Page Frame.

Il bit P (Present) ha senso solo per una Page Table e indica se la Page Frame a cui si riferisce l'elemento si trova in memoria (P=1) o su disco (P=0). Quando P=0, tutti i restanti bit, da 1 a 31, dell'elemento di Figura 5.15 sono a disposizione del SO che li può utilizzare per memorizzare varie informazioni.
Un tentativo di accesso ad una Page Frame che ha P=0 interrompe il programma in esecuzione e genera un'apposita eccezione (Page Fault); i SO possono intercettare tale eccezione, ricaricare la pagina in memoria e riavviare il programma.
In caso di eccezioni generate dal meccanismo di paginazione, l'indirizzo lineare del punto in cui si è verificato il problema viene caricato nel registro CR2, la cui struttura è illustrata in Figura 5.16.

Figura 5.16 - Control Register 2 (CR2)


Il bit A (Accessed) di Figura 5.15 viene posto automaticamente a 1 dalla CPU (anche nella Page Directory) ogni volta che accediamo in lettura o scrittura ad una Page Table o ad una Page Frame. I SO pongono A=0, per poi verificare se la CPU ha riportato tale bit a 1; in questo modo è possibile raccogliere dati statistici sulla frequenza di utilizzo di una tabella di secondo livello o di una pagina. Una pagina che per un certo tempo rimane inutilizzata (A=0), può essere spostata su disco per fare posto ad altre pagine.

Il bit D (Dirty) viene posto automaticamente a 1 dalla CPU ogni volta che accediamo in scrittura ad una Page Frame; ciò vale solo per gli elementi di una Page Table, mentre il bit D degli elementi di una Page Directory è indefinito.
I SO pongono D=0, per poi verificare se la CPU ha riportato tale bit a 1; in caso affermativo, lo stesso SO deve provvedere ad aggiornare una eventuale copia della pagina su disco in modo che coincida con quella in memoria.

I bit R/W e U/S vengono utilizzati per impostare i meccanismi di protezione applicati alle pagine.

I 3 bit del campo AVAILABLE sono a disposizione dei SO.


Possiamo riassumere tutti i concetti appena esposti, attraverso lo schema mostrato in Figura 5.17, che si riferisce al caso di una Page Frame presente nella RAM e illustra i passaggi che trasformano un indirizzo lineare in un indirizzo fisico.

Figura 5.17 - Paginazione della memoria


In presenza quindi di un indirizzo lineare e con la paginazione attiva, la CPU legge da CR3 il Base Address della Page Directory correntemente in uso; all'indirizzo base della Page Directory viene sommato DIR*4 e si ottiene la posizione dell'elemento che contiene il Base Address della Page Table.
All'indirizzo base della Page Table viene sommato PAGE*4 e si ottiene la posizione dell'elemento che contiene il Base Address della Page Frame.
All'indirizzo base della Page Frame viene sommato OFFSET e si ottiene l'indirizzo fisico in RAM da caricare sull'Address Bus.

Osservando la Figura 5.17 ci si rende conto che si viene a creare lo stesso problema già illustrato per GDT e LDT; i continui accessi alla Page Directory e alle varie Page Tables, possono avere serie ripercussioni in termini di prestazioni.
La 80386 risolve questo inconveniente grazie ad una ulteriore memoria cache ad alte prestazioni, denominata Translation Lookaside Buffer (TLB). Nel TLB vengono memorizzati gli elementi delle Page Tables maggiormente usati durante le varie operazioni legate alla paginazione; in questo modo, tali operazioni possono essere svolte via hardware dalla CPU, alla massima velocità possibile, in quanto vengono evitati gli accessi alle tabelle stesse.

Il TLB è inaccessibile ai normali programmi; la sua gestione è di competenza dei SO, in quanto richiede il massimo livello di privilegio. Ogni volta che viene modificato un elemento di una Page Table, il SO deve provvedere a svuotare il TLB; ciò può avvenire in modo implicito in seguito ad un task switch che modifica CR3, mentre il metodo esplicito consiste nell'utilizzo della variante di MOV con operando destinazione CR3.

MOV - Move to/from Special Registers

MOV   Reg32, CR0/CR2/CR3
MOV   CR0/CR2/CR3, Reg32
Questa forma di MOV richiede un operando di tipo Reg32, mentre l'altro operando deve essere un registro speciale (come i registri di controllo); gli altri registri speciali vengono esaminati nei capitoli successivi.
Questa forma di MOV è un'istruzione di sistema, disponibile però anche in modalità reale in quanto il suo scopo è predisporre le tabelle necessarie per la paginazione. Una volta entrati in modalità protetta, MOV può essere usata solo da un task che gira al massimo livello di privilegio (come il SO).

Ogni volta quindi che scriviamo, ad esempio
   mov   cr3, eax
provochiamo lo svuotamento del TLB.


Un aspetto molto importante da sottolineare quando si utilizza il modello di memoria segmentato in combinazione con la paginazione, riguarda il fatto che, mentre le pagine partono sempre da indirizzi multipli interi di 4096, i segmenti possono invece partire da un indirizzo qualunque; inoltre, appare evidente che si può presentare il caso di grossi segmenti che occupano centinaia di pagine, ma anche di segmenti più piccoli di una pagina.
Può capitare allora che un segmento contenga la fine di una pagina e l'inizio della pagina successiva; viceversa, può capitare che una pagina contenga la fine di un segmento e l'inizio del segmento successivo.
Queste situazioni in certi casi possono avere serie ripercussioni sulle prestazioni; ad esempio, un segmento più piccolo di 4096 byte che si trova a cavallo tra due pagine, comporta un aumento del numero di accessi alle tabelle.

Un modo molto efficace per ridurre questi problemi consiste nello scegliere opportuni requisiti di allineamento per i segmenti; l'ideale chiaramente sarebbe avere segmenti allineati alla pagina.
Nel caso poi di segmenti molto grandi, una tecnica largamente usata consiste nel dedicare una Page Table a ciascuno di essi; in questo modo si semplificano notevolmente le operazioni di accesso in quanto il segmento è gestito sempre dalla stessa Page Table a cui corrisponde un unico elemento nella Page Directory.

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)