Intro all' architettura Intel orientata alla programmazione in (m)asm, Segmentazione, Protected Mode

Data

By "Lonely Wolf"

 

30/07/2004

UIC's Home Page

Published by Quequero

Se son matto pazienza. Preferisco la mia follia alla saggezza degli altri - Van Gogh

Complimenti Lone, quando ti metti le fai per bene le cose!!
- Ne hai mai dubitato Que? Questo è solo l'inizio...
Ok allora sei solo pigriiiiiissimo ;p scherzo, complimenti per il lavoraccio!

Prima di correre, bisogna imparare a camminare...

....

E-mail: [email protected]
Su azzurra, #crack-it #cryptorev #pmode #olografix #cencioschat ..

....

Difficoltà

( )NewBies(X)Intermedio( )Avanzato( )Master

 

Oggi niente cracking o reversing...oggi architetturing, segmenti & co. :D che male non fa :P
Ma cosa c'entra sta roba col reversing???
C'entra, C'entra.... ;)


Intro all' architettura Intel orientata alla programmazione in (m)asm, Segmentazione, Protected Mode

Written by "Lonely Wolf"

Introduzione

Anni fa a scuola, iniziando il linguaggio assembler dopo la parte introduttiva, il prof (cmq mitiko!) distribuì a tutta la classe una fotocopia con lo "scheletro" di un programma assembler scritto con le direttive estese del masm, avete presente, stack segment para stack, ASSUME... qualche riga sotto il codice opportuno per l'inizializzazione dei segmenti, quella che era la parte "standard"(?) come la chiamò lui ed esordì dicendoci che dovevamo fidarci e prenderla per buona(!) per non entrare in dettagli e particolari che evidentemente non conosceva/non sapeva/non voleva spiegare. Boh, che dovevamo fare? L'abbiamo presa per buona e siamo andati avanti, ma personalmente mi scocciava avere un pezzo di codice che non sapevo bene che facesse e che non potevo spiegare..cmq vabbe', alla fine qualcosa ci ho capito. Sono passati diversi anni da allora e io mi son diplomato. Un mesetto fa, neanche, un mio amico (ciao Luchino!:D) che frequenta lo stesso corso di informatica mi chiama perchè ha bisogno che gli rispieghi alcune cose di assembler che non ha capito bene, gli faccio qualche domanda per farmi un'idea e cosa viene fuori? Che il loro prof (un altro a questo giro) incappando sullo stesso punto ha liquidato l'ennesima classe con il solito "prendete per buono questo pezzo di codice...ora non lo stiamo a spiegare...non scendiamo nei dettagli..." Mi sono detto: "Noooo! Ma Siamo ancora a questi livelli???"
Non solo, l'altro giorno ho visto di peggio: il libro di testo di sistemi adottato quest'anno dalla classe terza del mio amico Luca. Scritto da una donna...arriva al punto in cui spiega l'uso delle direttive estese e semplificate. Le semplificate al solito, .MODEL ecc...mentre per quella tipa le direttive estese sono:

DATA SEGMENT
 ...
DATA ENDS
STACK SEGMENT
 ..
STACK ENDS

CODE SEGMENT
...

CODE ENDS
Scandaloso! A questo punto faceva solo le direttive semplificate e faceva più figura...la scuola superiore...puah! Per questo motivo nasce quello che state leggendo, in ITALIANO - per la gioia di quei disgraziati che non sanno/vogliono leggere in inglese... :P -, assemblato con pezzi vari rimediati in qua e la da internet, anzi, se scrivo castronerie sono ben lieto che me lo facciate presente o anche se ritenete opportuno aggiungere qualcosa. Spero di essere chiaro e di fare un buon lavoro, e soprattutto che capiate (anzi, che capiamo, mi ci metto anche io visto che per arrivare in fondo a questo tutorial ho dovuto sudare e farmi spiegare diverse cosette) e che magari possiate fare una bella figura col prof, dandogli una bella "sverniciatina" ;) ihihih
Ma Masm6.1?? ma è obsoleto! potrebbe dire qualcuno. Beh, si, non è esattamente quello che si può definire l'ultimo grido, ma tuttavia ritengo che ne valga davvero la pena approfondire e (in teoria) tutto questo non dovrebbe essere inutile. Non voglio reinventare la ruota, per cui non troverete l'elenco delle istruzioni e come dovreste usarle, in rete ce ne sono anche troppi, ma semplicemente affrontare l'argomento segmentazione e inizializzazione, lo scopo per cui è nato, anche se redigendo il tute mi sono reso conto che per spiegare certe cose ne andavano spiegate delle altre, alcune delle quali al momento della stesura io stesso non sapevo o mi son reso conto di non aver capito poi così bene, per cui ho dovuto farmele spiegare da qualche anima pia in chan o da San Google ;) e per questo le dimensioni del tute sono lievitate. Inoltre, nel tute verranno presi in considerazione, quanto meno accennate, molti di quei paroloni o di quelle sigle oscure che potete trovare facilmente in giro da queste parti, quali...boh TLB, TSS, LDT, GDT...anche se una trattazione completa esula dagli scopi di questo tute. Ci ho passato le feste sopra e sicuramente ci dovrò tornare sopra per capire meglio varie cosine, ma ne è valsa la pena: ragazzi, basta con i bignami e le riviste su carta patinata, studiamo i manuali intel. Un si sbaglia ;D

