Le basi della protected mode
Come eseguire lo switch real mode - protected mode

Data

by Albe

 

07/02/2003

UIC's Home Page

Published by Quequero



Complimenti albe il tute e' eccellente ed estremamente interessante, bravo davvero

 

....

Home page se presente: http://www.albe.tk
E-mail: albe@bbk.org
Nick, UIN, canale IRC/EFnet frequentato

....

Difficoltà

( )NewBies (x)Intermedio ( )Avanzato ( )Master

 

In questo tutorial cercherò di spiegare i concetti fondamentali della modalità protetta dei processori 386+. Vedremo anche come scrivere un semplice programma che, all'avvio del PC, esegue lo switch da real mode a protected mode. Il programma non gira sotto nessun sistema operativo, ma deve essere scritto su un floppy e lanciato all'avvio del PC: per semplificare questo processo, useremo Bochs, che è un emulatore di PC.


Le basi della protected mode
Come eseguire lo switch real mode - protected mode
Written by Tuo nome

Introduzione

Diverse modalità

La "protected mode" è una delle modalità di funzionamento dei processori Intel; esiste fin dal lontano 286, ma è dal 386 che ha assunto la forma che oggi conosciamo, e con cui oggi operano i nostri Pentium4 o Athlon. E' caratterizzata da uno spazio di indirizzamento a 32 bit, e dalla presenza di numerosi strumenti per l'uso della memoria virtuale e protetta (se non conoscete questi due termini, non preoccupatevi: dopo tenterò di spiegarli).
Nonostante essa sia la modalità nativa dei processori 386+, quando una tale CPU viene resettata (ovvero, quando accendiamo / riavviamo il nostro PC) essa non opera da subito in protected mode: la modalità iniziale è la "real mode", che ha uno spazio di indirizzamento a 20 bit, e non possiede nessun meccanismo per la protezione e virtualizzazione della memoria.

Perchè succede questo? Non era più semplice che la CPU partisse direttamente in protected mode, visto che poi qualunque OS userà quest'ultima?
Esistono diverse ragioni per le quali è necessario che la CPU parta in modalità reale e non protetta.
Prima di tutto, c'è il discorso della compatibilità all'indietro: la real mode è la modalità di funzionamento dell'8086, e quindi, per garantire il funzionamento del software scritto per questa CPU anche sui processori a 32 bit, è necessario partire emulando l'8086. Prendiamo per esempio l'MS-DOS, che funziona in modalità reale: se un 386+ si avviasse direttamente in modalità protetta, non ci sarebbe verso di farci funzionare il DOS (a meno che non si scriva un software apposta per passare dalla modalità protetta alla modalità reale). Per inciso, la compatibilità all'indietro è ciò che ha caratterizzato lo sviluppo dei PC fin dalla loro nascita, e ne ha determinato una crescita non sempre ordinata e "pulita".
Un'altra ragione per la quale è necessaria una partenza in real mode consiste nel fatto che il settaggio della modalità protetta non è facile ed immediato: come vedremo, esistono alcune strutture da riempire, ed è possibile scegliere tra diverse opzioni di protezione e virtualizzazione della memoria. Non possiamo (e non vogliamo) aspettarci che la CPU faccia tutto questo da sola: dobbiamo essere noi ad eseguire lo switch dalla semplice real mode alla più elaborata protected mode, in modo da poter scegliere le opzioni che più si adattano alle nostre esigenze.


Caratteristiche della protected mode

