Modalità Protetta

Capitolo 4: Modalità Protetta 80386


Nei precedenti capitoli abbiamo visto che la segmentazione a 64 KiB e il modo contorto di saltare dalla modalità protetta a quella reale, sono le principali cause dello scarso interesse suscitato dalla 80286 tra gli sviluppatori di SO per PC. La segmentazione a 64 KiB vanifica la possibilità di accedere in modo lineare ai 16 MiB di memoria fisica indirizzabili in modalità protetta. La necessità di un reset della CPU per tornare in modalità reale, rende altamente inefficienti i SO che, pur essendo destinati alla modalità protetta, devono anche garantire la possibilità di eseguire i programmi scritti per la 8086.

La svolta tanto attesa dagli sviluppatori arriva nel 1986 quando, dopo un anno di sperimentazione, viene immessa sul mercato la CPU 80386; con il suo Address Bus a 32 bit, questo microprocessore è in grado di indirizzare una quantità di memoria fisica che può raggiungere
232 = 4294967296 byte = 4 GiB
La vera novità però sta nel fatto che la 80386 dispone di registri puntatori anch'essi a 32 bit; ciò rende possibile l'accesso lineare a 4 GiB di RAM attraverso offset che spaziano da 00000000h a FFFFFFFFh!

4.1 Caratteristiche generali della CPU 80386

La 80386 è stata progettata in modo da garantire la compatibilità con le CPU di classe inferiore; in questo caso, oltre alla modalità reale della 8086, tale compatibilità deve necessariamente riguardare anche la modalità protetta della 80286.

4.1.1 Organizzazione della memoria

Per garantire la compatibilità verso il basso, un primo aspetto fondamentale riguarda il modo di rappresentare la RAM. Come accade con i modelli precedenti di CPU, anche la 80386 fa in modo che ai programmi la memoria centrale "grezza" (o memoria fisica) appaia come un vettore di BYTE, indicizzati da 0 a 232-1; l'indice di ogni BYTE ne rappresenta anche l'indirizzo fisico. In Figura 4.1 vediamo ad esempio il BYTE in posizione 7 a cui corrisponde quindi l'indirizzo fisico 7. Come è stato spiegato nei precedenti capitoli, le CPU che supportano la modalità protetta non permettono però ai programmi di accedere in modo diretto alla memoria fisica; viene fatto ricorso invece ad un livello di astrazione denominato memoria virtuale.
La memoria virtuale viene simulata dalla CPU sfruttando, oltre alla memoria fisica disponibile, anche altri supporti, come gli hard disk; in questo modo si ottiene uno spazio di indirizzamento lineare notevolmente più grande di quello offerto dalla RAM realmente installata sul computer. Nel Capitolo 1 abbiamo visto, ad esempio, che la 80286 supporta sino a 16 MiB di memoria fisica, ma ben 1 GiB di memoria virtuale.
I programmi accedono alla memoria virtuale attraverso un metodo basato sul concetto di indirizzo logico; un indirizzo logico fa riferimento ad una locazione di memoria che può trovarsi nella RAM o su disco. Se la locazione di memoria si trova nella RAM, la CPU converte l'indirizzo logico in un indirizzo lineare, che rappresenta anche l'indirizzo fisico della locazione stessa; in caso contrario, la CPU genera un'eccezione che deve essere gestita nel modo più opportuno dal SO.
La Figura 4.2 mostra il caso di un computer che lavora con indirizzi logici formati da una generica coppia Base:Offset; in assenza di eccezioni, un indirizzo logico viene convertito in un indirizzo lineare mediante la somma Base+Offset. In questo esempio vediamo che l'indirizzo logico fa riferimento ad un'area della memoria virtuale che risiede nella RAM; di conseguenza, la CPU associa il corrispondente indirizzo lineare ad un indirizzo fisico. Quando una CPU della famiglia 80x86 opera in modalità reale, sappiamo che l'hardware rende visibile solo 1 MiB di RAM; l'accesso fisico alla memoria avviene in tal caso tramite indirizzi logici del tipo Seg:Offset da 16+16 bit, dove Seg indica uno tra i possibili 216 paragrafi da 16 byte da cui partono i vari segmenti, mentre Offset è uno spiazzamento compreso tra 0 e 216-1, all'interno del segmento di appartenenza.
Ogni indirizzo logico viene convertito dalla CPU in un indirizzo lineare a 20 bit secondo la formula:
Seg * 16 + Offset
L'indirizzo lineare che si ottiene corrisponde sempre ad un indirizzo fisico realmente presente nella RAM da 1 MiB, da cui il nome "modalità reale"; in questo caso, la memoria virtuale coincide con la memoria fisica.
Come vedremo nel seguito, la 80386 introduce una nuova modalità operativa denominata Virtual 8086 Mode (VM86); grazie a questa funzionalità, un sistema multitasking in modalità protetta può far girare uno o più programmi destinati alla modalità reale, riservando a ciascuno di essi uno spazio di indirizzamento virtuale da 1 MiB.

Nella modalità protetta 80x86, l'accesso fisico alla memoria avviene tramite indirizzi logici del tipo Selector:Offset; il campo Selector, come sappiamo, punta ad un descrittore di segmento che contiene il relativo Base Address, mentre Offset è uno spiazzamento all'interno del segmento stesso.
In assenza di eccezioni, un indirizzo logico viene convertito dalla CPU in un indirizzo lineare (a 24 bit per la 80286 e a 32 bit per la 80386) secondo la formula:
Base Address + Offset
L'indirizzo lineare che si ottiene viene poi associato dalla CPU ad un indirizzo fisico nella RAM.

Dalle considerazioni appena esposte risulta che in modalità protetta i programmi vedono il livello di astrazione della memoria virtuale come un insieme di segmenti; l'accesso ad ogni segmento avviene tramite un offset calcolato rispetto al Base Address del segmento stesso.
Come si vede in Figura 4.2, la memoria virtuale è molto maggiore di quella reale, per cui una parte di essa, se necessario, può essere simulata su un dispositivo esterno, come un disco fisso; se un indirizzo specificato da un programma in esecuzione fa riferimento ad un blocco di memoria spostato su disco, la CPU interrompe il programma stesso e genera una "eccezione di non presenza". Il SO può intercettare l'eccezione di non presenza e ricaricare in memoria il blocco richiesto; in questo modo, il programma precedentemente interrotto può essere riavviato.