Tools usati

Volontà - Non so dove si possa scaricare, provate su emule o idc, tanto ormai si trova praticamente di tutto :D Beh, il set di istruzioni del vostro processore :P

URL o FTP del programma

Sta sulla scheda madre :)

Notizie sul programma

Ragazzi, oggi si va giù pesi, rilassatevi, fumate, prendete una boccata d'aria prima...  

Essay

Prima di tutto, bisogna che abbiate ben chiaro come funzionano le "cose" nel computer e farvi una buona idea di come sia organizzata la memoria di un processore 8086 e 80386. Lo so, a scuola quando siamo seduti al banchino e ci cominciano a parlare dell'8086 ci sembra una cosa assurda: "Ma come, a casa ho un Pentium 4 fiammante e questi qua mi vengono a parlare di 8086?? e pure il 386 è superato!". Capisco, può sembrare inutile e forse un po' lo è ma se ci state dietro, un giorno capirete che non lo è affatto...e acquisendo queste conoscenze, passare ai DETTAGLI dell'ultimo Pentium fiammante (ma poi perchè limitarsi a Intel... ;P) sarà più semplice.
E'inutile addentrarsi negli "oscuri" meandri dell'assembler senza prima avere delle basi decenti, programmare in assembler non significa solamente imparare a pappagallo a usare determinate istruzioni..si le cose possono anche funzionare, ma per me come molti altri, programmare in Assembler significa prima di tutto sapere cosa si sta facendo, e come ogni singola istruzione influisce o viene influenzata dall'architettura...e più sapete COME funziona più sapete COSA state facendo e siete in grado di manipolare per raggiungere i vostri scopi...per questo cominceremo con la comprensione dei concetti fondamentali, in modo che poi siate capaci di localizzare e contestualizzare ogni singolo elemento effettuando i collegamenti neuronali del caso :P
Questi skill sono importanti perchè vi aiuteranno a vedere il computer e ogni programma in esecuzione con occhi diversi...e sono delle ottime basi che vi torneranno utili, sia che vogliate fare i programmatori, i sistemisti...

Prerequisiti: avere una conoscenza di base dei registri del microprocessore e delle operazioni fondamentali e che sappiate ad esempio la differenza tra un registro accumulatore e un registro di segmento. Darò per scontato che lo sappiate. Ovviamente, per una trattazione DETTAGLIATA di questi argomenti, vi rimando ai manuali intel o a qualche ricerca specifica su google.
Alcuni commenti tra parentesi sono di Ntoskrnl e sono indicati con ndnt. Ok, allora...Rock 'n Roll!

La segmentazione

E'un meccanismo per la gestione della memoria che permette ai programmatori di "partizionare" i loro programmi in "moduli" che operano indipendentemente l'uno dall'altro. Immaginiamo la memoria come un unico grande array di byte. In qualità di array, con un indice (l'indirizzo) si può selezionare un qualsiasi elemento di questo array. Chiamiamo questa modalità di indirizzamento Lineare (da qui Linear Address appunto) o Flat.
Dal manuale Intel (Basic Architecture): "Con la segmentazione, un registro di segmento di 16 bit contiene un puntatore (quindi un indirizzo) a un segmento di memoria fino a 64Kb (NdLonelyWolf: questo perchè essendo l'8086 un'architettura a 16 bit, per la grandezza del bus indirizzi, il numero massimo che può contenere è appunto 216 ed è questo il max numero di segmenti potenzialmente disponibili). Usando 4 registri di segmento alla volta i processori 8086/8088 sono capaci di indirizzare 256Kb senza switchare tra i segmenti.......Con il modello della memoria segmentata un programma vede la memoria come un gruppo di spazi di indirizzi indipendenti chiamati appunto Segmenti. Usando questo modello, codice, dati, stack sono tipicamente contenuti in segmenti diversi. Per indirizzare un byte in un segmento, un programma deve fornire un Logical Address(Indirizzo Logico) composto da un segmento selettore e un offset. Un indirizzo logico è spesso riferito come Far Pointer. Il segmento selettore identifica il segmento che deve essere acceduto e l'offset identifica un byte nello spazio di indirizzi di tale segmento...
Nonostante l'architettura segmentata, la memoria fisica presente sulla scheda madre viene comunque vista come un array lineare di byte per cui esisterà una funzione/meccanismo che convertirà un'indirizzo segmentato in uno lineare.
Internamente tutti i segmenti che vengono definiti per un sistema sono mappati nel Linear Address Space. Per accedere ad una locazione di memoria il processore traduce (in maniera trasparente) gli indirizzi logici in indirizzi lineari. La prima ragione per usare la segmentazione è per accrescere l'affidabilità dei programmi e dei sistemi. Ad esempio, mettendo lo stack in un segmento separato si evita che questo cresca nel codice o nei dati e sovrascriva i dati relativi.
Con il modello flat
(che vedremo cmq dopo) o segmentato, il linear address space viene mappato nello spazio di indirizzi fisico del processore sia direttamente che mediante paging. Quando si usa il mapping diretto (paging disabilitato) ogni indirizzo lineare ha una corrispondenza 1:1 con un indirizzo fisico (e quindi questi indirizzi lineari vengono mandati sul bus indirizzi del processore così come sono, senza dover essere tradotti). Quando si usa il meccanismo di paging (anche esso trasparente) della IA-32 lo spazio di indirizzi lineare viene diviso in pagine, mappate nella memoria (virtuale).