Perchè i sistemi operativi usano la protected mode? Non potrebbero rimanere in real mode?
La risposta a quest'ultima domanda ovviamente è "no", in quanto la modalità protetta offre delle caratteristiche uniche e necessarie per il funzionamento di un moderno sistema operativo.
La più ovvia è la possibilità di usare una grande quantità di memoria: in real mode è possibile accedere ad un massimo di 1 MB di RAM (le linee di indirizzamento dell'8086 sono 20, quindi è possibile accedere a 2^20 byte), mentre in protected mode, come abbiamo già detto, un sistema operativo può fare uso di ben 4 GB di memoria (o anche più, grazie a particolari estensioni che però noi non vedremo).
Un altro aspetto importante della protected mode è la protezione. Ai tempi del DOS un'applicazione poteva scrivere in memoria dove voleva, anche sopra al sistema operativo stesso. Nella modalità protetta, invece, esistono una serie di strumenti per confinare le applicazioni all'interno della loro area di memoria: un tentativo di accesso non consentito viene notificato al sistema operativo, che può chiudere l'applicazione.
Una caratteristica utile della protected mode è la memoria virtuale: grazie ad essa è possibile usare una quantità di memoria maggiore di quella presente fisicamente. Ciò è ottenuto dall'uso combinato della RAM e del disco fisso: quando una certa parte di RAM è poco usata (magari perchè c'è un'applicazione che non sta facendo nulla), il sistema operativo la può scaricare sull'hard disk, liberando quindi un po' di spazio che può essere utilizzato da altre applicazioni.

Ci vorrebbe un intero tutorial (o forse di più!) per trattare questi argomenti; io li ho solo accennati, semplificandoli parecchio, ma se siete più interessati provate a leggere il terzo manuale Intel (che trovate a http://developer.intel.com), oppure qualche buon testo di sistemi operativi. Ad ogni modo, nel resto di questo tutorial ci concentreremo semplicemente sul settaggio di alcune strutture necessarie alla protected mode, e sui passi per lo switch da real mode a protected mode, ed eviteremo quindi di esaminare in dettaglio le caratteristiche di protezione e virtualizzazione della modalità protetta.

Tools usati

Ho scritto il programma in assembly, quindi ci servirà un assemblatore...il mio preferito è il Nasm :)
Inoltre, se non volete fare continui reset del PC (io se fossi in voi non lo farei), risulta utile avere un emulatore di PC, come per esempio Bochs. Un altro buon emulatore è VMWare, che sicuramente ha meno bug di Bochs, però non è free.

Essay

Ok, eccoci qua all'essay :) Tutto il resto del tutorial sarà prevalentemente incentrato sull'analisi del programmino che esegue lo switch da real mode a protected mode; guarderemo insieme le diverse porzioni di codice, e tenterò di spiegarvi passo per passo alcuni dei concetti chiave della protected mode.
Prima di tutto, però, mi sembra opportuno spiegare il funzionamento dei tool che useremo.


1. Settiamo Bochs e Nasm

Entrambi i tool sono facili da installare: basta scaricare gli zip e decomprimerli nella directory che più vi piace; io li ho messi rispettivamente in "c:\programming\bochs" e "c:\programming\nasm". Per facilitarci il discorso, mettiamoci d'accordo anche sulla directory di lavoro: io ho usato "c:\programming\nasm\pmode". Quindi potete prendere il mio programma, e decomprimerlo in quest'ultima directory.

Una volta preparate le directory, la cosa da fare è il settaggio di Bochs: si tratta di creare il file "bochsrc" nella directory di Bochs, e di inserirci alcune direttive. Fortunatamente troviamo già un file di esempio ("bochsrc-sample.txt"), ma dobbiamo comunque cambiare qualche riga per adattarlo alle nostre esigenze. Quindi, copiamo "bochsrc-sample.txt" dentro ad un nuovo file "bochsrc", e apriamo quest'ultimo con un qualsiasi editor di testo.
La prima cosa da cambiare è "romimage": ci basta cambiare da "file=bios/BIOS-bochs-latest" a "file=BIOS-bochs-latest" (a meno che non vogliate creare una nuova directory "bios" e copiarci dentro i file di rom).
Subito sotto troviamo il settaggio della memoria emulata: Bochs prevede 32 MB...qui mettete quello che volete, io ho messo 8 MB, tanto non ci servirà parecchia memoria.
Arriviamo a "vgaromimage"...stesso discorso di "romimage", togliete la directory "bios" dal path (oppure copiate "VGABIOS-elpin-2.40" all'interno di una directory "bios").
Troviamo "floppya", che è impostato su /dev/fd0, invece noi useremo un file di immagine, quindi commentiamo quella linea e aggiungiamo invece "floppya: 1_44=pmode.img, status=inserted" (ho chiamato l'immagine del floppy "pmode.img").
Commentiamo la riga relativa a floppyb, e tutte quelle relative ai controller ata, diskc, diskd e cdrom.
Arrivati all'opzione "boot", commentiamo "boot: disk" e lasciamo non commentata invece la riga "book: floppy".
Lasciamo invariato il resto del file, e quindi salviamo il tutto ed usciamo dall'editor di testo.

Per quanto riguarda Nasm, non c'è nulla da settare...io per comodità mi sono creato un semplice file batch "make.bat" per velocizzare l'assemblaggio del file, in questo modo:
nasmw -f bin -o c:\programming\bochs\pmode.img pmode.asm
pause
Questa command line dice a Nasm che vogliamo un file binario ("-f bin", è di default ma è sempre bene specificarlo) e che l'output sarà "c:\programming\bochs\pmode.img".

Bene, una volta fatte queste operazioni preliminari, possiamo semplicemente copiare i file "pmode.asm" e "vga32.asm" nella directory "c:\programming\nasm\pmode", avviare il nostro comodo "make.bat", lanciare Bochs, scegliere l'opzione "Begin simulation" (basta premere enter visto che è l'opzione di default), e se tutto va bene dovremmo vedere una schermata del genere:


Che cosa sono quelle scritte? Cosa è successo? Beh...una cosa per volta, ora vedremo tutto :)


2. Il bootsector

Iniziamo ora ad addentrarci nei "meandri" del programma, che è composto principalmente da tre parti:
Accenno brevemente al funzionamento generale dei bootsector, senza soffermarmi eccessivamente su questa parte (che potete comunque approfondire per esempio da John Fine o, più semplicemente, su Bona Fide OS development).
Il bootsector è, in generale, il primo settore di un disco. Questo settore contiene un frammento di codice: è il codice che viene eseguito per primo, subito dopo l'accensione del PC e le routine di diagnostica del BIOS. Funziona così: il BIOS, una volta eseguite le sue routine, inizia a cercare un bootsector valido nei vari dischi (a seconda di come è stato configurato); una volta trovato un bootsector, il BIOS lo carica all'indirizzo di memoria 0x7C00, e poi esegue un jmp a questo indirizzo.
Tutti i sistemi operativi prevedono un bootsector, che può essere più o meno complicato, avere più o meno opzioni, ecc. Anche noi, dal momento che facciamo funzionare il nostro programma direttamente all'avvio del PC, abbiamo bisogno di un bootsector.
Il compito del nostro bootsector è semplice: deve solo caricare il resto del programma in memoria, e proseguire l'esecuzione saltando al codice appena caricato. Un bootsector, in generale, non può fare molto di più di questo: infatti il suo codice è limitato ad una dimensione di circa 512 byte (deve risiedere in un unico settore), e quindi nella maggior parte dei casi si limita a caricare qualcos'altro (di solito, il loader di un sistema operativo).

Detto questo, vediamo come funziona il nostro bootsector. Il codice, molto semplice, è il seguente:
	ORG	0x7C00		;0x7C00 è dove ci troviamo all'inizio del bootsector
	BITS	16

;--------------------------------------------------------------------
;bootsector - qui carichiamo il resto del programma

boot:

;prima cosa da fare: inizializzare i registri di segmento e lo stack
	xor	ax,ax
	mov	ds,ax
	mov	es,ax
	mov	ss,ax
	mov	sp,0x7C00	;mettiamo lo stack appena sotto al bootsector

;ora carichiamo il resto del programma
	mov	ax,0x0200 + BODY_SEC_LENGTH	;al=numero di settori da leggere
	mov	cx,2		;iniziamo a leggere dal secondo settore
	xor	dx,dx
	mov	bx,0x7E00
	int	0x13

;saltiamo al body che abbiamo appena caricato
	jmp	0x0000:0x7E00
Prima di tutto, specifichiamo al Nasm l'entry point: 0x7C00. Come abbiamo detto, infatti, verremo caricati lì dal BIOS. Specifichiamo anche che vogliamo un segmento a 16 bit ("BITS 16"). Il codice inizia con l'inizializzazione dei registri di segmento e dello stack: in questo momento, infatti, non sappiamo come il BIOS aveva inizializzato i segmenti, e neppure dove aveva posizionato lo stack: ci conviene quindi riportare tutte queste cose ad uno stato "sicuro". Settiamo SP a 0x7C00, in modo che lo stack cresca al di sotto del nostro codice, e quindi non lo sovrascriva.
Una volta "sbrigate" queste "formalità" iniziali, occupiamoci di caricare il "body" (così ho chiamato il resto del programma) nell'area di memoria successiva al bootsector; per fare questa operazione usiamo l'int 13h (per maggiori informazioni su questo interrupt e sugli int in generale, rimando alla [Ralph Brown's Interrupt List]). BODY_SEC_LENGTH è una costante che abbiamo dichiarato prima, e che contiene il numero di settori di cui si compone il nostro "body", e che quindi dovremo caricare.
L'ultima operazione del bootsector è un jmp al "body"; il resto del settore è riempito di zeri, tranne gli ultimi due byte che devono essere rispettivamente 0x55 e 0xAA: questo infatti è un "magic number", che dice al BIOS che il nostro è un bootsector valido. Se il BIOS non trovasse questi due byte, ci direbbe una cosa tipo "FATAL: Not a bootable disk".

Abbiamo detto tutto sul bootsector...vediamo ora il "body" che eseguirà lo switch da real mode a protected mode.


3. Body: lo switch in protected mode

Il compito di questa porzione di programma consiste nell'eseguire lo switch in protected mode. Inoltre, per abbellirlo un po', ho aggiunto la visualizzazione di qualche stringa ;)
Lo switch è composto da cinque passi:
1 - disabilitazione degli interrupt
2 - abilitazione della linea A20
3 - caricamento della GDT
4 - settaggio del PE bit
5 - far jmp al codice a 32 bit

Nei prossimi paragrafi cercherò di spiegare approfonditamente ognuno di questi step.

3.1. Disabilitazione degli interrupt

Come mai vogliamo disabilitare gli interrupt? Il problema sta nella tabella degli interrupt: la real mode ha una tabella completamente diversa dalla protected mode. Se durante lo switch si verificasse un interrupt, senza che noi avessimo preparato la corretta tabella...non sarebbe un bell'evento :) La CPU tenterebbe di leggere una tabella di interrupt che non esiste, salterebbe ad un indirizzo che non corrisponde a nessuna ISR (Interrupt Service Routine), e quindi in parole povere otterremmo un reboot della macchina.

Per disabilitare gli interrupt, eseguiamo prima di tutto una "cli"; inoltre, per completezza, disabilitiamo anche i NMI (Non Maskable Interrupt): questi ultimi non vengono disabilitati da una normale "cli", ma bisogna scrivere un comando direttamente al CMOS:
	cli
	in	al,0x70		;disabilito i NMI
	or	al,0x80
	out	0x70,al
La porta del CMOS, come vediamo dal codice sopra, è la 0x70...e il bit che maschera i NMI è l'ottavo bit del byte letto da questa porta.
A titolo informativo, i NMI sono interrupt che si possono verificare per due motivi: Entrambe le eventualità non capiteranno MAI durante l'esecuzione del nostro semplice programmino...tuttavia, i manuali Intel consigliano di disabilitare tutto il disabilitabile, e, visto che sono tre righe di codice, tanto vale farlo :)

3.2. Abilitazione della linea A20

3.2.1 Indirizzi fisici in real mode

In questo paragrafo spiego il modo in cui la real mode traduce gli indirizzi logici in indirizzi fisici: questo ci servirà per capire le ragioni per cui dovremo abilitare la linea A20. Se conoscete già il modo in cui la real mode tratta gli indirizzi fisici, passate pure al prossimo paragrafo.
In real mode, siamo abituati ad esprimere un indirizzo di memoria nella forma segmento:offset, per esempio CS:IP o SS:SP. Questi indirizzi "logici" vengono poi tradotti dalla CPU in indirizzi fisici in questo modo: si prende il valore del segmento, si moltiplica per 16 (0x10), e si aggiunge il valore dell'offset. Il risultato rappresenta il nostro indirizzo fisico.
Per esempio, se abbiamo segmento=0xABCD e offset=0x1234, l'indirizzo fisico corrispondente sarà:
0xABCD * 0x10 = 0xABCD0; 0xABCD0 + 0x1234 = 0xACF04.

3.2.2 Venti linee di indirizzamento

L'8086, che funziona solo in real mode, ha un bus CPU-memoria largo 20 bit, quindi possiede 20 linee di indirizzamento, chiamate solitamente A0, A1...A19. In sostanza, l'indirizzo di memoria massimo a cui si può accedere è (2^20-1). In realtà, il modo in cui la real mode tratta gli indirizzi permetterebbe, da un punto di vista logico, di accedere a circa 64 kb oltre questo limite: infatti, se noi consideriamo un segmento 0xFFFF e un offset 0xFFFF, otteniamo un indirizzo fisico pari a 0xFFFF0 + 0x0FFFF = 0x10FFEF, che corrisponde ad un'area di memoria grande 1 MB + 64 KB - 16 Byte. Il problema è che il bus, invece, non può spingersi oltre 1 MB (dal momento che fisicamente manca la 21-esima linea di indirizzamento). Quindi per ogni accesso al di sopra di 0xFFFFF viene eseguito un warp-around a 0: è come se si eseguisse un AND tra l'indirizzo e la mask 0x0FFFFF.
Dal 286 in poi, invece, la 21-esima linea esiste (e naturalmente viene chiamata linea A20). Quindi, pur rimanendo in real mode, è possibile usare quei 64 KB - 16 Byte in più. Tra l'altro, questa è l'area di memoria conosciuta con il nome di "High Memory Area": quando si caricava il DOS high, in realtà si caricava il sistema proprio in questi 64 KB.
Il problema di tutto questo era che, a questo punto, la real mode di un 286+ non era più totalmente compatibile con la real mode dell'8086, e quindi i progettisti Intel dovettero pensare ad una soluzione per mantenere comunque la compatibilità all'indietro (probabilmente qualche programmatore 8086 "giocava" con il warp-around, cioè scriveva applicazioni con dei puntatori che sfruttavano la proprietà di warp-around: queste applicazioni non sarebbero più funzionate correttamente sotto un 286+).
La soluzione adottata fu di creare un AND tra la linea A20 e un'altra linea di qualche chip, inizializzando quest'ultima linea a zero: in questo modo la linea A20 rimaneva a zero finchè non veniva portata ad 1 la linea con cui era posta in AND.
Quindi il warp-around continua ad esserci, e la compatilibità è garantita; per sfruttare invece tutta la memoria accessibile dalla protected mode, abbiamo bisogno di porre ad 1 la linea in AND con la A20. (Se non lo facessimo, potremmo accedere unicamente ai MB dispari! O meglio, ogni MB pari diventerebbe un "mirror" del precedente)

3.2.3 Abilitiamo la linea A20

La A20 è posta in AND con un pin del keyboard controller, quindi dovremo andare a scrivere qualche byte a questo chip. I passi che dovremo compiere consistono in:
1 - aspettare che il keyboard buffer sia vuoto
2 - leggere il current status del controller
3 - porre ad 1 il secondo bit del current status
4 - aspettare di nuovo che il buffer sia vuoto
5 - scrivere il nuovo status con il secondo bit a 1
Se volessimo fare qualcosa di più accurato, potremmo anche rileggere, alla fine, lo status e verificare che effettivamente sia andato tutto per il verso giusto (cioè che il secondo bit sia ad 1).
L'attesa per il buffer vuoto si traduce in un loop di questo tipo:
.kbd_wait_output:
;cicliamo finchè il command buffer della tastiera non è vuoto
	in	al,0x64
	test	al,2
	jnz	short .kbd_wait_output
Quando usciamo da questo loop, vuol dire che siamo pronti a mandare un byte al keyboard controller: manderemo il comando 0xD0, che corrisponde a "read output port" (ovvero informiamo il controller che siamo interessati a leggere lo status).
Una volta mandato questo byte, possiamo quindi leggere lo status; prima di fare questo, però, dobbiamo eseguire un altro loop per aspettare che il buffer di output della tastiera non sia vuoto (ovvero, dobbiamo aspettare che il controller ci fornisca il byte che abbiamo richiesto):
.kbd_wait_input:
;cicliamo finchè non c'è un dato disponibile nel buffer della tastiera
	in	al,0x64
	test	al,1
	jz	short .kbd_wait_input
Usciti da questo loop, siamo in grado di leggere lo status:
	xor	ax,ax
	in	al,0x60		;current status in AL
	or	al,2		;settiamo il bit 2, che abilita la linea a20
	push	ax		;salviamolo per dopo
Ora dobbiamo semplicemente scrivere il nuovo status: manderemo il comando 0xD1, aspetteremo di nuovo che il buffer sia vuoto, scriveremo il byte, e... ta-dan, avremo la linea A20 abilitata :)

3.3 La GDT, ovvero Global Descriptor Table

La GDT è una struttura fondamentale per la protected mode: senza di essa, non è possibile eseguire lo switch.
Nei paragrafi successivi vedremo come è fatta e come costruirla.

3.3.1 Segmenti, descrittori, selettori

Come in real mode, anche in protected mode esistono i segmenti. Quelli della modalità reale, però, non hanno nulla a che vedere con quelli della protetta: mentre i primi sono semplicemente un "trucco" per estendere la quantità di memoria indirizzabile (ovvero, pur avendo registri da 16 bit, riesco ad indirizzare 20 bit usando due registri combinati insieme), i secondi sono "segmenti di memoria" nel vero senso della parola: sono, cioè, porzioni di memoria di dimensioni e scopi variabili. Inoltre, essi non sono fissati dalla CPU: il programmatore (di sistemi operativi) è libero di scegliere, per ogni segmento, le dimensioni e i tipi che più soddisfano i suoi bisogni.
Più in dettaglio, ogni segmento è caratterizzato da: Tutte queste informazioni vengono memorizzate in particolari strutture chiamate descrittori di segmento. I descrittori, a loro volta, sono contenuti in un'unica grande struttura chiamata Global Descriptor Table.

Inoltre, per usare un segmento (cosa che risulta necessaria ad ogni accesso alla memoria) abbiamo bisogno di un meccanismo che ci permetta di specificare, ogni volta, il segmento stesso che intendiamo usare; o, meglio, dobbiamo specificare il descrittore del segmento. Per far ciò, usiamo dei selettori: questi ultimi non sono altro che i vecchi registri di segmento della real mode, ovvero CS,DS,SS, ecc., che però in protected mode assumono un significato diverso. La loro funzione, infatti, è di puntare al descrittore voluto.
Il formato di un selettore è il seguente:
Chiarisco questo punto, forse un po' oscuro, con un esempio tratto dal nostro programmino. Noi useremo due segmenti: uno per il codice ed un altro per i dati. Il descrittore del segmento di codice starà alla posizione 1 della GDT, mentre quello del segmento dei dati starà alla posizione 2. Quindi il campo Index del selettore del segmento di codice sarà 1, mentre quello del selettore del segmento dei dati sarà 2.
In pratica, avremo CS=0x08 (quindi il campo Index di CS è 1), e DS=ES=...=0x10 (quindi il campo Index di DS,ES, ecc. è 2).

3.3.2 Descrittori in dettaglio

Una volta chiariti i ruoli di segmenti, descrittori e selettori, possiamo procedere alla costruzione della nostra GDT. Per fare ciò, dobbiamo innanzitutto sapere com'è fatto un descrittore, ovvero conoscerne la struttura; essa consiste in:
segment_limit_0_15	dw	?	;i primi 16 bit del segment limit
base_address_0_15	dw	?	;i primi 16 bit del base address
base_address_16_23	db	?	;bit 16-23 del base address
type__flag1		db	?	;indicatore del tipo di segmento, più alcuni flag
limit_16_19__flag2	db	?	;bit 16-19 del limit, più altri flag
base_address_24_31	db	?	;bit 24-31 del base address
Segment limit specifica la dimensione del segmento. Se osservate attentamente la struttura sopra, noterete che ci sono solo 20 bit per questo campo; non vi sareste invece aspettati una dimensione massima di 32 bit? Con 20 bit di dimensione, ogni segmento è grande al massimo 1 MB...e quindi non abbiamo guadagnato molto rispetto alla real mode.
Ma...c'è un trucco :) E' possibile specificare, tra i vari flag, la granularità (G) della dimensione del segmento. Questo flag indica l'unità base in cui viene espresso il segment limit; in pratica, se G=0, il segment limit viene interpretato in unità di byte, mentre se G=1 il limit è interpretato in unità di 4 KB.
Quindi, facendo due conti, se poniamo G=1 e limit=0xFFFFF (quindi dimensione = limit+1 = 0x100000), otteniamo una dimensione del segmento pari a (limit+1)*(4 KB) = 0x100000*0x1000 = 0x100000000 byte, che è esattamente 2^32 byte ( = 4 GB). Se invece, con lo stesso limit, avessimo tenuto G=0, avremmo avuto una dimensione di 0x100000*1 = 0x100000, ovvero 2^20 byte.

Base address indica il punto di partenza del segmento. Se pensavate che tutti i segmenti partissero da 0, ovvero dall'inizio della memoria, sbagliavate: un segmento può partire da qualunque punto della memoria. Se per esempio ho un segmento con base=0x1000, e voglio accedere ad un dato contenuto alla posizione 1 di quel segmento (ad esempio, con "mov eax,dword [es:1]"), in realtà accederò all'indirizzo fisico 0x1001. Il base address viene sempre sommato all'indirizzo specificato; in questo modo può essere garantita una qualche forma di protezione. Per esempio, costruendo un segmento di codice per un'applicazione, potremmo stabilire come base il punto di partenza del codice in memoria, e come limite la dimensione del codice stesso: in questo modo, se ci fosse qualche puntatore sbagliato, le altre applicazioni e il sistema operativo stesso sarebbero protetti da eventuali scritture sbagliate, perchè il programma buggato rimarrebbe comunque entro i suoi limiti. (O, meglio, se cercasse di scavalcare il segment limit, verrebbe generata un'eccezione, e quindi il sistema operativo sarebbe in grado di chiudere l'applicazione buggata).

Il campo type specifica il tipo di segmento. Come abbiamo già detto nel paragrafo 3.3.1, esistono diversi tipi; a noi interessano in particolare il tipo data read-write (type=2) e code execute-read (type=0x0A).
Il campo type è contenuto nei 4 bit meno significativi di type__flag1.

Sempre in type__flag1 troviamo alcuni flag:
In limit_16_19__flag2, oltre a 4 bit dedicati al segment limit, troviamo altri flag:

3.3.3 Costruiamo la nostra GDT

La nostra GDT sarà composta, come ho già accennato, da due segmenti: uno di codice e uno di dati. Ho scelto di porre la base di entrambi a 0, e il limit a 0xFFFFF (con granularità di 4 KB, quindi una dimensione di 2^32 byte). Questa configurazione è quella che sul manuale Intel viene chiamata basic flat model: è il modello di memoria più semplice che si possa immaginare, in cui la memoria è vista come un unico grosso spazio ("flat") di 4 GB, ed ogni indirizzo logico equivale al corrispondente indirizzo fisico (perchè la base è 0).
I nostri due descrittori quindi saranno:
code_desc:
	dw	0xFFFF		;limit 0-15
	dw	0x0000		;base 0-15
	db	0x00		;base 16-23
	db	GDTFLAG_P+GDTFLAG_S+GDTFLAG_TYPE_CODE
	db	GDTFLAG_G_4KB+GDTFLAG_DB_32+0x0F
	db	0x00		;base 24-31

data_desc:
	dw	0xFFFF		;limit 0-15
	dw	0x0000		;base 0-15
	db	0x00		;base 16-23
	db	GDTFLAG_P+GDTFLAG_S+GDTFLAG_TYPE_DATA
	db	GDTFLAG_G_4KB+GDTFLAG_DB_32+0x0F
	db	0x00		;base 24-31
Per aumentare un po' la leggibilità, ho dichiarato i vari flag come costanti (trovate tutto in "pmode.asm").
Guardando nel codice, noterete che ho dichiarato anche un null descriptor: è necessario, infatti, che il primo descrittore della GDT sia un descrittore nullo. E' possibile, in questo modo, porre a 0 i selettori non utilizzati; se poi uno di questi selettori inutilizzati viene per sbaglio usato, si verifica un'eccezione.

3.3.4 Carichiamo la GDT

Una volta creata la nostra GDT, dobbiamo dire alla CPU il suo indirizzo e la sua dimensione. Esiste uno speciale registro, GDTR, che contiene appunto queste due informazioni. Il formato di GDTR è: Allora, possiamo scrivere da qualche parte queste due informazioni, in questo modo:
gdtr:
	dw	GDT_SIZE
	dd	null_desc
"null_desc" è l'indirizzo del primo descrittore della nostra GDT, mentre GDT_SIZE, definita in fondo alla GDT come "GDT_SIZE equ $ - null_desc", ne contiene la dimensione.
Per caricare questi dati in GDTR usiamo l'istruzione "lgdt":
	lgdt	[gdtr]
A questo punto la CPU è pronta per passare in protected mode; per poter essere a tutti gli effetti in modalità protetta, ci manca solo da settare un flag del registro CR0 (Control Register 0).

3.4 Lo switch

Come ho già accennato, ora dobbiamo porre a 1 il bit PE (Protection enable) del registro CR0 (il PE è il primo bit di questo registro): quando settato, infatti, questo bit dice alla CPU che ci troviamo in protected mode.
Una volta fatto questo, dobbiamo eseguire un far jump ad un segmento di codice a 32 bit. Le ragioni di quest'ultima operazione non sono del tutto chiare (nel senso che il manuale Intel sorvola sull'argomento, fornendo spiegazioni non troppo esaurienti), comunque è bene eseguirla, perlomeno per inizializzare CS in modo che punti al descrittore del code segment nella GDT (ovvero, abbiamo bisogno di inizializzare CS a 0x08). Se vi interessa approfondire l'argomento c'è questo thread (del 2001) su alt.os.developing, in cui si discute l'utilità del far jump.