La 80386 fornisce una serie di funzionalità che permettono ai SO di organizzare la gestione della memoria virtuale nel modo più opportuno; in particolare, per venire incontro alle esigenze degli sviluppatori, vengono resi disponibili via hardware due modelli di memoria: Il modello segmented è del tutto analogo a quello visto nei precedenti capitoli per la 80286 e prevede che codice, dati e stack di ogni task vengano distribuiti in distinti segmenti di programma a cui si accede tramite indirizzi logici Selector:Offset. Il campo Selector punta ad un descrittore di segmento (nella GDT o LDT) che contiene il Base Address del segmento a cui vogliamo accedere; il campo Offset è uno spiazzamento all'interno del segmento stesso e viene sommato al Base Address per ottenere l'indirizzo lineare. La novità sta nel fatto che stavolta la componente Offset ha un'ampiezza di 32 bit, per cui ogni segmento può raggiungere virtualmente la dimensione di 4 GiB!
La conseguenza di tutto ciò è che la memoria virtuale indirizzabile dalla 80386 è enormemente maggiore dell'unico GiB gestibile con la 80286. Come sappiamo, ogni task ha a disposizione la GDT e la propria LDT, ciascuna delle quali permette di gestire sino a 213 descrittori di segmento; ogni segmento può avere un'ampiezza massima di 232 byte, per cui la memoria virtuale totale indirizzabile dalla 80386 raggiunge il colossale valore di ben:
2 * 213 * 232 = 21+13+32 = 246 byte = 64 TiB
(1 terabyte = 1024 gigabyte)!

Con il modello flat, nel caso più semplice un programma vede la RAM come un unico gigantesco segmento da 4 GiB; all'interno di tale segmento i BYTE risultano indicizzati in modo lineare (flat) da 0 a 232-1.
Si tratta in realtà di un modello di memoria ugualmente segmentato; la differenza fondamentale sta nel fatto che i vari segmenti di programma condividono tutti lo stesso Base Address, per cui risultano sovrapposti tra loro.
Questo modello di memoria viene gestito in modo estremamente efficiente in quanto i vari registri di segmento, una volta inizializzati, non vengono più modificati per tutta la fase di esecuzione; di conseguenza, gli indirizzamenti sono tutti di tipo NEAR. Il codice macchina che ne deriva risulta più compatto e più rapido da eseguire, grazie anche al fatto che la CPU non deve effettuare i controlli di protezione necessari ogni volta che si carica un nuovo selettore in un registro di segmento.
Sia il modello segmentato sia quello flat, possono usufruire di un secondo livello di astrazione rappresentato dalla paginazione della memoria; in questo modo, come viene spiegato nel seguito, anche lo spazio di indirizzamento lineare da 4 GiB della 80386 viene virtualizzato.

4.1.2 Paginazione della memoria

Il modello segmentato della 80386 in determinate circostanze può presentare un serio problema. Nel caso della 80286, la memoria risulta segmentata a 64 KiB e ciò permette di suddividere un programma in tanti piccoli moduli, ciascuno dei quali viene normalmente inserito in un distinto segmento. La struttura modulare così ottenuta si rivela piuttosto vantaggiosa nel momento in cui si ha la necessità di spostare segmenti dalla memoria al disco fisso o viceversa (swapping); ciò è dovuto proprio al fatto che la dimensione massima di un segmento non supera i 64 KiB.
Con la 80386 la situazione è ben diversa in quanto un segmento di programma può raggiungere virtualmente i 4 GiB; in tali condizioni, lo scambio di grossi segmenti tra memoria e disco creerebbe seri problemi di efficienza.
Per ovviare a questo inconveniente, la 80386 rende disponibile un secondo livello di astrazione che si inserisce tra la memoria virtuale e quella fisica (Figura 4.2) ed è denominato paging (paginazione). Se si attiva la paginazione, lo spazio di indirizzamento lineare da 4 GiB della CPU viene suddiviso in tante "pagine", ciascuna delle quali ha una dimensione fissa di 4 KiB; un indirizzo lineare ottenuto da un indirizzo logico, ricade quindi in una delle pagine disponibili, che può trovarsi nella RAM o su disco. Senza la paginazione, siamo costretti ad esempio a caricare interamente nella RAM un grosso segmento contenente dati da elaborare; con la paginazione, il segmento stesso può essere suddiviso in tante pagine e solo quelle necessarie vengono caricate nella RAM.
La situazione che si viene a creare rende molto efficiente lo scambio di blocchi di programma tra memoria e disco; infatti, tutto si riduce alla "movimentazione" di piccolissime pagine da 4 KiB ciascuna.

Chiaramente, i due livelli di astrazione appena illustrati (memoria virtuale e paginazione) comportano ripercussioni negative sulle prestazioni; gli svantaggi però vengono compensati abbondantemente dai vantaggi nel momento in cui si ha la necessità di ricorrere in modo massiccio allo swapping delle pagine di memoria. Bisogna tenere presente, infatti, che la paginazione è stata pensata per situazioni particolari; ad esempio, programmi che devono gestire strutture dati di enormi dimensioni, anche di diversi terabyte.

Se la paginazione è disattivata, un indirizzo lineare valido viene associato direttamente ad un indirizzo fisico.

4.1.3 Tipi di dati supportati

Come si vede in Figura 4.3, la 80386 può maneggiare direttamente blocchi di dati da 8, 16 e 32 bit; tali blocchi vengono denominati, rispettivamente, BYTE, WORD e DWORD (Double Word). In ciascuno di questi blocchi, il bit in posizione 0 è il "bit meno significativo" (Least Significant Bit o LSB), mentre il bit più a sinistra è il "bit più significativo" (Most Significant Bit o MSB).

La convenzione Little Endian seguita anche dalla 80386, prevede che un dato di tipo WORD o DWORD venga disposto in memoria con il suo BYTE meno significativo che occupa l'indirizzo più basso; tale indirizzo è anche quello del dato stesso. In Figura 4.4 vediamo il caso di una DWORD che occupa gli offset 8, 9, 10, 11 e contiene il valore 3DF18C1Ah; si ha quindi 1Ah all'offset 8, 8Ch al 9, F1h al 10 e 3Dh all'11. L'indirizzo della DWORD è l'offset 8 (rispetto al Base Address del segmento di appartenenza). Notiamo ancora una volta che la rappresentazione del vettore di memoria con gli indirizzi crescenti da destra verso sinistra, è molto vantaggiosa in quanto i dati di tipo WORD e DWORD appaiono orientati nel modo naturale, con il peso delle cifre che cresce da destra verso sinistra.

