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:
- gestione della memoria
- protezione
- multitasking
- input/output
- eccezioni e interruzioni
- inizializzazione
- coprocessing e multiprocessing
- debugging
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:
- Global Descriptor Table (GDT)
- Local Descriptor Table (LDT)
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)