Il codice quindi risulta:
	mov	eax,CR0
	or	al,1		;settiamo PE
	mov	CR0,eax

	jmp	dword 8:body_32	;far jump
Da questo punto in poi l'esecuzione proseguirà da body_32.


4. Body_32: il codice in protected mode

La prima cosa che dobbiamo fare, una volta arrivati in modalità protetta, è inizializzare tutti i selettori (tranne CS, che è già stato inizializzato dal far jump) in modo che puntino ad un descrittore valido. Notate che inizializziamo anche SS con il descrittore del segmento dati: lo stack infatti è un data segment a tutti gli effetti. Sarebbe stato forse meglio prevedere un descrittore separato dedicato allo stack...ma per i nostri scopi va più che bene un unico descrittore dati.
Inizializziamo anche ESP, ponendolo a 0x000A0000...qui un indirizzo vale l'altro, io ho scelto questo perchè è il limite dei primi 640 KB di memoria (al di sopra dei quali iniziano le varie rom e memory mapped i/o, ad esempio della vga).

Una volta fatto ciò, visualizziamo una stringa; ho scritto alcune routine per la gestione della vga a basso livello, ma le ho messe in un altro file ("vga32.asm") perchè non rientrano nel nostro discorso sulla protected mode. Se siete interessati, potete consultare le pagine del FreeVga Project, un'ottima risorsa per la programmazione della vga e della svga a basso livello.