Ciascuno dei blocchi di dati di Figura 4.3 può essere disposto in memoria ad un indirizzo qualunque; la 80386 traduce tale indirizzo in un numero adeguato di accessi (cicli di memoria) in lettura o scrittura. Grazie alla possibilità di accedere a gruppi di 1, 2, 3 o 4 byte per volta, la 80386 limita a 2 il numero massimo di cicli necessario per completare una operazione di lettura o scrittura in memoria; questo aspetto è stato già illustrato in dettaglio nel Capitolo 8 del tutorial Assembly Base, da cui è tratta la Figura 4.5. La Figura 4.5 si riferisce alla 80386 modello DX, che dispone di un Data Bus interno e esterno a 32 bit; il modello SX è una versione più economica con Data Bus interno a 32 bit e esterno a 16 bit.

Come si può notare, la memoria centrale viene organizzata in quaterne di celle; ogni cella occupa 1 byte e ogni quaterna parte sempre da un indirizzo multiplo intero di 4. Le 32 linee (da D0 a D31) del Data Bus della 80386 DX possono posizionarsi esclusivamente su una qualunque di queste quaterne; non è possibile quindi avere il Data Bus posizionato a cavallo tra due quaterne (ad esempio, sulle celle C3, C4, C5 e C6).
La conseguenza di tutto ciò è che, se vogliamo spingere al massimo la velocità di accesso in memoria, dobbiamo provvedere ad allineare adeguatamente i blocchi di dati di Figura 4.3.

I dati di tipo BYTE non richiedono alcun tipo di allineamento; la 80386, infatti, può accedere ad un BYTE in memoria sfruttando uno qualunque dei 4 gruppi di 8 linee del suo Data Bus. In riferimento alla Figura 4.5, per accedere ad esempio al BYTE contenuto nella cella C2, la CPU usa le linee da D16 a D23.

Un dato di tipo WORD richiede un solo accesso in memoria, purché si trovi interamente contenuto in una quaterna di celle. In riferimento alla Figura 4.5, per accedere ad esempio alla WORD contenuta nelle celle C2 e C3, la CPU usa le linee da D16 a D31; se però la WORD si trova nelle celle C3 e C4, sono necessari due accessi in memoria in quanto il Data Bus deve essere prima posizionato sulla prima quaterna e successivamente sulla seconda.
Un modo semplice per evitare problemi del genere consiste nel disporre i dati di tipo WORD ad indirizzi multipli interi di 2 (si parla allora di "allineamento alla WORD"); in questo modo siamo sicuri che i dati stessi si troveranno sempre all'interno di una quaterna.

Un dato di tipo DWORD richiede un solo accesso in memoria, purché si trovi interamente contenuto in una quaterna di celle. In riferimento alla Figura 4.5, per accedere ad esempio alla DWORD contenuta nelle celle C0, C1, C2 e C3, la CPU usa le linee da D0 a D31; se però la DWORD si trova a cavallo tra due quaterne, sono necessari due accessi in memoria in quanto, come abbiamo appena visto, il Data Bus deve essere spostato da una quaterna all'altra.
Un modo semplice per evitare problemi del genere consiste nel disporre i dati di tipo DWORD ad indirizzi multipli interi di 4 (si parla allora di "allineamento alla DWORD"); in questo modo siamo sicuri che i dati stessi si troveranno sempre all'interno di una quaterna.

I requisiti di allineamento appena illustrati assumono particolare importanza per i segmenti di stack, i quali con la 80386 possono lavorare solo con dati di tipo WORD o DWORD.
Per uno stack destinato a dati di tipo WORD è vivamente raccomandato un allineamento alla WORD; in caso contrario, tutte le operazioni con PUSH o POP si troverebbero a lavorare con dati posizionati ad indirizzi dispari e in certi casi anche a cavallo tra due quaterne di celle.
Ancora più grave è il caso di uno stack destinato a dati di tipo DWORD, ma non allineato alla DWORD; in una situazione del genere, tutti i dati a 32 bit si troverebbero a cavallo tra due quaterne di celle e ogni operazione con PUSH o POP richiederebbe due accessi in memoria!
I blocchi di dati di Figura 4.3 possono essere utilizzati per contenere svariati tipi di informazioni, come numeri, stringhe e indirizzi. Nel tutorial Assembly Base è stato spiegato che una CPU lavora unicamente con due livelli di tensione elettrica, basso e alto, che possono essere associati ai simboli 0 e 1 del sistema di numerazione binario; la CPU stessa quindi non ha la più pallida idea di cosa sia un numero intero, un puntatore FAR, una lettera dell'alfabeto, etc. Sappiamo però che i circuiti logici della CPU sono concepiti per attribuire un preciso significato a determinate codifiche binarie; in questo modo è possibile simulare, ad esempio, i numeri interi e le operazioni matematiche su di essi, come addizioni, sottrazioni, etc.
Ogni CPU quindi è in grado di supportare in modo diretto determinati tipi di dati; quelli relativi alla 80386 sono illustrati nel seguito. La Figura 4.6 illustra gli estremi dei numeri interi senza segno (unsigned) rappresentabili dalla 80386 con i blocchi di dati di Figura 4.3. In Figura 4.7 vediamo gli estremi dei numeri interi con segno (signed) in complemento a 2, rappresentabili con i blocchi di dati di Figura 4.3; come sappiamo, con questo tipo di codifica, il MSB vale 0 per i numeri positivi (compreso lo zero) e 1 per i numeri negativi. Dato di tipo BYTE contenente nel nibble meno significativo una cifra esadecimale compresa tra 0h e 9h; il nibble più significativo deve valere 0h.
Una sequenza di BYTE di questo tipo permette di codificare numeri in base 10 attraverso numeri esadecimali, con notevoli benefici in termini di efficienza nei calcoli; ad esempio, il numero 3850 può essere codificato come:
03h 08h 05h 00h
Dato di tipo BYTE contenente in ogni nibble una cifra esadecimale compresa tra 0h e 9h.
Si tratta di una codifica più compatta del tipo Unpacked BCD; il numero precedente diventa:
38h 50h
Il "campo di bit" è una sequenza consecutiva e contigua di bit, che può iniziare da un bit qualunque di un BYTE in memoria; la sua ampiezza massima è pari a 32 bit. Dato formato da una sequenza di BYTE, WORD o DWORD, disposti in memoria in modo consecutivo e contiguo; l'ampiezza di una stringa può arrivare sino a 4 GiB.
Il contenuto di ogni elemento può rappresentare qualsiasi cosa; in particolare, le stringhe di BYTE sono spesso usate per contenere sequenze di codici ASCII. La "stringa di bit" è una sequenza consecutiva e contigua di bit, che può iniziare da un bit qualunque di un BYTE in memoria; la sua ampiezza massima può raggiungere 232 bit. La 80386 supporta i due tipi di indirizzo logico illustrati in Figura 4.8. Il NEAR POINTER è un offset a 32 bit all'interno di un segmento e può essere usato nel modello di memoria flat e in quello segmented; un puntatore NEAR sfrutta le associazioni predefinite con i registri di segmento.
Il FAR POINTER è un indirizzo logico a 48 bit formato da una componente Offset a 32 bit e una componente Selector a 16 bit; è destinato al modello di memoria segmented.
In base ad una nota convenzione adottata dalle CPU della famiglia 80x86, la componente Offset di un puntatore FAR deve precedere in memoria la componente Selector; posto, ad esempio: l'istruzione
les   ebx, [far_addr]
carica correttamente l'offset 00FAE1A4h in EBX e il selettore 0018h in ES.