Ogni indirizzo è rappresentato dunque come

segmento:offset

dove segmento e offset consistono sempre (su 8086) in un valore a 16 bit. In Real Mode (Modalità Reale), il valore del segmento è un indirizzo fisico che ha una relazione matematica col valore dell'offset. Insieme, segmento e offset creano, anzi DEVONO creare un'indirizzo fisico di 20 bit perchè tale è la grandezza del bus indirizzi.
Ma come fa il processore a combinare un segmento e un offset di 16 bit per formare un indirizzo di 20 bit da mettere sul relativo bus? Ecco come/cosa fa:

1. A scuola ci dicevano "si mette uno 0 in fondo all'indirizzo..." si è vero, il risultato è quello ma un momento ;)
Il processore shifta verso sinistra l'indirizzo del segmento (lasciando per questo 4 bit a zero), producendo così un indirizzo di 20 bit. Questa operazione corrisponde alla moltiplicazione dell'indirizzo del segmento per 16, ovvero 10 in hex infatti non serve la calcolatrice per fare 53C2 * 10 come da esempio sotto (in asm corrisponde a un SHL reg, 4 dove reg ovviamente è un REGistro e guarda caso 24 fa 16... un po'più esaustiva come spiegazione, non trovate? :P)
2. Il processore aggiunge questo indirizzo di segmento di 20 bit all'offset di 16 (che NON viene shiftato)
3. Il processore utilizza così l'indirizzo risultante di 20 bit chiamato indirizzo fisico, per accedere alla corrente locazione nello spazio di indirizzi di 1 megabyte (si 1 Mb. 2^20 boys)

Es.


      53C2:107A 

    5 3 C 2 0   SHL reg, 4
  +
      1 0 7 A   l'offset di 16 bit
--------------
    5 4 C 9 A   indirizzo fisico di 20 bit
Nota:
Un'indirizzo fisico di 20 bit può essere specificato con 4096 segmento:offset diversi! Infatti gli indirizzi 0000:F800, 0F00:0800 e 0F80:0000 si riferiscono tutti allo stesso indirizzo fisico 0F800.

Questi concetti sono molto importanti, perchè riguardano molti aspetti della programmazione in asm, specialmente per gli indirizzi e i puntatori.

Ancora sulla segmentazione e paginazione dai manuali intel: La segmentazione provvede un meccanismo per isolare codice individuale, dati e stack in modo che più programmi (task) possano essere eseguiti sullo stesso processore senza interferire l'uno con l'altro. La paginazione fornisce un meccansimo per implementare una demand-paged convenzionale, un sistema di memoria virtuale dove sezioni dell'ambiente di esecuzione di un programma sono mappate nella memoria fisica quando necessario (Quando segmentazione e paginazione vengono combinate insieme, i segmenti possono essere mappati in pagine in molti modi)
Anche la paginazione può essere utilizzata per fornire isolamento tra task multipli (ogni segmento viene diviso in pagine solitamente di 4Kb che vengono memorizzate nella memoria fisica o eventualmente su disco). Le informazioni che il processore usa per mappare indirizzi lineari nei relativi indirizzi fisici e per generare eventuali Page-Fault vengono mantenute in "Page Directory" e un set di "Page Tables" in memoria, per tenere traccia delle pagine. In questo modo, quando un programma tenta di accedere ad una locazione nello Linear Address Space il processore usa la Page Directory e la Page Tables per *Tradurre* l'indirizzo lineare nel corrispondente indirizzo fisico.



Se la pagina richiesta non dovesse trovarsi nella memoria fisica il processore interromperebbe l'esecuzione del programma (generando una eccezione "Page-Fault, #PF"). L'Exception Handler per il Page-Fault la caricherebbe dal disco e il ritorno dall'Exception Handler farebbe riprendere l'esecuzione del programma.
Per minimizzare i cicli di bus richiesti per la traduzione degli indirizzi, le Page Directory accedute più frequentemente e le Page Tables entries sono cachate nel processore in dispositivi chiamati Translation Lookaside Buffers (TLBs). La famiglia P6 e processori Pentium hanno TLBs separate per cachare dati e istruzioni. L'istruzione CPUID può essere usata per determinare la grandezza del TLB del proprio processore. Le TLBs soddisfano molte richieste di lettura della Page Directory corrente e Page Tables senza richiedere un ciclo di bus, ciclo che occorrerebbe solo in caso di "cache miss" (NdLonelyWolf: ovvero quando non è presente nel buffer, ergo la pagina non è stata acceduta da un pezzo). Mentre esiste un modo per abilitare/disabilitare il paging (il bit 31 del registro di controllo CR0..Disponibile su tutti i processore IA-32 a partire dal 80386) non c'è alcun modo per disabilitare la segmentazione.....
La segmentazione fornisce un meccanismo per dividere lo spazio di memoria indirizzabile del processore (Linear Address Space) in spazi di indirizzi più piccoli protetti, chiamati appunto SEGMENTI. I segmenti possono essere usati per mantenere codice, dati e stack per un programma oppure per le strutture dati del sistema (come LDT o TSS). Se più di un programma è in esecuzione a ognuno di questi viene assegnato un suo set di segmenti. Il processore gestisce anche i limiti di questi segmenti e assicura che un programma non possa interferire con l'esecuzione di un altro scrivendo in un altro segmento. Il meccanismo di segmentazione permette anche di tipizzare i segmenti in modo che alcune operazioni su tali segmenti, possano essere ristrette o meno.
Il selettore è un identificatore unico del segmento. Provvede anche un offset in una descriptor table (ad esempio GDT) che a sua volta punta a una struttura dati chiamata Segment Descriptor. Ogni segmento ha un proprio segment descriptor che specifica BASE e LIMIT, i diritti di accesso e il livello di privilegio per il segmento (Ring 0-1-2-3), il tipo di segmento e la locazione del primo byte del segmento nello Linear Address Space (chiamato Base Address). L'offset viene poi aggiunto a questo indirizzo base per localizzare un byte all'interno del segmento. Base Address + Offset = Linear Address Space nello Linear Address Space del processore. (Un Physical Address - Indirizzo Fisico - viene definito come il range di indirizzi che il processore può generare sul suo bus indirizzi).
Non vi spaventate...

Real Mode e Protected Mode

In real mode può essere eseguito un solo processo alla volta (niente multithreading quindi, ndnt) e gli indirizzi corrispondono sempre alla Reale locazione in memoria (indirizzi fisici volevo dire). Sempre dal Manuale Intel (spero di tradurre a modino ;P): Protected Mode usa i contenuti di un registro di segmento come selettori o puntatori a una tabella di descrittori. I descrittori provvedono un indirizzo a 24 bit, fino a 16Mb di memoria fisica, supporto per la gestione della memoria virtuale su un segmento basato su swapping e vari meccanismi di protezione. I meccanismi di protezione includono: Controllo dei limiti di un segmento, opzioni sui segmenti di read-only o execution-only, e fino a 4 livelli di privilegio (Anche se in pratica se ne usano solo 2, ndnt) per proteggere il codice del sistema operativo (gli osannati Ring) da interferenze. In aggiunta, hardware task switching e LDT (Local Descriptor Table) permettono al sistema operativo di proteggere le appplicazioni degli utenti l'una dalle altre. ...Per garantire la compatibilità, si può sempre usare la Real Mode, ma solo attivando la modalità Virtual 8086.
Quando si opera in protected mode tutti gli accessi alla memoria passano attraverso la GDT o la LDT. Queste tabelle contengono delle entries chiamate Segment Descriptor. Un segment descriptor fornisce l'indirizzo di base di un segmento e diritti di accesso, tipo e informazioni di uso. Ognuno di questi segment descriptor possiede un Segment Selector univoco associato. Il segment selector fornisce un indice nella GDT o LDT, un flag global/local (che determina appunto se il segment descriptor punta alla GDT o alla LDT) e informazioni sui diritti di accesso. Per accedere a un byte in un segmento, entrambi i segment selector e un offset devono essere forniti. Il segment selector fornisce l'accesso al segment descriptor per il segmento (nella GDT o LDT). Dal segment selector, il processore ottiene l'indirizzo base del segmento nel Linear Address Space. L'offset poi provvede la locazione del byte relativa al base address. Questo meccanismo può essere usato per accedere a qualsiasi segmento codice, dati, o stack valido nella LDT o GDT, in base all'accessibilità del segmento data dal CPL (Current Privilege Level) nel quale il processore sta operando (Il CPL è definito come livello di protezione del codice segmento in esecuzione). Il linear address della base della GDT è contenuto nel registro GDTR mentre quello della LDT nel registro LDTR (registri per i quali abbiamo istruzioni dedicate).
In protected mode gli indirizzi non corrispondono direttamente alla memoria fisica. Il processore alloca e gestisce la memoria dinamicamente. Quindi se il codice e i dati di un prog occupano meno di 64K risiedono nello stesso segmento per cui basta solo l'offset per localizzare una variabile o un'istruzione. In caso contrario, se ad esempio il segmento data occupa 2 o più segmenti il programma dovrà specificare sia segmento che l'offset per localizzare una determinata variabile. (Questo problema non sussiste invece in uno spazio di indirizzi "flat" della modalità protetta a 32 bit) Con l'"avvento" dei processori in modalità protetta, l'architettura segmentata ebbe un altro scopo. I segmenti potevano separare blocchi differenti di codice o dati e proteggerli da interazioni indesiderate (e inoltre supportavano il modello di memoria flat). Quando un programma vuole accedere a un segmento quindi, "dietro le quinte", il segment selector per quel segmento deve essere stato caricato in uno dei registri di segmento. Così, sebbene un sistema possa definire migliaia di segmenti, solo 6 possono essere disponibili per un uso immediato. Gli altri segmenti sono resi disponibili con il caricamento del loro segment selector in questi registri durante l'esecuzione del programma. Ogni registro di segmento ha una parte "visibile" e una "nascosta" (la parte nascosta è qualche volta riferita come Descriptor Cache o Shadow Register).



(http://www.x86.org/articles/pmbasics/tspec_a1_doc.htm) Articolo molto interessante, spiega molto bene questo discorso del Segment Descriptor Cache accennando anche a come in Real Mode su 80386 si potrebbe avere un grosso data segment in read only di 4 Gb....Quando un segment selector viene caricato nella parte visibile di un registro di segmento, il processore carica anche la parte nascosta del registro di segmento con il base address, limit e le informazioni di accesso prese dal descrittore puntato dal selettore. Queste informazioni cachate nel registro di segmento (visibili e invisibili) permettono al processore di tradurre indirizzi senza cicli bus extra per leggere quelle informazioni dal descriptor (che volponi che sono ;D). L'indirizzo fisico presentato sul bus address della CPU è formato con l'aggiunta dell'offset (16 o 32 bit) al base address presente nella descriptor cache
http://www.x86.org/ddj/aug98/aug98.htm Descrive in maniera decisamente migliore questa Descriptor Cache, altra lettura consigliata se proprio non avete niente di meglio da fare. C'è un paragrafetto "Descriptor-Cache Registers In Real Life"...ci sono modi diversi per prendere vantaggio dai registri segment-descriptor cache. System-management mode (SMM) ti da il controllo diretto su ogni campo nella segment-descriptor cache. (See my DDJ January >/ March/May 1997 columns for an in-depth look at System Management Mode.) Anche In-circuit emulators (ICEs) permette di prendere il controllo diretto su ogni campo nella segment-descriptor cache. (Refer to my DDJ July /September /November 1997 columns for information on in-circuit emulation.)
L'autore racconta di come gli capita a volte di sbagliare qualcosa nella dichiarazione di una sua GDT usando un base address errato ad esempio, e debuggando con ICE (uhmmmm pensate anche voi quello che penso io? :P) carica e può modificare determinati valori direttamente li. L'articolo prosegue con quella che è stata definita Unreal Mode, cosa che ha attirato la mia attenzione. "...l'uso di queste tecniche per la manipolazione della Segment-Descriptor Cache può essere una sfida. Comunque, esiste un altro modo programmatico di far funzionare questa Segment-Descriptor cache, creando una modalità operativa di CPU chiamata Unreal Mode. Unreal Mode viene creata quando un segmento in modalità reale si ritrova con un segment limit di 4 Gb. Unreal Mode può essere creata senza l'uso di debugger hardware o programmazione in SMM con un semplice programma in assembler. Immagina un programma che inizia in modalità reale e poi salta in modalità protetta. Una volte in protected mode il programma carica tutti i registri di segmento con descrittori contenti segment limits di 4 Gb. Dopo aver settato i segment limits ritorna immediante in modalità reale senza ripristinare i registri di segmento contenenti segmenti di 64Kb (compatibili modalità reale). Una volta in modalità reale il segment limit resterà quello di 4Gb. A quel punto i programmi DOS potrebbero trarre vantaggio dell'intero spazio di indirizzamento a 32 bit senza programmazione in protected mode.
Unreal Mode è stata usata comunemente da quando fu scoperta sull'80386. Questa modalità è così comunemente usata infatti, che Intel è stata forzata a supportarla come strascico del comportamento dell' 80x86 anche se non è mai stata documentata. Giochi e Memory Managers spesso traggono vantaggio da questo. Source Code disponibile su DDJ (see "Resource Center,") oppure ftp://ftp.x86.org/dloads/UNREAL.ZIP oppure qua http://www.x86.org/ftp/dloads/unreal.zip
Tuttavia, entrare nei meriti di SMM e ICE esula dagli scopi di questo tutorial, forse pubblicherò qualcosa più in la, visto che il sottoscritto è curioso e vuole approfondire.

Memory Management Register (Sempre dai manuali Intel, Vol.3)

Il processore provvede 4 registri per la gestione della memoria (GDTR, LDTR, IDTR e TR) che specificano le locazioni delle strutture dati della gestione della memoria segmentata. (NdLonelyWolf: per la cronaca, esistono anche altri 2 gruppi di registri, i Control Register e i Debug Register che per ora almeno non prenderemo in esame, ma MOLTO importanti). Istruzioni speciali sono incaricate di leggere e scrivere in questi registri.



Il registro GDTR contiene il base address di 32 bit e il limite di 16 bit per la GDT. Il base address specifica il linear address del byte 0 nella GDT; limite specifica il numero di byte nella tabella....All'accensione o al reset del processore, il base address è settato al valore di default 0 e il limite a FFFFh. Un nuovo indirizzo base deve essere caricato nel GDTR come parte del processo di inizializzazione del processore per le operazioni in protected mode.
Il registro LDTR contiene il selettore del segmento di 16 bit, il base address di 32 bit, il limite del segmento di 16 bit e glil attributi del descrittore per la LDT. Questo base address specifica l'indirizzo lineare del byte 0 del segmento LDT; il limite del segmento specifica il numero di bytes nel segmento...il segmento che contiene la LDT deve avere un segment descriptor nella GDT...quando l'istruzione LLDT carica un segment selector nel LDTR, il base address, limit e gli attributi del descrittore vengono presi dal descrittore della LDT e caricati nel LDTR. Quando si verifica un task switch, LDTR è caricato automaticamente con il segment selector e il descrittore LDT per il nuovo task. I contenuti del LDTR non sono salvati automaticamente prima della scrittura delle nuove informazioni nel registro.
Il registro IDTR contiene il base address di 32 bit e il limit di 16 bit per la IDT. Il base address specifica l'indirizzo lineare del byte 0 della IDT e il limit il numero di byte nella tabella.
Il TR - Task Register contiene un segment selector di 16 bit, il base address di 32 bit, il limit di 16 bit e gli attributi del descrittore per il TSS (Task State Segment per il supporto del multitasking) del task corrente. Esso si riferisce a un TSS descriptor nella GDT. Il base address specifica l'indirizzo lineare del byte 0 del TSS; il limite del segmento specifica il numero di bytes nel TSS. Quando si verifica un task switch, il task register viene caricato automaticamente con il segmento selettore e il descrittore per il TSS del nuovo task. I contenuti del task register non sono salvati prima della scrittura delle informazioni del nuovo TSS nel registro.


Ancora (http://www.x86.org/articles/pmbasics/tspec_a1_doc.htm )

...in modalità protetta la segmentazione di memoria è invece definita da un set di tabelle (chiamate tabelle di descrittori) e i registri di segmento contengono puntatori a queste tabelle. Ogni entry in queste tabelle è di 8 byte, perciò i valori contenuti nei registri di segmento sono definiti in multipli di 8 (08h, 10h, 18h, etc.), questo implica un'eccezione quando si cerca di caricare un registro di segmento con un valore che non è un multiplo di 8.

Descriptor Tables e Segment Selectors

(http://microlabs.cs.utt.ro/~mmarcu/books/03/p_all2.htm)
I descrittori sono speciali strutture in memoria (grandi 8 byte) che descrivono un segmento e registrano i seguenti attributi di segmento:

- L'indirizzo fisico di partenza del segmento all'interno della memoria (32 bit)
- La lunghezza del segmento (20 bit)
- Informazioni addizionali come i diritti di accesso (il campo Descriptor Privilege Level, DPL) e il tipo di segmento (data, codice)



Poichè lo spazio per la selezione del descrittore all'interno del selettore è limitato solo a 13 bit una Descriptor Table può supportare solo 8192 descrittori (213= 8192). NdLonelyWolf: ma come 13 bit? non erano 16??? Il selettore, oltre a selezionare una Descriptor Table memorizza altre 2 informazioni importantissime (3 bit)! Il bit TI (Table Indicator) determina se il selettore si sta riferendo alla GDT o alla LDT. L'altro bit impiegato è RPL (Requested Privilege Level) e indica il livello di privilegio desiderato.



Facciamo un esempio: Selector: 02FBh
binary: 0000001011111011b -> Vedi Tabella qua sopra. Gli ultimi 3 bit ci dicono che si "parla" di GDT e che il RPL è Level 3. Inoltre la grandezza massima di una Descriptor Table è limitata a 64 KB (64 KB (65536) / 8 = 8192 possible, 8 byte long entries = 8192 Descriptors). Il processore differenzia tra almeno 2 tipi differenti di Descriptor Tables. Uno è la Global Descriptor Table (GDT) che è equalmente accessibile da tutti i processi/programmi, da qui infatti il nome global.

La GDT contiene informazioni di segmentazione che riguardano la grandezza, il punto di partenza e i diritti di accesso a un certo segmento.
Ogni sistema DEVE avere una GDT definita che può essere usata da tutti i programmi e task nel sistema. Opzionalmente possono essere definite una o più LDT (in questo caso la GDT deve contenere un descrittore per la LDT che può stare dovunque nella GDT. Se il sistema supporta LDT multiple ognuna deve avere un selettore e descrittore separato nella GDT). Per esempio, una LDT può esere definita per ogni task in esecuzione oppure più task possono condividere la medesima LDT. La GDT non è un segmento è piuttosto una struttura dati nel Linear Address Space
L'altra tabella di descrittori è la Interrupt Descriptor Table (IDT) anch'essa disponibile per tutti i processi/programmi. Contiene degli speciali descrittori chiamati Gates o Gate-Descriptors. Un Gate-Descriptor viene usato per registrare un indirizzo di una Interrupt Routine in memoria. Un sistema operativo dovrebbe usare la IDT per fornire Exception Handlers per tutte le possibili eccezioni. La IDT si può comparare alla vecchia Interrupt Vector Table della Real Mode. Oltre le 2 Descriptor Table specificate sopra, c'è la possibilità di creare una Descriptor Table per ogni task. Questa speciale tabella viene chiamata Local Descriptor Table (LDT).

Fermiamoci un attimo, cerchiamo di digerire tutta sta roba. Dunque, è tutto chiaro fin qui? Capito la logica della protected mode, questa faccenda dei descrittori? Ma giustamente un programma per potervi accedere deve sapere DOVE andare a cercare in memoria questi descrittori. Queste informazioni sono mantenute in questi 3 nuovi registri. Oltre ai 3 nuovi registri abbiamo anche qualche istruzione nuova da imparare. Ma come si usano? Facciamo un esempio (per gli altri, google :P)

Global Descriptor Table

LGDT mem (LOAD GDT) si aspetta un puntatore (mem) alla seguente struttura:
GDTSTRUC STRUC
           Limit    dw ?
           BaseAddr dd ?
GDTSTRUC ENDS
SGDT mem (STORE GDT) memorizza il valore corrente del registro GDTR nell'operando indicato.

Ecco, siccome ero curioso di fare almeno una prova a usare uno di questi registri mi son codato un programmino idiota di poche righe che fa una SGDT e ne visualizza Base e Limit.


; 6/11/2004
; Show SGDT Base and Limit with MessageBoxes
;
; Lonely Wolf - [email protected]

.386
.model flat, stdcall
option casemap :none

include \masm32\include\windows.inc 
include \masm32\include\kernel32.inc 
includelib \masm32\lib\kernel32.lib 
include \masm32\include\user32.inc 
includelib \masm32\lib\user32.lib 


.data
GDTR df 0  ; 6 byte = 48 bit
MsgBoxC1 db "GDT Base", 0 
MsgBoxC2 db "GDT Limit", 0 

specB db "%.8X", 0
specL db "%.4X", 0
buffer db 8 dup (0)

.code
start:

  lea esi, GDTR
  SGDT fword ptr [esi] 

  invoke wsprintf, addr buffer, addr specB, GDTR+2
  invoke MessageBox, NULL, addr buffer, addr MsgBoxC1, MB_OK 

  mov dx, word ptr GDTR
  
  invoke wsprintf, addr buffer, addr specL,  dx ; Limit è di 16 bit
  invoke MessageBox, NULL, addr buffer, addr MsgBoxC2, MB_OK 


  invoke ExitProcess,0

end start
Il programmino mi mostra 2 MessageBox che mi dicono che Base = C25B000 e Limit = FFF. Ci dobbiamo credere?
Pensando a come verificarlo, mi ricordai del comando GDT del Softice... Fire up!
CTRL+D
GDT

Base = C25B000h Limit = 0FFFh

Direi che ci siamo! :)



Gates

Spendiamo ora due parole su questi "Gates" che sono stati rammentati poco fa. I processori Intel a partire dalla famiglia 80286 possiedono un concetto di protezione che impedisce tra le altre cose l'esecuzione del codice di un programma in un livello di privilegio (anello -> Ring) diverso da quello corrente (il CPL, Current Privilege Level). Questa abilità è importante per la stabilità dell'intero sistema. Un errore di programmazione in un basso livello di privilegio (diciamo più vicino all'utente) influenzerà solo il task corrente, e non l'intero sistema operativo. Tuttavia un applicazione di livello 3 potrebbe aver bisogno di usare una routine del sistema operativo (come una richiesta di memoria, aprire un file...) e per far questo sono nati questi "gates", ovvero una entry point verso i livelli bassi. La figura rende l'idea:



I Gates appartengo agli oggetti di sistema (e usano un proprio formato per i loro descrittori) che tuttavia non spiegheremo in questa sede.

Virtual Addressing

(http://microlabs.cs.utt.ro/~mmarcu/books/03/p_all2.htm) (NdLonelyWolf: Forse è un po'ridondante questo paragrafetto ma certe cose mi sembrano spiegate proprio bene e i disegnini son carini :P)
A differenza della Real Mode, il processore considera la memoria COME virtuale, nel senso che l'indirizzo fisico finale di memoria è sconosciuto a un programma in esecuzione e ogni accesso diretto alla memoria è prevenuto (NdLonelyWolf: anche se ci sono tecniche per farlo ugualmente, vedi sotto windows l'accesso a \device\PhysicalMemory...ma questa è un'altra storia..).
Un virtual address è costituito da una parte a 16 bit chiamata Selettore e da una parte a 32 bit Offset. Come in Modalità Reale, il valore a 16 bit del selettore deve essere prima caricato in un registro di segmento (CS, DS, ES, FS, GS) poi si potrà accedere alla memoria mediante l'offset. Il selettore comunque, non contiene l'indirizzo del segmento in memoria, ma un numero noto come Segment Descriptor. Tale descrittore registra l'indirizzo fisico di partenza così come la sua grandezza insieme a degli attributi addizionali. Il processore usa il selettore come indice in una tabella con i segmenti descrittori (Descriptor Table) per trovare l'indirizzo di partenza del segmento selezionato. Unendo così questi valori, l'indirizzo finale viene costruito e acceduto.



Guardate meglio la figura, non notate niente di strano?
Ve ne mostro un'altra sempre da quell'articolo su x86.org (http://www.x86.org/articles/pmbasics/tspec_a1_doc.htm):



Dal manuale Intel:La prima entry della GDT non è usata dal processore. Un segmento selettore che punta a questa entry della GDT (cioè, un segmento selettore con indice pari a 0 e il flag TI settato a 0) è usato come "Null Segment Selector". Il processore non genera un eccezione quando un registro segmento (Oltre ai registri CS o SS) viene caricato con un null selector. La genera invece quando un registro di segmento inizializzato con un null descriptor cerca di accedere alla memoria provocando una eccezione General Protection #GP. Un altro articolo su x86.org (http://www.x86.org/productivity/nulldescriptor.htm) spiega come questo potrebbe essere usato: "...Il Null descriptor è unico nella GDT ed ha TI = 0 e Index = 0. Molta documentazione asserisce che questa entry debba essere 0. Anche se Intel è un po'ambigua su questo soggetto, non ha mai detto per cosa NON può essere usato. Intel asserisce che l'entry 0 non è mai referenziata dal processore.
Poichè il processore non referenzia mai il NULL descriptor, questo implica che i dati memorizzati in quel posto possono essere usati per qualunque scopo. Il mio uso preferito del NULL descriptor è come puntatore alla GDT stessa! L'istruzione LGDT necessita di un puntatore a 6 byte alla GDT e il NULL descriptor ha 8 byte che non vengono acceduti dalla CPU, rendendolo un candidato ideale per questo scopo (Per voi scettici, ho fatto questo per una decina d'anni). Il protocollo normale usato nell'indirizzamento della GDT è il seguente:

     GDT_PTR   DW   GDT_LENGTH-1
               DD   PHYSICAL_GDT_ADDRESS
Poi nel segmento codice

     LGDT      GDT_PTR
Usando il NULL descriptor come puntatore alla GDT semplifica il segmento data e la concettualizzazione della GDT come segue:

                  +-----------------+
                  |                 |
                  V                 |  Offset
     +------------------------+     |
GDT  |   Pointer to the GDT   |  ---+  00h
     +------------------------+
     |                        |        08h
     +------------------------+
     |    ...  ...  ...  ...  |
Quindi nel segmento codice

LGDT GDT

La variabile GDT_PTR non è più necessaria visto che il NULL descriptor è usato al suo posto. (C'è anche un sorgente che usa questa variante http://www.x86.org/ftp/dloads/int09.zip, guardate soprattuto la macro.

Ok Wolf, ci hai tenuto banco per tutte queste pagine parlando di Protected Mode, Protected Mode, e Protected Mode ma come si fa ad "attivare" questa protected mode?

Boni, ora non voglio sconfinare nelle terre dell' OS Dev perchè di cose da dire ce ne sarebbero veramente TANTE, tanto è tutta roba che mi sto pappando anche io, per cui tra un po' rilascerò altri articoli, anche perchè per rispondere a questa domanda ad esempio, dovremo parlare del boot, del bootloader...cioe' incollarvi un pezzo di codice che ho a portata di clic non mi costa niente, però mi sembra inutile... Intanto dovete/dobbiamo digerire tutta sta roba, aspe', e non sono sceso in dettagli, ho saltato del tutto la parte sui debug register e Control Register (e la pmode si attiva proprio mediante il CR0)...insomma, abbiate fede, non abbandonerò i miei fedeli :D ROTFL Poi mi garbava anche di scrivermi dei dumper personali...vero evil, io e te avevamo anche cominciato, vero signorino? :P mannaggia a te, mannaggia......

Note finali

Saluto Ntoskrnl (ovviamente :P grazie davvero per le tue note e per aver corretto qualche mio strafalcione, attualmente 02/08/2004 in Norvegia, sulle isole Lofoten, mi ha appena mandato sms), albe, Quequero, andreageddon, evilcry (...), Ironspark, i satelliti di marte :D , ZaiRoN, MrCode, la nostra Giulia (giù io aspetto sempre il tuo Tasm vs Masm :P), e tutti quelli che hanno e hanno avuto la sfortuna di conoscermi :P

Disclaimer

Sono contento di averlo fatto, ho imparato molto. Come sono andato?