L'ultima cosa da sottolineare è il conteggio della memoria fisica: questo avviene con un semplice loop, in cui vado a scrivere una dword in un certo indirizzo, e poi controllo se la dword che leggo allo stesso indirizzo è uguale a quella scritta; se è diversa, vuol dire che in quel punto non esiste più memoria fisica.
In realtà, questo non è uno dei modi migliori per contare la memoria: sarebbe meglio chiamare la funzione 0xE801 dell'int 15h (consultate la Ralph Brown's interrupt list, o http://www.uruk.org/orig-grub/mem64mb.html per maggiori informazioni). Ad ogni modo, questo conteggio voleva solo servire come semplice esempio per dimostrare che possiamo accedere all'intera memoria fisica.
L'ultima cosa che facciamo è "appendere la cornetta": un "cli / hlt" fa al nostro caso :) Potevamo anche ritornare in real mode ed eseguire un reboot del sistema; il ritorno in real mode, però, non è del tutto banale (cioè, non basta rimettere il PE bit a zero); per maggiori informazioni, consultate il manuale 3 di Intel, al paragrafo 8.8.2, che descrive dettagliatamente l'intera procedura.


Bibliografia e referenze

Interrupt real mode e porte hardware:
Ralph Brown's Interrupt List (anche qua per una versione in html)

Linea A20:
http://www.frick-cpa.com/dos/DOS_Memory_1.asp
http://www.mega-tokyo.com/os/os-faq.html
http://osdev.neopages.net/tutorials/a20.php


Protected mode in generale:
http://x86.ddj.com/articles/pmbasics/tspec_a1_doc.htm
http://ritz.oltrelinux.com/osdevel/i32.txt

I dettagli (manuali Intel):
http://developer.intel.com

Altri link:
FreeVga Project
John Fine
Bona Fide OS development
alt.os.development


Note finali

Un saluto a tutti i frequentatori di #asm e #crack-it @ irc.azzurranet.org: siete tutti fantastici ;)
Ringrazio i Dream Theater per la musica :P

Disclaimer

Ma serve un disclaimer?? Non ho scritto nulla di male!