4.1.4 Registri della 80386

Un altro aspetto di fondamentale importanza per la compatibilità verso il basso è quello relativo ai registri interni della CPU; analizziamo innanzi tutto i registri generali, di segmento, di stato e di istruzione della 80386.
Come si vede in Figura 4.9, la 80386 fornisce 8 registri generali a 32 bit, concepiti come prolungamento di quelli a 16 bit delle CPU 8086 e 80286. I registri generali vengono così chiamati in quanto possono essere impiegati per svolgere svariati compiti. Spesso ricoprono il ruolo di operandi delle istruzioni aritmetiche e logiche. Un altro uso frequente consiste nella gestione delle varie forme di indirizzamento; come viene illustrato nel seguito, una importante novità introdotta dalla 80386 è che tutti i registri generali possono essere impiegati per rappresentare indirizzi.
Alcune istruzioni utilizzano registri predefiniti per svolgere il proprio compito. EAX è l'accumulatore e ricopre quindi il ruolo di registro di riferimento in numerose operazioni. ECX è il registro contatore nei loop. ESI e EDI sono i registri sorgente e destinazione per le istruzioni che operano sulle stringhe. ESP e EBP sono destinati principalmente alla gestione dei segmenti di stack.

La Figura 4.9 mostra che la WORD meno significativa di ogni registro a 32 bit coincide con il corrispondente registro a 16 bit delle 8086 e 80286; a loro volta, AX, BX, CX e DX sono scomponibili nei due BYTE identificati da H (high) e L (low).
Tutte le istruzioni che utilizzano registri, locazioni di memoria e valori immediati a 8 o 16 bit, producono gli identici codici macchina delle 8086 e 80286; ciò permette alla 80386 di eseguire senza alcuna modifica anche i programmi a 16 bit.


La 80386 dispone di 6 registri di segmento a 16 bit, la cui parte "visibile" è illustrata in Figura 4.10; il loro scopo è quello di contenere in ogni istante i selettori dei segmenti di programma correntemente in uso dal task in esecuzione. I registri di segmento vengono largamente sfruttati nel modello di memoria segmented, dove ogni programma risulta distribuito in blocchi di codice, dati e stack. Un programma può definire numerosi segmenti, ma in un dato istante solo alcuni di essi risultano in uso; i segmenti correntemente in uso vengono individuati dalla CPU proprio attraverso i registri di Figura 4.10.

Il selettore presente nel registro CS indica quale segmento di codice è attualmente in esecuzione. L'offset contenuto nell'Instruction Pointer (EIP), illustrato più avanti, viene sommato automaticamente dalla CPU al Base Address del segmento associato a CS, in modo da ottenere l'indirizzo CS:EIP della prossima istruzione da eseguire.
La gestione di CS è riservata rigorosamente alla CPU; il contenuto di tale registro viene modificato implicitamente dalle istruzioni per il trasferimento del controllo (CALL, JMP, INT, RET, etc).

Il selettore presente nel registro SS indica quale segmento di stack è attualmente attivo per il task in esecuzione. Nei precedenti capitoli abbiamo visto che un task può avere a disposizione uno o più segmenti di stack; in particolare, un task che richiede l'esecuzione di codice più privilegiato, deve predisporre un distinto segmento di stack con quello stesso livello di privilegio. In un determinato momento solo uno stack è attivo; la CPU ricava questa informazione dal selettore correntemente caricato in SS.
Il registro ESP di Figura 4.9 indica in ogni istante la cima dello stack attivo (TOS); tale registro viene usato implicitamente dalle istruzioni come PUSH, POP, PUSHA, POPA, PUSHAD, etc.
Il registro EBP di Figura 4.9 è a disposizione dei programmi per l'accesso allo stack attivo; un suo uso molto frequente è quello di indirizzare i parametri e le variabili locali delle procedure.
Tutti gli indirizzamenti NEAR che usano come base ESP o EBP, vengono associati automaticamente dalla CPU a SS.

I restanti registri di Figura 4.10 sono destinati alla gestione dei segmenti di dati; come si può notare, la 80386 introduce i nuovi registri FS e GS, che aggiunti a DS e ES permettono ad un task di controllare in ogni momento sino a 4 segmenti di dati distinti.
DS (Data Segment) è il registro predefinito per i segmenti di dati. Tutti gli indirizzamenti NEAR che usano come base EAX, EBX, ECX, EDX, ESI o EDI, vengono associati automaticamente dalla CPU a DS; come sappiamo, possiamo aggirare queste regole ricorrendo al segment override.


La Figura 4.11 illustra la struttura dell'Instruction Pointer (EIP) e del Flags Register (EFLAGS) della 80386; entrambi questi registri sono le estensioni a 32 bit dei corrispondenti IP e FLAGS. In ogni istante, EIP contiene l'offset della prossima istruzione da eseguire, all'interno del segmento di codice puntato da CS; tale registro non è direttamente accessibile ai programmi in quanto il suo uso è riservato rigorosamente alla CPU.
Quando la 80386 esegue i programmi scritti per la 8086 o 80286, i 16 bit meno significativi di EIP equivalgono al registro IP di tali CPU; è fondamentale in questo caso che qualunque offset a 32 bit caricato in EIP abbia i 16 bit più significativi posti a 0.

