Intel x86 Assembly Language & Microarchitecture
Modalità reali vs protette
Ricerca…
Modalità reale
Quando Intel ha progettato l'originale x86, 8086 (e derivato 8088), hanno incluso la segmentazione per consentire al processore a 16 bit di accedere a più di 16 bit di indirizzo. Lo hanno fatto rendendo gli indirizzi a 16 bit relativi a un dato registro di segmenti a 16 bit, di cui hanno definito quattro: Segmento di codice ( CS ), Segmento di dati ( DS ), Segmento extra ( ES ) e Segmento di stack ( SS ) .
La maggior parte delle istruzioni implicava il registro dei segmenti da utilizzare: le istruzioni erano corrette dal segmento di codice, PUSH e POP implicavano il segmento di stack e semplici riferimenti di dati implicavano il segmento di dati, anche se questo poteva essere ignorato per accedere alla memoria in uno qualsiasi degli altri segmenti.
L'implementazione era semplice: per ogni accesso alla memoria, la CPU avrebbe preso il registro dei segmenti implicito (o esplicito), spostandolo di quattro posizioni a sinistra, quindi aggiungere l'indirizzo indicato:
+-------------------+---------+
Segment | 16-bit value | 0 0 0 0 |
+-------------------+---------+
PLUS
+---------+-------------------+
Address | 0 0 0 0 | 16-bit value |
+---------+-------------------+
EQUALS
+-----------------------------+
Result | 20-bit memory address |
+-----------------------------+
Questo ha permesso varie tecniche:
- Permettendo a tutti codice, dati e pila di essere reciprocamente accessibili (
CS,DSeSSavevano tutti lo stesso valore); - Mantenendo il codice, i dati e lo stack completamente separati l'uno dall'altro (
CS,DSeSStutti i 4K (o più) separati l'uno dall'altro - ricorda che viene moltiplicato per 16, quindi questo è 64K).
Permetteva anche sovrapposizioni bizzarre e ogni sorta di cose strane!
Quando è stato inventato l'80286, ha supportato questa modalità legacy (ora chiamata "Modalità reale"), ma ha aggiunto una nuova modalità chiamata "Modalità protetta" (qv).
Le cose importanti da notare è che in modalità reale:
- Qualsiasi indirizzo di memoria era accessibile, semplicemente inserendo il valore corretto all'interno di un Segment Register e accedendo all'indirizzo a 16 bit;
- L'estensione della "protezione" era quella di consentire al programmatore di separare diverse aree di memoria per scopi diversi e rendere più difficile scrivere accidentalmente dati errati, pur continuando a farlo.
In altre parole ... non molto protetto!
Modalità protetta
introduzione
Quando è stato inventato l'80286, ha supportato la legacy 8086 Segmentation (ora chiamata "Modalità reale") e ha aggiunto una nuova modalità chiamata "Modalità protetta". Questa modalità è presente in tutti i processori x86, anche se migliorata con vari miglioramenti come l'indirizzamento a 32 e 64 bit.
Design
In modalità protetta, il semplice "Aggiungi indirizzo al valore di registro del segmento spostato" è stato eliminato completamente. Conservavano i registri dei segmenti, ma invece di usarli per calcolare un indirizzo, li usavano per indicizzare in una tabella (in realtà, uno dei due ...) che definiva il segmento a cui accedere. Questa definizione non solo descriveva dove era in memoria il Segmento (usando Base e Limite), ma anche quale tipo di Segmento era (Codice, Dati, Stack o Sistema) e quali tipi di programmi potevano accedervi (OS Kernel, programma normale , Device Driver, ecc.).
Registro dei segmenti
Ogni registro dei segmenti a 16 bit ha assunto il seguente formato:
+------------+-----+------+
| Desc Index | G/L | Priv |
+------------+-----+------+
Desc Index = 13-bit index into a Descriptor Table (described below)
G/L = 1-bit flag for which Descriptor Table to Index: Global or Local
Priv = 2-bit field defining the Privilege level for access
Globale / Locale
Il bit Global / Local ha definito se l'accesso fosse in una tabella globale di descrittori (chiamata, senza sorprese, la tabella descrittiva globale o GDT) o una tabella descrittore locale (LDT). L'idea per il LDT era che ogni programma potesse avere una propria tabella descrittiva - il sistema operativo definiva un insieme globale di segmenti e ogni programma avrebbe il proprio insieme di segmenti di codice locale, dati e stack. Il sistema operativo gestiva la memoria tra le diverse tabelle descrittive.
Tabella dei descrittori
Ciascuna tabella descrittiva (globale o locale) era una matrice di 64K di 8,192 descrittori: ognuno un record di 8 byte che definiva più aspetti del segmento che stava descrivendo. I campi dell'indice dei descrittori del segmento Registri consentiti per 8.192 descrittori: non a caso!
descrittore
Un descrittore conteneva le seguenti informazioni: si noti che il formato del descrittore cambiava quando venivano rilasciati nuovi processori, ma lo stesso tipo di informazione era conservato in ciascuno di essi:
- Base
Questo ha definito l'indirizzo iniziale del segmento di memoria. - Limite
Questo ha definito la dimensione del segmento di memoria, una sorta di. Hanno dovuto prendere una decisione: una dimensione di0x0000significa una dimensione di0, quindi non accessibile? O la dimensione massima?
Invece hanno scelto una terza opzione: il campo Limite era l'ultima posizione indirizzabile all'interno del Segmento. Ciò significava che un segmento one-bye poteva essere definito; o uno di dimensioni massime per la dimensione dell'indirizzo. - genere
Esistono più tipi di segmenti: il codice tradizionale, i dati e lo stack (vedi sotto), ma sono stati definiti anche altri segmenti di sistema:- I segmenti della tabella dei descrittori locali hanno definito il numero di descrittori locali a cui è possibile accedere;
- I segmenti dello stato delle attività potrebbero essere utilizzati per la commutazione del contesto gestita dall'hardware;
- Controllato "Call Gates" che potrebbe consentire ai programmi di chiamare nel sistema operativo - ma solo attraverso punti di ingresso gestiti con cura.
- attributi
Sono stati mantenuti anche alcuni attributi del segmento, se pertinenti:- Sola lettura vs Lettura-Scrittura;
- Indipendentemente dal fatto che il segmento fosse presente o meno, consentendo la gestione della memoria su richiesta;
- Quale livello di codice (OS vs Driver vs programma) potrebbe accedere a questo segmento.
Vera protezione finalmente!
Se il sistema operativo manteneva le tabelle descrittive in segmenti a cui non era possibile accedere da semplici programmi, allora poteva gestire strettamente quali segmenti erano stati definiti e quale memoria era stata assegnata e accessibile a ciascuno di essi. Un programma poteva produrre qualunque valore del registro dei segmenti che gli piaceva - ma se avesse avuto l' audacia di caricarlo effettivamente in un registro dei segmenti ! ... l'hardware della CPU riconoscerebbe che il valore del descrittore proposto rompeva un gran numero di regole, e invece di completare la richiesta, genererebbe un'eccezione al sistema operativo per consentirgli di gestire il programma errante.
Questa eccezione era solitamente la # 13, l'eccezione di protezione generale, resa famosa nel mondo da Microsoft Windows ... (Qualcuno pensa che un ingegnere Intel sia superstizioso?)
Errori
I tipi di errori che potrebbero accadere includevano:
Se l'indice descrittore proposto era più grande della dimensione della tabella;
Se il descrittore proposto era un descrittore di sistema piuttosto che codice, dati o pila;
Se il descrittore proposto era più privilegiato del programma richiedente;
Se il descrittore proposto è stato contrassegnato come Non leggibile (come un segmento di codice), ma si è tentato di essere letto piuttosto che eseguito;
Se il descrittore proposto è stato contrassegnato come Non presente.
Si noti che l'ultimo potrebbe non essere un problema fatale per il programma: il sistema operativo potrebbe notare la bandiera, ripristinare il segmento, contrassegnarlo come ora Presente quindi consentire all'istruzione di errore di procedere correttamente.
Oppure, forse il Descrittore è stato caricato con successo in un Segment Register, ma successivamente un accesso futuro ha infranto una delle numerose regole:
- Il registro dei segmenti è stato caricato con l'indice del descrittore
0x0000per il GDT. Questo è stato prenotato dall'hardware comeNULL; - Se il descrittore caricato è stato contrassegnato in sola lettura, è stata tentata una scrittura.
- Se una parte dell'accesso (1, 2, 4 o più byte) era al di fuori del limite del segmento.
Passaggio in modalità protetta
Passare alla modalità protetta è semplice: basta impostare un singolo bit in un registro di controllo. Ma rimanendo in modalità protetta, senza la CPU che alza le mani e si reimposta per non sapere cosa fare dopo, richiede molta preparazione.
In breve, i passaggi richiesti sono i seguenti:
È necessario impostare un'area di memoria per la tabella dei descrittori globali per definire un minimo di tre descrittori:
- Il zeroeth,
NULLDescriptor; - Un altro descrittore per un segmento di codice;
- Un altro descrittore per un segmento di dati.
Questo può essere usato sia per i dati che per lo stack.
- Il zeroeth,
Il Global Descriptor Table Register (
GDTR) deve essere inizializzato per indicare questa area di memoria definita;GDT_Ptr dw SIZE GDT dd OFFSET GDT ... lgdt [GDT_Ptr]Il bit
PMinCR0deve essere impostato:mov eax, cr0 ; Get CR0 into register or eax, 0x01 ; Set the Protected Mode bit mov cr0, eax ; We're now in Protected Mode!I registri dei segmenti devono essere caricati dal GDT per rimuovere i valori correnti della modalità reale:
jmp 0x0008:NowInPM ; This is a FAR Jump. 0x0008 is the Code Descriptor NowInPM: mov ax, 0x0010 ; This is the Data Descriptor mov ds, ax mov es, ax mov ss, ax mov sp, 0x0000 ; Top of stack!
Si noti che questo è il minimo indispensabile , solo per portare la CPU in modalità protetta. Per ottenere l'intero sistema pronto può richiedere molti più passaggi. Per esempio:
- Potrebbe essere necessario abilitare le aree di memoria superiore - disattivando la porta
A20; - Gli interrupt dovrebbero essere sicuramente disabilitati - ma forse i vari Fault Handler potrebbero essere impostati prima di entrare in modalità protetta, per consentire errori all'inizio dell'elaborazione.
L'autore originale di questa sezione ha scritto un intero tutorial sull'inserimento della Modalità protetta e sul suo utilizzo.
Modalità irreale
La modalità irreale sfrutta due fatti su come entrambi i processori Intel e AMD caricano e salvano le informazioni per descrivere un segmento.
Il processore memorizza nella cache le informazioni sul descrittore recuperate durante uno spostamento in un registro di selezione in modalità protetta.
Queste informazioni sono memorizzate in una parte architettonica invisibile del registro di selezione.Nella modalità reale i registri selettori sono chiamati registri di segmento ma, a parte questo, designano lo stesso insieme di registri e come tali hanno anche una parte invisibile. Queste parti sono riempite con valori fissi, ma per la base che è derivata dal valore appena caricato.
In tale vista, la modalità reale è solo un caso speciale di modalità protetta: in cui le informazioni di un segmento, come la base e il limite, vengono recuperate senza un GDT / LDT ma vengono comunque lette dalla parte nascosta del registro dei segmenti.
Passando in modalità protetta e creando un GDT è possibile creare un segmento con gli attributi desiderati, ad esempio una base di 0 e un limite di 4GiB.
Attraverso un caricamento successivo di un registro di selezione tali attributi vengono memorizzati nella cache, è quindi possibile tornare in modalità reale e disporre di un registro di segmenti attraverso il quale accedere all'intero spazio di indirizzamento a 32 bit.
BITS 16
jmp 7c0h:__START__
__START__:
push cs
pop ds
push ds
pop ss
xor sp, sp
lgdt [GDT] ;Set the GDTR register
cli ;We don't have an IDT set, we can't handle interrupts
;Entering protected mode
mov eax, cr0
or ax, 01h ;Set bit PE (bit 0) of CR0
mov cr0, eax ;Apply
;We are now in Protected mode
mov bx, 08h ;Selector to use, RPL = 0, Table = 0 (GDT), Index = 1
mov fs, bx ;Load FS with descriptor 1 info
mov gs, bx ;Load GS with descriptor 1 info
;Exit protected mode
and ax, 0fffeh ;Clear bit PE (bit0) of CR0
mov cr0, eax ;Apply
sti
;Back to real mode
;Do nothing
cli
hlt
GDT:
;First entry, number 0
;Null descriptor
;Used to store a m16&32 object that tells the GDT start and size
dw 0fh ;Size in byte -1 of the GDT (2 descriptors = 16 bytes)
dd GDT + 7c00h ;Linear address of GDT start (24 bits)
dw 00h ;Pad
dd 0000ffffh ;Base[15:00] = 0, Limit[15:00] = 0ffffh
dd 00cf9200h ;Base[31:24] = 0, G = 1, B = 1, Limit[19:16] = 0fh,
;P = 1, DPL = 0, E = 0, W = 1, A = 0, Base[23:16] = 00h
TIMES 510-($-$$) db 00h
dw 0aa55h
considerazioni
- Non appena viene ricaricato un registro di segmento, anche con lo stesso valore, il processore ricarica gli attributi nascosti in base alla modalità corrente. Questo è il motivo per cui il codice sopra usa
fsegsper contenere i segmenti "estesi": tali registri hanno meno probabilità di essere usati / salvati / ripristinati dai vari servizi a 16 bit. - L'istruzione
lgdtnon carica un puntatore lontano al GDT, ma carica un indirizzo lineare a 24 bit (può essere ignorato a 32 bit). Questo non è un indirizzo vicino , è l' indirizzo fisico (dal momento che il paging deve essere disabilitato). Questa è la ragione diGDT+7c00h. - Il programma sopra riportato è un bootloader (per MBR, non ha BPB) che imposta
cs/ds/sstp 7c00h e avvia il contatore di posizione da 0. Quindi un byte all'offset X nel file è a offset X nel segmento 7c00h e all'indirizzo lineare 7c00h + X. - Gli interrupt devono essere disabilitati in quanto non è impostato un IDT per il breve round trip in modalità protetta.
- Il codice usa un hack per salvare 6 byte di codice. La struttura caricata da
lgdtviene salvata nel ... GDT stesso, nel descrittore null (il primo descrittore).
Per una descrizione dei descrittori GDT, consultare il Capitolo 3.4.3 di Intel Manual Volume 3A .