Il registro EFLAGS in ogni istante contiene lo stato della 80386 e fornisce informazioni dettagliate sul risultato prodotto da varie operazioni; la Figura 4.12 mostra il significato dei vari campi. I bit colorati in grigio sono riservati e non devono essere modificati. I campi indicati con SY sono flags di sistema, quelli con ST sono flags di stato, mentre quelli con C sono flags di controllo.
I 16 bit meno significativi di EFLAGS hanno l'identico contenuto del registro FLAGS della 80286 (che, a sua volta, include tutti i campi destinati alla 8086); in questo modo, viene garantita ancora una volta la compatibilità verso il basso.
I 16 bit più significativi di EFLAGS contengono i due nuovi campi VM e RF, che saranno analizzati in dettaglio nei capitoli successivi. Per il momento è sufficiente sapere che RF, se posto a 1, permette il mascheramento selettivo di alcune eccezioni in fase di debugging; il flag VM, se posto a 1, fa in modo che la 80386 passi in modalità VM86.

4.1.5 Struttura delle istruzioni per la 80386

Quando la 80386 sfrutta pienamente la sua architettura a 32 bit, rende disponibili una serie di nuove funzionalità che comprendono, tra l'altro, operandi e indirizzamenti a 32 bit; tale modalità operativa viene definita modo 32 bit. Quando invece la 80386 opera in modalità di compatibilità con i programmi a 16 bit per le 8086 e 80286, si utilizza la definizione di modo 16 bit.
Per garantire la compatibilità con i programmi a 16 bit, le istruzioni della 80386 utilizzano un sistema di codifica che è una estensione di quello delle 8086 e 80286; la Figura 4.13 illustra tutti i dettagli. Rispetto alla struttura delle istruzioni per le 8086 e 80286, si nota la presenza di due nuovi prefissi opzionali, Address Size Prefix e Operand Size Prefix, che specificano la dimensione, 16 o 32 bit, degli indirizzamenti e degli operandi; come si può facilmente intuire, questi due prefissi svolgono un ruolo fondamentale nella compatibilità verso il basso della 80386.
L'effetto prodotto da questi due prefissi nel modo 16 bit è stato già illustrato nel Capitolo 11 del tutorial Assembly Base; si deve prestare però molta attenzione al fatto che il loro significato nel modo 32 bit è diametralmente opposto!

Nel modo 16 bit, la dimensione di default per gli operandi e gli indirizzi è 16 bit; si ha pertanto che: Nel modo 32 bit, la dimensione di default per gli operandi e gli indirizzi è 32 bit; si ha pertanto che: Sia nel modo 16 bit sia nel modo 32 bit, si può presentare il caso di istruzioni che usano operandi a 8 bit; come viene illustrato più avanti e come abbiamo già visto nel Capitolo 11 del tutorial Assembly Base, ferme restando le regole appena illustrate, tale caso viene gestito ponendo a 0 il bit w del campo Opcode di Figura 4.13.

La Figura 4.14 illustra l'elenco completo dei codici macchina (in binario e in esadecimale) di tutti i prefissi disponibili. Se si eccettuano i casi dei due nuovi prefissi e dei registri di segmento FS e GS, tutti i restanti codici macchina risultano identici a quelli delle 8086 e 80286.

Tornando alla Figura 4.13, vediamo che il campo Opcode (l'unico sempre presente) può occupare 1 o 2 byte; questo ampliamento è necessario in quanto l'unico byte del campo Opcode delle 8086 e 80286 non è più sufficiente per codificare tutte le nuove istruzioni disponibili con la 80386. Chiaramente, tutte le vecchie istruzioni vengono codificate con i soliti opcode da 1 byte; il secondo byte, invece, viene utilizzato solo per le nuove istruzioni introdotte dalla 80386.

Il campo Displacement può assumere una ampiezza di 32 bit necessaria per rappresentare anche gli offset a 32 bit disponibili con la 80386; osserviamo inoltre che con la 80386, il campo Immediate può contenere ovviamente anche valori numerici a 32 bit. Il campo mod_reg_r/m rimane inalterato; naturalmente, anche con la 80386 in certi casi i 3 bit del sottocampo reg si combinano con il campo Opcode.

Osserviamo infine in Figura 4.13 la presenza di un campo opzionale da 1 byte chiamato S.I.B.; tale campo viene utilizzato per le nuove forme di indirizzamento illustrate più avanti.

La Figura 4.15 mostra come vengono codificati i registri generali sulla 80386; il modo 16 bit e il modo 32 bit si riferiscono ai casi di default (assenza dell'Operand Size Prefix). Come si può notare, nel modo 16 bit della 80386 i codici macchina dei registri sono identici a quelli delle 8086 e 80286. Se l'Operand Size Prefix è assente, allora w=0 nell'Opcode indica operandi a 8 bit, mentre w=1 indica operandi a 16 bit; se, invece, l'Operand Size Prefix è presente, allora w=0 indica operandi a 8 bit, mentre w=1 indica operandi a 32 bit.
Nel modo 32 bit la situazione come abbiamo visto si inverte. Se l'Operand Size Prefix è assente, allora w=0 nell'Opcode indica operandi a 8 bit, mentre w=1 indica operandi a 32 bit; se, invece, l'Operand Size Prefix è presente, allora w=0 indica operandi a 8 bit, mentre w=1 indica operandi a 16 bit.

Come al solito, se l'operando di riferimento è di tipo Reg, allora il suo codice a 3 bit ricavato dalla Figura 4.15 viene sistemato nel sottocampo reg del campo mod_reg_r/m; se anche il secondo operando è di tipo Reg, allora mod=11b, mentre r/m contiene il codice a 3 bit del registro, ricavato sempre dalla Figura 4.15.

In Figura 4.16 vediamo come vengono codificati i registri di segmento sulla 80386. I 6 registri di segmento della 80386 vengono codificati con 3 bit, anziché i 2 usati dalle 8086 e 80286 per CS, DS, ES e SS. I primi 4 codici di Figura 4.16 hanno il bit più significativo che vale 0 e quindi coincidono con quelli delle 8086 e 80286.

Mettendo assieme i concetti appena esposti, nel modo 16 bit una istruzione come
add bx, ax
produce il codice macchina
00000001b 11000011b = 01h C3h
che è identico a quello delle 8086 e 80286 in quanto 16 bit è la dimensione di default degli operandi.

Nel caso invece dell'istruzione
add ebx, eax
si ottiene il codice macchina
01100110b 00000001b 11000011b = 66h 01h C3h
che è lo stesso di prima, preceduto però da 66h (Operand Size Prefix) in quanto gli operandi sono a 32 bit che non è la dimensione di default.

Nel modo 32 bit una istruzione come
add bx, ax
produce il codice macchina
01100110b 00000001b 11000011b = 66h 01h C3h
che è preceduto dall'Operand Size Prefix per indicare che la dimensione degli operandi è di 16 bit e non quella di default di 32 bit.

Nel caso invece dell'istruzione
add ebx, eax
si ottiene il codice macchina
00000001b 11000011b = 01h C3h
che è lo stesso di prima, senza però l'Operand Size Prefix in quanto gli operandi sono a 32 bit che è la dimensione di default.

4.1.6 Modalità di indirizzamento della 80386 - Effective Address

Se mod è diverso da 11b nel campo mod_reg_r/m, allora il secondo operando di una istruzione è di tipo Mem (locazione di memoria) o Imm (valore immediato); se il secondo operando è di tipo Mem, bisogna indicare alla CPU il modo per accedere alla corrispondente locazione di memoria.
Un indirizzo di memoria, come sappiamo, è formato da un selettore di segmento e da un offset all'interno del segmento stesso; nel caso più generale, un offset si suddivide nelle seguenti componenti: La componente BASE deve essere un registro e in genere rappresenta l'offset iniziale di un blocco di memoria.
La componente INDEX deve essere un registro e in genere rappresenta un offset (indice) da sommare a BASE per spostarsi all'interno di un blocco di memoria.
La componente DISPLACEMENT deve essere un valore immediato che viene trattato come numero intero con segno in complemento a 2; il suo scopo in genere è quello di rappresentare la posizione fissa di un elemento (ad esempio, un membro di una struttura), rispetto a INDEX.

In fase di decodifica, la CPU calcola
BASE + INDEX + DISPLACEMENT
e ottiene un cosiddetto Effective Address, che è l'offset assoluto della locazione di memoria a cui si vuole accedere.

Si tenga presente che un Effective Address non deve necessariamente comprendere tutte le componenti; sono permesse infatti le seguenti forme:
DISPLACEMENT
BASE
BASE + DISPLACEMENT
BASE + INDEX
BASE + INDEX + DISPLACEMENT

Le 8086 e 80286 rendono disponibili 24 modalità di indirizzamento differenti per accedere ad una locazione di memoria; tali modalità sono illustrate dalla Figura 11.8 del Capitolo 11, tutorial Assembly Base.
La stessa Figura 11.8 mostra che solamente BX e BP possono svolgere il ruolo di BASE; solamente SI e DI possono svolgere il ruolo di INDEX. Non è consentita la combinazione [BX+BP] o [BP+BX] ed è proibito l'uso di SP.
In presenza di BX, il registro di segmento predefinito è DS; in presenza di BP, il registro di segmento predefinito è SS, indipendentemente dall'ordine dei registri (ad esempio, [BP+DI] è del tutto equivalente a [DI+BP]).

Nel modo 16 bit, la 80386 supporta tutte le 24 modalità di indirizzamento delle 8086 e 80286, con gli identici codici macchina; anche le regole da applicare per combinare BASE, INDEX e DISPLACEMENT sono le stesse.
Nel modo 32 bit, la 80386 fornisce ulteriori 21 modalità di indirizzamento (senza S.I.B. byte), illustrate in Figura 4.17. Notiamo subito che nel modo 32 bit, qualunque registro generale a 32 bit (ad eccezione di ESP) può essere usato come puntatore. In presenza di EAX, EBX, ECX, EDX, ESI o EDI, il registro di segmento predefinito è DS; in presenza di EBP, il registro di segmento predefinito è SS.

Alcune combinazioni tra mod e r/m, visibili in Figura 4.17, indicano la presenza del campo S.I.B. nel codice macchina di una istruzione; in tal caso, si ha a che fare con un Effective Address del tipo Scale Index Base.
Un indirizzamento di tipo S.I.B., nel caso più generale è formato dalle seguenti componenti: La novità, rispetto agli indirizzamenti delle 8086 e 80286, è la presenza del campo SCALE (fattore di scala) che permette di moltiplicare INDEX per 1, 2, 4 o 8; la CPU ottiene l'Effective Address attraverso il calcolo:
BASE + (INDEX * SCALE) + DISPLACEMENT
La moltiplicazione ovviamente viene tradotta (via hardware) in uno shift a sinistra di, rispettivamente, 0, 1, 2 o 3 posizioni dei bit di INDEX. In assenza del fattore di scala, si ha implicitamente SCALE=1.

In Figura 4.17 possiamo notare che la presenza del S.I.B. byte viene indicata da r/m=100b e mod che può valere 00b, 01b o 10b; in una situazione del genere, la CPU sa che dopo il campo mod_reg_r/m è presente il campo S.I.B., il quale assume la struttura mostrata in Figura 4.18. Il sottocampo scale è formato da 2 bit (posizioni dalla 6 alla 7) e codifica il fattore di scala; le 4 possibili codifiche del fattore di scala vengono mostrate in Figura 4.19. Il sottocampo index di Figura 4.18 occupa 3 bit (posizioni dalla 3 alla 5) e codifica il registro a 32 bit che svolge il ruolo di INDEX; il sottocampo base di Figura 4.18 occupa 3 bit (posizioni dalla 0 alla 2) e codifica il registro a 32 bit che svolge il ruolo di BASE. Questi codici a 3 bit possono essere ricavati dalla quarta e quinta colonna della Figura 4.15.
Per ogni valore del sottocampo mod (00b, 01b o 10b) sono previste 8 differenti modalità di indirizzamento; in totale abbiamo quindi 3*8=24 modalità che vengono illustrate dalla Figura 4.20. Il termine "(indice scalato)" indica simbolicamente il prodotto tra un registro indice e un fattore di scala come, ad esempio, (ESI*8). Osserviamo subito che questa volta è permessa anche la presenza del registro puntatore ESP, che però può svolgere esclusivamente il ruolo di registro base; è proibito quindi scrivere indirizzi del tipo [EAX+ESP+02h]. Dalle considerazioni appena esposte risulta quindi che nel modo 32 bit possiamo scrivere istruzioni del tipo:
mov ebx, [eax+(edx*4)+000003F8h]
Queste forme di indirizzamento sono molto utili per accedere a strutture dati complesse; supponiamo, ad esempio, di avere a che fare con un vettore i cui elementi sono strutture del tipo: Osserviamo che la struttura ha una dimensione di 8 byte, per cui ci serve un fattore di scala pari a 8 per spostarci tra i vari elementi. Ponendo allora in EAX l'offset iniziale del vettore e in EBX l'indice dell'elemento che ci interessa, se vogliamo caricare in DX il campo varSt3 (che è in posizione 2 nella struttura) possiamo scrivere:
mov dx, [eax+(ebx*8)+2]
Se i fattori di scala 2, 4 o 8 non sono adeguati alle nostre esigenze, possiamo servirci del campo DISPLACEMENT per impostare lo spiazzamento desiderato.


Come è stato spiegato in precedenza, un Effective Address non deve necessariamente comprendere tutte le componenti; sono permesse infatti le seguenti forme:
DISPLACEMENT
BASE
BASE + DISPLACEMENT
BASE + INDEX
(INDEX * SCALE) + DISPLACEMENT
BASE + INDEX + DISPLACEMENT
BASE + (INDEX * SCALE) + DISPLACEMENT

4.1.7 Nuove istruzioni della 80386

Le istruzioni fornite dalle CPU che supportano anche la modalità protetta, possono essere suddivise in due grandi categorie: Le istruzioni per le applicazioni sono quelle destinate alla scrittura dei normali programmi (ad esempio, MOV, ADD, SHL, PUSH, etc); le istruzioni di sistema sono quelle destinate alla scrittura dei sistemi operativi (ad esempio, LGDT, LTR, ARPL, etc).

In questa sezione esaminiamo le nuove istruzioni fornite dalla 80386 per la scrittura dei normali programmi; per maggiori dettagli si consiglia di consultare il tutorial Assembly Base o i manuali della CPU. Nel seguito indichiamo come al solito con SRC l'operando sorgente e con DEST l'operando destinazione.
Questa istruzione scandisce SRC alla ricerca del primo bit che vale 1; la scansione avviene in avanti (forward) a partire dal bit in posizione 0 di SRC. Se la ricerca fallisce (SRC=0), si ha ZF=1, mentre il contenuto di DEST è indefinito. Se la ricerca ha successo, si ha ZF=0, mentre in DEST viene salvato l'indice del bit trovato.
Questa istruzione scandisce SRC alla ricerca del primo bit che vale 1; la scansione avviene all'indietro (reverse) a partire dal bit più significativo di SRC. Se la ricerca fallisce (SRC=0), si ha ZF=1, mentre il contenuto di DEST è indefinito. Se la ricerca ha successo, si ha ZF=0, mentre in DEST viene salvato l'indice del bit trovato (riferito al bit più significativo di SRC).
Premettiamo che, con il termine bitBase si indica il bit in posizione 0 di un operando Reg o di una locazione di memoria (Mem); con il termine bitOffset si indica un numero che rappresenta la distanza in bit tra il bitBase e il bit che vogliamo esaminare.

L'istruzione BT legge il bitOffset da SRC e lo somma al bitBase di DEST, in modo da ottenere la posizione del bit da esaminare; il valore di tale bit viene copiato in CF (Carry Flag). Ad esempio:
bt   edx, 7
copia in CF il valore del bit in posizione 7 di EDX.

Se DEST è un Reg, bitOffset viene calcolato modulo n, dove n è la dimensione in bit (16 o 32) del Reg; ciò è necessario per non eccedere l'ampiezza in bit di un registro generale.
Se DEST è un Mem16, il bit da copiare in CF si trova alla distanza bitOffset dall'indirizzo iniziale:
Effective Address + (2 ∗ (bitOffset DIV 16))
Se DEST è un Mem32, il bit da copiare in CF si trova alla distanza bitOffset dall'indirizzo iniziale:
Effective Address + (4 ∗ (bitOffset DIV 32))
(DIV indica la divisione intera).
L'istruzione BTC legge il bitOffset da SRC e lo somma al bitBase di DEST, in modo da ottenere la posizione del bit da esaminare; il valore di tale bit viene sottoposto a complemento a 1 (inversione), dopo che l'originale era stato salvato in CF.
Valgono tutte le considerazioni esposte per BT in relazione a bitBase e bitOffset.
L'istruzione BTR legge il bitOffset da SRC e lo somma al bitBase di DEST, in modo da ottenere la posizione del bit da esaminare; il valore di tale bit viene azzerato, dopo che l'originale era stato salvato in CF.
Valgono tutte le considerazioni esposte per BT in relazione a bitBase e bitOffset.
L'istruzione BTS legge il bitOffset da SRC e lo somma al bitBase di DEST, in modo da ottenere la posizione del bit da esaminare; il valore di tale bit viene posto a 1, dopo che l'originale era stato salvato in CF.
Valgono tutte le considerazioni esposte per BT in relazione a bitBase e bitOffset.
Questa istruzione tratta il contenuto di AX come intero con segno a 16 bit in complemento a 2 e lo converte in una DWORD mediante estensione del bit di segno; il risultato viene salvato in EAX.
Si tenga presente che l'analoga istruzione CWD svolge lo stesso compito, ma salva il risultato in DX:AX.
Questa istruzione tratta il contenuto di EAX come intero con segno a 32 bit in complemento a 2 e lo converte in una QWORD mediante estensione del bit di segno; il risultato viene salvato in EDX:EAX.
Questa istruzione legge un indirizzo dalla memoria e lo copia nei registri indicati.
Se DEST è un Reg16, allora SRC deve essere un indirizzo di tipo Seg:Offset da 16+16 bit; la componente Offset viene copiata in DEST, mentre la componente Seg viene copiata in FS.
Se DEST è un Reg32, allora SRC deve essere un indirizzo di tipo Seg:Offset da 16+32 bit; la componente Offset viene copiata in DEST, mentre la componente Seg viene copiata in FS.

Bisogna ribadire che questo tipo di istruzione funziona correttamente solo se la componente Offset in memoria precede Seg.
Questa istruzione è del tutto analoga a LFS; la componente Seg viene copiata in GS.
Questa istruzione è del tutto analoga a LFS; la componente Seg viene copiata in SS.
Questa istruzione legge il contenuto di SRC, lo estende con zeri a sinistra e salva il risultato in DEST.
Questa istruzione legge il contenuto di SRC, lo estende con il bit di segno a sinistra e salva il risultato in DEST.
Queste istruzioni modificano l'unico operando DEST in base allo stato assunto dai flags in seguito ad una operazione logico aritmetica appena eseguita; se la condizione richiesta è verificata, si ha DEST=1, mentre in caso contrario si ha DEST=0.
Come si può notare, alcune forme di SETcc fanno esplicito riferimento allo stato dei flags ("if overflow", "if not overflow", "if sign", etc). Per le altre forme di SETcc il riferimento ai flags è implicito ("if above", "if below", "if not greater or equal", etc); in tal caso bisogna distinguere tra numeri interi senza segno o con segno. Come sappiamo, "above" e "below" trattano gli operandi come numeri interi senza segno; viceversa, "greater" e "less" trattano gli operandi come numeri interi con segno. Questa distinzione non è necessaria nel caso di "zero", "not zero", "equal" e "not equal".
Questa istruzione richiede i tre operandi SRC, DEST e COUNT (contatore). COUNT (terzo operando) indica di quante posizioni bisogna far scorrere a sinistra i bit di DEST (primo operando); i posti che si liberano a destra in DEST vengono riempiti con altrettanti bit fatti uscire dalla sinistra di SRC (secondo operando). Il contenuto di SRC rimane inalterato.
Vengono presi in considerazione solo i primi 5 bit di COUNT, in modo da ottenere un numero di scorrimenti compreso tra 0 e 31. Per ogni singolo scorrimento, il bit che trabocca dalla sinistra di DEST viene salvato in CF.
Questa istruzione richiede i tre operandi SRC, DEST e COUNT (contatore). COUNT (terzo operando) indica di quante posizioni bisogna far scorrere a destra i bit di DEST (primo operando); i posti che si liberano a sinistra in DEST vengono riempiti con altrettanti bit fatti uscire dalla destra di SRC (secondo operando). Il contenuto di SRC rimane inalterato.
Vengono presi in considerazione solo i primi 5 bit di COUNT, in modo da ottenere un numero di scorrimenti compreso tra 0 e 31. Per ogni singolo scorrimento, il bit che trabocca dalla destra di DEST viene salvato in CF.

4.1.8 Istruzioni che operano implicitamente su dati a 32 bit

Oltre alle nuove istruzioni appena illustrate, la 80386 supporta anche le varianti a 32 bit delle vecchie istruzioni per 8086 e 80286; in tali varianti, è implicito il riferimento a blocchi di dati da 32 bit.

Le istruzioni per la manipolazione delle stringhe, STOSD, LODSD, MOVSD, SCASD, CMPSD, operano su dati di tipo DWORD; ad esempio:
movsd
legge una DWORD da DS:SI e la copia in ES:DI.

Le istruzioni per l'I/O con le porte hardware, INSD e OUTSD fanno riferimento implicitamente ad un dato di tipo DWORD; ad esempio:
insd
legge una DWORD dalla porta hardware indicata da DX e la copia in ES:DI.

Al termine di una ISR, nel modo 16 bit l'istruzione IRET come sappiamo estrae dallo stack due WORD per l'indirizzo di ritorno CS:IP e una WORD per FLAGS; nel modo 32 bit le istruzioni IRET e IRETD sono equivalenti e estraggono dallo stack una WORD e una DWORD per CS:EIP e una DWORD per EFLAGS.

L'istruzione PUSHAD salva nello stack il contenuto di EAX, ECX, EDX, EBX, ESP originale, EBP, ESI e EDI; l'istruzione POPAD estrae dallo stack 8 DWORD e le carica negli stessi registri in ordine inverso.

L'istruzione PUSHFD salva nello stack il contenuto di EFLAGS; l'istruzione POPFD estrae una DWORD dallo stack e la carica in EFLAGS.

4.1.9 Indirizzamenti nel modo 16 bit e 32 bit

Abbiamo visto in precedenza che una istruzione come MOVSD, legge una DWORD da DS:SI e la copia in ES:DI.

Questo è ciò che accade quando la 80386 opera nel modo 16 bit; in tale modalità operativa, i segmenti di programma non possono superare la dimensione di 64 KiB, per cui le componenti offset devono essere comprese tra 0000h e FFFFh.

Quando però la 80386 opera nel modo 32 bit, la precedente istruzione legge una DWORD da DS:ESI e la copia in ES:EDI; in questo caso, è possibile lavorare con segmenti virtuali da 4 GiB, per cui le componenti offset possono variare da 00000000h a FFFFFFFFh.

Questi aspetti influiscono anche sul comportamento delle varie istruzioni. Quando la 80386 si trova nel modo 16 bit e incontra, ad esempio, una istruzione RETF, estrae dallo stack due WORD per l'indirizzo di ritorno e le carica in CS:IP; i 16 bit più significativi di EIP vengono azzerati.
Nel modo 32 bit, invece, vengono estratte dallo stack una WORD e una DWORD per l'indirizzo di ritorno; tali dati vengono caricati in CS:EIP.
Un altro esempio può essere l'istruzione LOOP; nel modo 16 bit il contatore è CX, mentre nel modo 32 bit viene usato ECX.

Per indicare agli assembler come MASM e NASM quale modo (16 o 32 bit) vogliamo utilizzare, possiamo servirci dell'apposito attributo "Dimensione" per i segmenti di programma; come sappiamo, tale attributo può essere USE16 o USE32.

In presenza di un segmento di codice del tipo:
CODESEGM SEGMENT ALIGN=16 PUBLIC USE16 CLASS=CODE
l'assembler provvede a tradurre le istruzioni nel codice macchina relativo al modo 16 bit; eventuali istruzioni con operandi o indirizzamenti a 32 bit, vengono precedute dagli appositi prefissi illustrati nella sezione 4.1.5.

Se invece il segmento di codice viene definito come:
CODESEGM SEGMENT ALIGN=16 PUBLIC USE32 CLASS=CODE
l'assembler provvede a tradurre le istruzioni nel codice macchina relativo al modo 32 bit; eventuali istruzioni con operandi o indirizzamenti a 16 bit, vengono precedute dagli appositi prefissi illustrati nella sezione 4.1.5.

Più avanti vedremo che in modalità protetta, la 80386 utilizza un apposito bit nel descrittore di segmento per indicare la dimensione di default degli operandi e degli indirizzamenti.

4.2 Programmazione di sistema con la 80386

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.

4.2.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 4.21 illustra la struttura di un descrittore di segmento. 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. 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 4.21, 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 4.22. 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 4.23. 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 4.24. 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 4.25 illustra tutti i dettagli. 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). 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). Se vogliamo conoscere il contenuto del GDTR, dobbiamo usare l'istruzione di sistema SGDT (Store Global Descriptor Table Register). 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.

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 4.26. 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 4.27 illustra tutti i dettagli. 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). 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). 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 4.28. 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. La Figura 4.29 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. 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.

4.2.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 4.30. 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 4.30.

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 disabilitando funzionalità come la paginazione della memoria.

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 4.30, tranne per il campo LIMIT che deve specificare l'offset massimo per i vari blocchi di codice, dati e stack, come in Figura 4.29; 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.

4.2.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 4.31). La paginazione della memoria può essere attivata o disattivata tramite il registro CR0, la cui struttura è illustrata in Figura 4.32. 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.

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 4.33. 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 4.34. 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 4.35. 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 4.35 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 4.36. Il bit A (Accessed) di Figura 4.35 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 4.37, 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. 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 4.37 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.
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)