Programmare in modo protetto

scritto da Rossi Gianluca il 21/04/97

ranger(at)racine.ra.it

Prerequisiti:

Gli esempi più significativi sono scritti in assembler, perciò è necessario conoscerlo, almeno quello 8086. Il 386 mantiene la compatibilità con l’8086, perciò ha gli stessi registri, quelli a 32 bit hanno lo stesso nome di quelli a 16 bit preceduti da una E, così avremo istruzioni tipo: MOV EAX,EBX.

Allo stesso modo avremo EFLAGS (registro FLAGS a 32 bit) EIP (puntatore istruzione a 32 bit) , ESP, EDI, ...

in più: 3 registri di controllo (CR0,CR2,CR3), 6 di debug (DR0,DR1,DR2,DR3,DR6,DR7), 2 di segmento (GS ed FS).

I registri di controllo e quelli di debug sono a 32 bit, mentre i registri di segmento sono sempre a 16 bit. Sono presenti anche altri registri detti di test, ma non hanno una grande utilità, ed inoltre sono presenti solo su 386 e 486, quindi utilizzarli causa incompatibilità coi processori più nuovi.

Se non avete mai programmato a 32 bit, vi sarà sicuramente utile procurarvi la lista completa delle nuove istruzioni e fare qualche prova coi registri a 32 bit in modo reale oppure date un’occhiata agli esempi che trovate assieme a questo documento.

Convenzioni:

1.    i campi a bit vengono indicati tra parentesi quadre, prima il più significativo poi quello meno significativo, ad esempio word[5,0] indica il gruppo di bit 0-5 della word, a volte sostituisco i numeri col codice mnemonico, ad esempio FLAGS[CY]  indica il bit di carry.

2.    l’indicazione A...B indica tutti i valori compresi fra A e B

3.    PMODE e RMODE indicano rispettivamente modo protetto e modo reale.

Introduzione storica:

I primi personal computer dotati di microprocessore 8086 e sistema operativo MS-DOS potevano utilizzare fino ad 1Mb di memoria RAM, della quale i primi 640Kb erano disponibili per i programmi, ed i restanti 384Kb per la ROM ed il BIOS. Col microprocessore 286 INTEL ha introdotto il PMODE, chiamato così perché il sistema operativo viene protetto dai programmi applicativi e può più facilmente controllare le risorse di sistema assegnate a ciascun programma. Il PMODE è infatti necessario quando più programmi vengono eseguiti in un ambiente multitasking, ovvero quando vengono eseguiti “contemporaneamente”. MS-DOS è stato sviluppato per l’8086 che funziona solamente in modo reale: ciò non consente al SO di controllare i programmi che vengono eseguiti, perciò un programma applicativo può facilmente sovrascrivere il SO, e questo non è normalmente una buona cosa...

Per compatibilità comunque INTEL ha progettato i successori dell’8086 in modo che ad ogni reset (ad ogni accensione) il microprocessore disabilitasse tutte le caratteristiche avanzate, e si comportasse come un 8086: ciò permette alle CPU odierne di caricare il vecchio MS-DOS senza problemi.

Alla versione 4.0 del DOS sono state aggiunte alcune utility che permettevano di utilizzare la memoria oltre il primo Mb che normalmente il SO non utilizzava, questa memoria viene detta memoria XMS (eXtended Memory Specification) oppure EMS (Expanded Memory Specification) a seconda di come viene gestita.

La memoria XMS può essere utilizzata da processori 286+, mentre la memoria EMS solo da CPU 386+.

In realtà la memoria EMS veniva già sfruttata dagli 8086, solo che dovevano essere montate schede hardware apposite, il meccanismo software può essere implementato solo su 386+.

Altri “tipi” di memoria sono la memoria UMB (Upper Memory Block) e HMA (High Memory Area).

La memoria UMB cerca di sfruttare le aree non utilizzate dalla ROM e dal BIOS, mentre la HMA rappresenta i primi 64K-16 byte del secondo Mb. Per evitare equivoci è meglio dire che tutti questi tipi di memoria utilizzano la RAM del PC, e differiscono fra loro solo nel modo in cui viene gestita. Avrete già capito che tutte queste sigle creavano molta confusione fra i programmatori, figuriamoci fra gli utenti...

In questo clima le specifiche DPMI (Dos Protected Mode Interface) che permettevano ai programmi di accedere facilmente a tutta la memoria disponibile apparve quasi un miracolo. Il DPMI consente ai programmi di entrare in PMODE, eseguire il loro lavoro utilizzando tutta la RAM disponibile, e tornare al vecchio DOS senza troppa fatica.

Il DPMI può essere sia a 16 che a 32 bit, il DPMI a 16 bit può venire utilizzato da CPU 286+, mentre il DPMI a 32 bit solo da CPU 386+. È da notare che se un programma utilizza il DPMI a 16 bit su una CPU > 286, la CPU emulerà il comportamento a 16 bit.

Il modo protetto e il DPMI:

Entriamo ora più in dettaglio, esaminando da vicino quale differenze ci sono fra il modo reale e quello protetto.

Farò riferimento al modo protetto a 32 bit, poiché quello a 16 bit ha molte limitazioni, e non ha grandi vantaggi. Velocemente queste sono le caratteristiche mancanti al processore 286 :

·      modo V86 (emulazione 8086 in PMODE)

·      protezione delle porte di I/O

·      paginazione della memoria (gestione flessibile della memoria)

·      spazio degli indirizzi separato per ogni task

·      4Gb di memoria fisica indirizzabile (col 286 solo 16Mb)

la CPU 386+ può funzionare perciò in 3 differenti modi: reale (emulazione 8086), protetta, ed 8086 virtuale (abbreviata in V86) nella quale il processore è in PMODE, ma può eseguire codice scritto per programmi in modo reale.

Questa caratteristica è stata essenziale per la diffusione del modo protetto, poiché permette di riutilizzare i vecchi programmi. Il 286 non aveva questa caratteristica, ed inoltre una volta entrato in modo protetto non era più possibile tornare a quello reale (se non con un reset della CPU), ed è per questo motivo che non si sono (quasi) mai visti programmi in modo protetto per 286.

Veniamo ora a qualche esempio significativo :

[codice in RMODE a 16 bit]
  MOV      AX,0
  MOV      ES,AX
  MOV      AX,word ptr ES:[0]

il codice precedente carica la prima word all’indirizzo 0 nel registro ax. Questo è il tipico esempio di codice non funzionante in PMODE, vediamo perché :

in RMODE  gli indirizzi vengono calcolati come : segmento*16+offset,

mentre in PMODE la corrispondenza fra indirizzo lineare (offset) e fisico non è così semplice, ed a meno di situazioni particolari è impossibile (per i programmi) !

Praticamente in PMODE i registri di segmento vengono utilizzati come indici all’interno di una tabella di dimensioni variabili e nella quale ogni elemento ha dimensione 8 byte. La tabella, chiamata GDT (Global Descriptor Table), è un array che può contenere al massimo 8192 elementi (chiamati descrittori) ed i registri di segmento vengono chiamati selettori.


Ogni descrittore descrive una zona di memoria, i campi più importanti che compongono il descrittore sono :

·      base : indica l’indirizzo di partenza della zona di memoria da descrivere

·      limite : indica la lunghezza in byte della zona di memoria - 1

·      flags : una serie di bit che specificano se la zona di memoria può essere letta, scritta oppure eseguita.

Se avessimo un descrittore con base 100 limite 5 (6 byte) e accessibile in lettura/scrittura indicizzato da ES, una istruzione MOV AL, BYTE PTR ES:[0] caricherebbe in AL il byte all’indirizzo 100, mentre una istruzione MOV AL,BYTE PTR ES:[6] non sarebbe legale poiché abbiamo impostato il limite a 5 (stiamo invece leggendo il settimo byte), la CPU in questo caso genera un errore di protezione generale (ben noto a chi utilizza windows...).

Senza entrare in ulteriore dettaglio credo che sia abbastanza chiaro che in PMODE non potete caricare valori arbitrari nei registri di segmento. A volte e` però necessario accedere ad un determinato indirizzo (si pensi alla memoria video all’indirizzo fisico B8000h) e ciò è possibile solo tramite i servizi del DPMI, accessibili tramite l’interrupt 31h.

vediamo ora come poter accedere alla memoria video tramite il DPMI, i passi da seguire sono questi:

1.    creare un descrittore nella tabella

2.    impostare la base del descrittore a B8000h

3.    impostare il limite a 3999 (una pagina di testo e` 4000 byte)

passo 1 : aggiungo un descrittore

  MOV AX,0   ; indico al DPMI che voglio aggiungere un descrittore alla tabella
  MOV CX,1   ; indico che voglio aggiungere un solo descrittore
  INT 31h        ; chiamo DPMI

ora se il flag di carry è a 0 avremo in bx un selettore che punta al descrittore appena creato.

Per default il DPMI crea un descrittore con base 0, limite 0 e di tipo dati. Se il flag di carry è impostato significa che si è verificato un errore (ad esempio abbiamo riempito la tabella dai descrittori).

passo 2 : imposto la base a B8000h

  MOV AX,7  ;indico che voglio impostare la base 
  MOV BX,selettore  ; in BX metto il selettore appena allocato
  MOV CX,0
  MOV DX,0B8000h ; in CX:DX metto la base che voglio
  INT 31h

al solito il flag di carry indica una condizione di errore, altrimenti il descrittore è stato modificato.

L’istruzione MOV BX,selettore non è necessaria visto che al passo 1 il selettore era già contenuto in BX.

passo 3: imposto il limite a 3999

  MOV AX,8              ; indico che imposto il limite
  MOV BX,selettore   ; carico il selettore da modificare
  MOV CX,0              
  MOV DX,3999         ; carico in CX:DX il limite
  INT 31h

ora se tutto è ok in BX avremo il nostro selettore che descrive l’area della memoria video, e potremo

scrivere :

MOV ES,BX                                 ; metti il selettore appena creato in ES
MOV BYTE PTR ES:[0],’A’

per avere la conferma che il nostro lavoro non è stato inutile  !  :-)

Una volta che il selettore non è più usato bisognerebbe liberarlo, in modo che la tabella non si riempia, questo è possibile tramite :

 MOV AX,1             ; indico che voglio liberare il descrittore
 MOV BX,selettore
 INT 31h

sarete lieti di sapere che esiste una funzione che fa tutto questo in automatico :

 MOV AX,2               
 MOV BX,B800h          ; segmento (non indirizzo fisico) utilizzato in RMODE
 INT 31h

ora in AX avremo il selettore già pronto (carry permettendo) !

Avrete notato che il DPMI nasconde il descrittore, l’unico modo per scriversi un proprio descrittore è quello di utilizzare la funzione 0Ch, o 0Bh per leggerlo, ma non è possibile accedere direttamente alla tabella.

Altri servizi del DPMI :

Il DPMI offre numerosi altri servizi per :

1.    gestire i vettori di interrupt

2.    interfacciarsi col modo reale

3.    gestire la memoria virtuale

4.    accedere ai registri speciali

1.   i vettori di interrupt in PMODE non utilizzano lo stesso formato di quelli in RMODE, ma anche qui vengono utilizzati i descrittori, comunque il DPMI richiede solo l’indirizzo dell’interrupt come in RMODE e non potete modificare direttamente la tabella degli interrupt (chiamata IDT, Interrupt Descriptors Table che in PMODE può essere in una qualunque zona di memoria, non è fissa come in RMODE)

2.   è possibile chiamare routine che sono state scritte in RMODE, oppure interrupt DOS/BIOS, in questo caso il DPMI commuta in RMODE, esegue la routine indicata e ritorna i PMODE.

3.   in PMODE è possibile che una parte della memoria non risieda nella RAM, ma su disco, in questo caso è possibile indicare quali porzioni non devono mai essere trasferite su disco, come ad esempio le procedure di interrupt hardware.

4.   accedere ai registri di debug causa un errore di protezione generale, perciò è necessario passare attraverso il DPMI per modificarli, queste funzioni vengono usate solamente dai debugger.

Per ottenere la lista completa delle funzioni DPMI procuratevi le specifiche ufficiali (vedi bibliografia) di seguito descriverò alcune caratteristiche più specifiche e non del tutto ovvie, che spesso risultano utili in programmi che utilizzano gli interrupt hardware, le porte di I/O o che si interfacciano con codice in RMODE.

Un’ultima precisazione: il DPMI è disponibile solamente quando il programma gira in modo protetto, perciò visto che i programmi MS-DOS iniziano in modo reale è necessario provvedere.


Normalmente i compilatori che hanno come target il DOS a 32 bit forniscono un file (chiamato stub)

che esegue questo compito prima che il vostro programma venga eseguito, ma se programmate in assembly puro dovete fare questo da soli :

  [inizio programma]
  pm_offset DW  ?
  pm_segment DW ?
  MOV            AX,1687h
  INT                2Fh          ; non INT 31h !!!
  CMP              AX,0
  JNZ                errore_nessun_dpmi
  ; BX[0] = 1 se il dpmi è a 32bit, altrimenti 16, CL = tipo di processore (2=286,3=386,..)
  ; DX versione DPMI , SI = memoria necessaria (in paragrafi) , ES:DI = indirizzo DPMI
  MOV             [pm_offset],di
  MOV            [pm_segment],es
  CMP            SI,0
  JZ            no_memory
  MOV            AH,48h
  MOV            BX,SI
  INT            21h
 no_memory:
  MOV            AX,0                           ; mettere AX=1 se si vuole il pmode a 32 bit
  CALL            DWORD PTR [pm_offset]
  JC        errore_dpmi
  ; ora il programma è in pmode !!!
  ..
  .
  .
  errore_dpmi:
  errore_nessun_dpmi:
  MOV             AH,4Ch    ;termina il programma (sia in pmode che in rmode)
  INT             21h

naturalmente sarebbe meglio mostrare qualche messaggio di errore invece che terminare il programma senza dir nulla...

Ancora i descrittori !

La spiegazione precedente che ho dato dei descrittori e dei selettori era abbastanza sommaria, ora entrerò più nel dettaglio, vediamo prima i selettori (il valore caricato nei registri di segmento) :

selettore[3,0] = Requestor’s Privilege Level (RPL)

selettore[4] = Table Indicator (TI) se a 0 indica che il descrittore è nella GDT altrimenti nella LDT

selettore[15,4] = indice all’interno della tabella

Il campo RPL viene utilizzato per abbassare il livello di privilegio, verrà descritto più avanti.

Il campo TI indica in quale tabella cercare il descrittore, prima ho parlato della GDT, in realtà esiste un’altra tabella chiamata LDT (Local Descriptor Table) con lo stesso formato, solo che è appunto Locale, nel senso che ogni programma (task)  ha la sua LDT, mentre di GDT ce n’è una sola. Normalmente tramite il DPMI si possono utilizzare solo i descrittori nella LDT.

Il campo indice fornisce l’indice relativo alla tabella ed individua il descrittore.

Ora il formato del descrittore (ogni descrittore è di 8 byte) :

  descriptor struc = {
              word limite0_15;
              word base0_15;
              byte  base15_23;
              byte  access;
              byte  limite16_20;
              byte  base24_31; }

il formato è abbastanza criptico, ma vediamo di chiarire :

la base si calcola “componendo” i campi indicato con basexxx, cioè :

base0_15 + (base15_23 shl 16) + (base24_31 shl 24)

il limite : limite0_15+((limite16_19 AND 0x0F) shl 16)

i 4 bit superiori del campo limite16_19 hanno il seguente significato :

limite16_19[5,4] = sempre 0

limite16_19[6]    = chiamato bit D o B

limite16_19[7]    = granularità, detto bit G

il bit G indica come la CPU interpreta il valore del limite, tramite la formula :

limite shl (G*12) bytes

praticamente se G = 0 il limite rappresenta proprio la lunghezza in byte, altrimenti rappresenta la lunghezza in Pagine (una pagina = 4096 bytes = 1 shl 12). Il massimo valore ammesso per il limite è perciò 1048575 (1Mb-1) se G=0, altrimenti 4Gb-1 (Nota che è impossibile avere un limite ad esempio di 1048577).

il byte access indica che tipo di descrittore si tratta ed il livello di privilegio desiderato :

access[0] = questa zona di memoria è stata letta dalla CPU

access[1] = bit R o W

access[2] = bit C o ED

access[4,3] = 3 oppure 2 per codice o dati

access[6,5] = privilegio

access[7] = presente in memoria

il bit 0 viene impostato dalla CPU quando la zona di memoria descritta viene letta o scritta.

Il bit 1 indica se è possibile leggere (per i descrittori di codice) o scrivere (per i dati) l’area di memoria.

Il bit 2 qui il discorso è abbastanza complesso e (forse) lo spiegherò più avanti, meglio lasciarlo a 0.

I bit 3 e 4 indicano se si tratta di un descrittore di codice (3) oppure di dati (2).

I bit 5 e 6 indicano il privilegio, normalmente è 0 per il sistema operativo e 3 per i programmi.

Il bit 7 indica se la memoria descritta è veramente nella RAM (1) o su disco (0).

Un tipico valore per access è 0xFC per i segmenti di codice leggibili (bit R=1), mentre 0xF2 per i segmenti dati scrivibili (W = 1).

Vediamo ora cosa succede con l’istruzione MOV AL,ES:[1234], supponendo che in ES ci sia il valore 8 : per ES si ha RPL = 0, TI = 0, index =1 e ciò indica che l’offset (1234) è riferito al primo descrittore della GDT (visto che TI=0). Supponiamo ora che il primo descrittore della GDT abbia base = 0x5100, limite 1 pagina (G=1, 4096 byte) e sia un tipico descrittore dati, allora l’istruzione precedente caricherà in AL il byte all’indirizzo 1234+0x5100 (istruzione legale perché 1234 < 4096).

L’ultima cosa che rimane da dire è che il descrittore 0 non viene utilizzato, quindi è possibile caricare 0 in un registro di segmento, ma non è possibile utilizzare tale registro per realizzare accessi in memoria. Credo che ora appaia chiaro perché non è possibile caricare selettori arbitrari (come succede nei programmi scritti per DOS).

Le eccezioni e gli anelli di protezione

Quando la CPU è in PMODE controlla sempre che il programma in esecuzione esegua solo istruzioni legali, vediamo meglio i controlli che fa :

1.    l’offset deve sempre essere minore o uguale al limite

2.    i descrittori devono avere un formato valido

3.    l’operazione (lettura o scrittura) deve essere valida

4.    il programma deve avere sufficiente livello di privilegio

Il punto 1 è già stato esaminato prima, ricordo solo che accedere alla word ad indirizzo 10 causerà la l’accesso ai byte 10 e 11, ed entrambi dovranno essere all’interno del limite.

Il punto 2 si spiega da solo, non potete mettere valori a caso nei descrittori.

Il punto 3 è semplice : i segmenti di codice possono essere eseguiti o al più letti (se R=1),mentre quelli dati possono essere letti o al più scritti (se W=1). Esistono altri tipi di descrittore che non consentono né di leggere né di scrivere, e vengono gestiti solo dalla CPU (ad esempio le zone dove la CPU mette i dati relativi ai task).

Il punto 4 merita una descrizione più dettagliata : tutti i descrittori hanno un livello di privilegio che va da 0 (massimo, usato dal sistema operativo) a 3 (minimo, usato dai programmi) permettendo al sistema operativo di controllare i programmi. In particolare al livello 0 tutto è permesso, e tutto funziona come ci si aspetta, ai livelli inferiori (normalmente il 3 visto che il 2 e l’1 non sono mai usati) invece i programmi non possono eseguire istruzioni “privilegiate” che potrebbero danneggiare il sistema operativo. Se ad esempio un programma utente potesse disabilitare le interruzioni hardware (istruzione CLI) il meccanismo del task switch (che funziona grazie al timer interno) non potrebbe funzionare, altre istruzioni non ammesse sono quelle che consentono di modificare la GDT o la IDT. È inoltre proibito chiamare procedure o utilizzare dati con privilegio superiore a quello attuale (detto CPL = Current Privilege Level che è il DPL del segmento di codice correntemente in esecuzione) .

Quindi  se siete a CPL=3 non potete chiamare una procedura a CPL0 e neanche utilizzare un selettore dati con CPL < 3 : questo causerebbe un errore di protezione generale, che in pratica risulta in una chiamata all’interrupt 0Dh che normalmente termina il programma.

Ci si potrebbe chiedere come è possibile per un programma utente (CPL3) utilizzare il sistema operativo (CPL0) : la risposta sta nei gates (in italiano porte, ma userò la terminologia inglese, perché altrimenti mi vengono in mente tutti quei libri dove si trovano termini come “doppie-parole” o “flag di trappola” o peggio “topo micromorbido” [mouse microsoft  :-))) ]) che possono essere di 3 tipi : interrupt gate, trap gate, o call gate e che vengono gestiti da particolari tipi di descrittori.

Praticamente gli interrupt gate stanno solo nella IDT e hanno lo stesso effetto dell’istruzione INT del modo reale, i trap gate sono uguali ai precedenti solo che non resettano il flag di interrupt, i call gate invece possono venire utilizzati solamente dall’istruzione CALL. Per accedere al sistema operativo si usa quindi un’istruzione INT (come avviene per il DPMI : INT 31h) oppure una call, se ad esempio sappiamo che il selettore 20h punta ad un descrittore di tipo call gate possiamo fare CALL 0020:xxxxx (al posto delle x ci mettete quello che volete, tanto viene ignorato). Ad essere precisi potete utilizzare anche l’istruzione JMP col call gate, ma solo se si ha un trasferimento fra procedure con lo stesso privilegio (perciò è abbastanza inutile visto che si può fare anche senza call-gate).

Questa limitazione deriva dal fatto che se un programma utente facesse qualcosa tipo :

  PUSH             selettore
  PUSH            offset
  JMP               0020:xxxxx

all’arrivo dell’istruzione RET del sistema operativo, il controllo passerebbe all’indirizzo costruito dal programma utente, violando così l’integrità del sistema.

Ora forse risulterà chiaro il significato di RPL (i 2 bit meno significativi contenuti nei registri di semento) : servono unicamente al sistema operativo per abbassare il privilegio dei selettori forniti dal programma utente. Una tipica funzione del sistema operativo è quella di leggere i dati da un file su disco, a questo scopo occorre : l’handle del file, il numero di bytes e l’indirizzo del buffer dove copiare i dati. Se un programma utente passasse (obbligatoriamente sullo stack visto che non potrebbe nei registri) un indirizzo di un’area privilegiata, il sistema operativo che ha il diritto di utilizzare qualsiasi indirizzo, sovrascriverebbe un’area di memoria, violando ancora una volta l’integrità del sistema. Impostando invece il campo RPL al valore del CPL del chiamante tutto si aggiusta. A meno che non scriviate un sistema operativo (o un dos extender particolarmente avanzato) non avrete mai bisogno di utilizzare l’RPL, impostatelo semplicemente a 0.

Riporto qui la tabella delle eccezioni più importanti assieme all’interrupt chiamato

0

Divisione per 0

istruzione DIV

1

eccezione di debug

usata solo dal debug

2

NMI

interruzione non mascherabile

3

debug break-point

usato dal debug

4

istruzione INTO

istruzione INTO con flag O=1

5

istruzione BOUND

indice fuori range

6

istruzione non valida

istruzione sconosciuta

7

coprocessore non presente

 

8

eccezione doppia

una eccezione all’interno di un’altra

9

errore di protezione del coprocessore

 

10

task non valido

 

11

segmento non presente

quando il segmento è su disco

12

stack overflow

 

13

errore di protezione generale

molte cause

14

errore di pagina non presente

simile alla 11, ma per le pagine (4K)

alcune eccezioni hanno significato ovvio, e le prime 6 sono identiche al modo reale. L’eccezione 8 si verifica quando si esegue una operazione non valida all’interno  di un gestore di eccezione, se si verifica un’altra eccezione  (tripla) la CPU si resetta automaticamente :-) !

L’eccezione 11 viene generata se si utilizza un descrittore col bit Present = 0, questo consente l’utilizzo del disco quando la RAM è esaurita, questa eccezione è presente principalmente per compatibilià col 286, col 386 si utilizza l’eccezione 14.

L’eccezione 12 viene generata se il segmento di stack non è presente o si verifica uno stack overflow.

L’eccezione 13 è appunto quella generale, che viene generata ogni volta che si verifica una operazione illegale non gestita dagli altri.

L’eccezione 14 viene utilizzata per implementare la memoria virtuale sul 386, praticamente il principio è simile a quello dell’eccezione 11, ma visto che sul 386 i segmenti possono essere fino a 4Gb sarebbe abbastanza scomodo trasferire così tanti dati in una volta, perciò la memoria viene divisa in un milione di pagine di 4K ciascuna, e il sistema operativo mantiene una tabella dove viene indicato alla cpu quali pagine sono in memoria e quali su disco, l’indirizzo di questa struttura dati è nel registro CR3. Questo modo di funzionamento non è obbligatorio, la CPU può essere in pmode senza che la paginazione sia abilitata, in caso contrario il meccanismo di traduzione degli indirizzi spiegato precedentemente non è completo, ma non approfondirò ulteriormente l’argomento, si sappia solo che l’indirizzo a 32 bit viene scomposto e utilizzato come indice in altre tabelle, e che quindi non ha più nessuna relazione con l’indirizzo fisico, viene perciò detto “indirizzo virtuale”.

Commutazione real mode ó protect mode

La commutazione fra i 2 modi non sarebbe necessario se non fosse che quando siamo in PMODE non vogliamo rinunciare ai servizi DOS e BIOS (in particolare quelli per l’accesso ai file su disco), perciò il DPMI ogni volta che incontra una chiamata al DOS (INT 21h) commuta in RMODE e trasferisce il controllo al DOS. La commutazione si ha anche quando si verifica un interrupt hardware, questo perché i gestori delle interruzioni sono gestiti dal BIOS, che lavora solo in modo reale. Questo meccanismo porta ad alcune complicazioni, poiché mentre il programma lavora il PMODE, se si verifica una commutazione, tutti gli interrupt hardware utilizzeranno la tabella del modo reale, e per il nostro programma potrebbe essere una cosa inaccettabile (si pensi a quando si deve gestire l’interrupt della tastiera o quello del timer, o ancora una scheda sonora...).

Normalmente il DPMI effettua una nuova commutazione e trasferisce il controllo alla procedura giusta, ma non sempre, dipende dalle varie implementazioni.

In altre occasioni è necessario chiamare una procedura in PMODE da una in RMODE, ad esempio il driver del mouse consente di “agganciare” una procedura al gestore degli eventi, solo che si aspetta di trovare una procedura in RMODE. A questo scopo servono le “real-mode callback”, praticamente

viene fornito l’indirizzo di una procedura che commuta in PMODE , chiama la procedura e poi ritorna in RMODE.

I più attenti si saranno anche accorti che gli interrupt 8..15 vengono usati dalla CPU per le eccezioni, ma in RMODE  sono usati per gli interrupt hardware, per questo pasticcio dovete ringraziare l’IBM che quando nei manuali INTEL ha letto che gli int da 0 a 19h sono riservati alla CPU a pensato bene di fregarsene...

per risolvere il problema è necessario riprogrammare il gestore delle interruzioni (PIC) ogni volta che si commuta fra il PMODE e RMODE. Questo non è sempre vero per 2 motivi :

1.    non tutti gli extender riprogrammano il PIC

2.    non tutti gli extender commutano in RMODE

alla prima classe appartengono pochi extender, credo che sono quelli shareware siano di questo tipo e questo solo per motivi di prestazioni (riprogrammare il PIC è un’operazione abbastanza lenta).

Alla seconda classe di extender appartengono tutti i sistemi operativi che emulano il dos ed alcuni dos extender, ed utilizzano il cosiddetto modo V86, nel quale il 386 emula il comportamento di un 8086 trattandolo come un task con privilegio 3, questa è la soluzione che offre maggior sicurezza.

Ottimizzare le prestazioni in pmode

Quando dite ad un utente medio che il vostro programma è a 32 bit, sicuramente farete un figurone, magari ne esiste uno 16 bit che fa la stessa cosa e anche meglio, ma 32 bit significa “nuova applicazione” proprio come windows 95 !! (infatti i CD musicali che hanno solo 16 bit appartengono ormai al passato, ora escono i DVD che saranno sicuramente a 32 bit ... vero?)

Scherzi a parte se avete un normale programma in modo reale e lo ricompilate per il DPMI a 16 bit, state sicuri che girerà più lento (a meno che non facesse swap su disco per mancanza di RAM).

Se ricompilate tutto a 32 bit, dipende dal tipo di programma, normalmente il codice è più grosso (se programmate in assembly ricordate di usare USE32 per il segmento di codice e di evitare codice a 16 bit, quello a 8 bit è permesso se si ottimizza in dimensione, ma per la velocità è meglio evitare), però la gestione della memoria sarà notevolmente semplificata, e ciò può influire parecchio sulle prestazioni. Quando siete in PMODE  dovete tenere presente che la CPU non gradisce le modifiche  ai registri di segmento, in special modo il Pentium Pro™ (ecco perché perde dei punti in RMODE). Il motivo è ovvio : cambiare il selettore costringe la CPU a consultare la GDT o la LDT per cercare il descrittore e leggerne il contenuto. Altro consiglio : accedere ai dati in modo ordinato (sequenziale), perché si sfrutta meglio la cache ed inoltre quando la paginazione è attivata la CPU deve tradurre gli indirizzi virtuali in fisici, e se non ha questi dati nei registri interni al processore deve accedere alla RAM.

Questi registri sono 32, ed ognuno  tiene traccia dell’indirizzo di una pagina (4K), perciò si potrà accedere a 32*4K=128K di memoria contemporaneamente senza perdita di prestazioni.

Non eseguite istruzioni privilegiate: esse generano un errore di protezione generale, e costringono il DPMI a emulare queste istruzioni via software : calcolate circa 500 clock persi per queste istruzioni. Inoltre evitate il più possibile lo switch fra RMODE e PMODE, per darvi un’idea del sovraccarico della CPU vi farò un esempio : col mio 486 ed il dos extender DOS32 riesco a programmare il timer in modo che generi circa 50000 interrupt al secondo, mentre usando l’extender della Borland che ha una gestione più elaborata il massimo è circa 5000, una differenza di 10 volte !

In una finestra DOS (sotto windows95) invece non si arriva nemmeno a 500 !!

Se la gestione degli interrupt non è un problema, ricordate che anche gli accessi al disco avvengono in RMODE.

Appendice:

Istruzioni privilegiate:

CLTS,  HLT, LGDT, LIDT,  LLDT, LMSW, SMSW, LTR, MOV registri CRx e DRx .

Queste istruzioni possono venire usate solo dal sistema operativo e se usate causano la terminazione del programma, con l’eccezione di MOV reg,CR0 che speso viene emulata via software (ma non MOV CR0,reg !). In questo caso il valore che viene ritornato potrebbe non essere quello reale, ad esempio se provate sotto windows95, vi ritornerà il valore 0, che è sicuramente “errato”. Viene usato questo trucco per fare credere al programma di essere in RMODE, quando però cercate di usare istruzioni come MOV CR0,reg allora windows95 vi avverte che il programma deve essere eseguito in “modalità MS-DOS”.

Istruzioni sensibili all’IOPL (I/O Privilege Level) :

L’IOPL è il livello di privilegio necessario per eseguire operazioni di I/O, tutte le volte che CPL > IOPL il programma non può eseguire le seguenti istruzioni : CLI, IN, INS, OUT, OUTS, ed ha limitazioni nell’eseguire PUSHF, POPF, IRET. L’ IOPL di un programma è rappresentato dai bit 12 e 13 di EFLAGS, normalmente i programmi che girano in una finestra DOS hanno IOPL=0 (e non possono eseguire le istruzioni precedenti) mentre i normali DOS extender concedono IOPL=3. Avere IOPL=0 significa che ogni istruzione precedente deve essere emulata via software, e quindi sono da considerarsi molto lente, e vanno utilizzate solamente quando indispensabili. Le istruzioni PUSHF, POPF ed IRET possono essere utilizzate normalmente, solamente che se CPL > IOPL non modificano il flag di interrupt, che viene gestito solo dal sistema operativo (anche con una istruzione CLI il sistema operativo è libero di non abilitare il flag di interrupt !). attenzione quindi a tutti i programmi che usano pesantemente le istruzioni IN/OUT (ad esempio i programmi che usano il modo grafico X) e non fare assunzioni sul flag di interrupt. A questo scopo esistono apposite funzioni del DPMI che permettono di verificare questo flag.

I registri di controllo :

I registri di controllo (CRx) possono venire utilizzati solo al livello 0, oppure quando si è in RMODE (non V86 !!).  Il registro CR0 contiene una serie di flag che indicano al processore come interpretare le istruzioni, i flag più importanti sono :

1.    CR0[0] = Protection Enable (PE) se posto a 1 il processore esegue le istruzioni in modo protetto, se 0 in modo reale

2.    CR0[31] = Paging enabled (PG) abilitazione della paginazione

ricordate di fare seguire una istruzione JMP dopo ogni modifica a CR0.

il registro CR2 contiene l’indirizzo della pagina non presente quando si verifica un errore di pagina (Page Fault, int 14).

Il registro CR3 contiene l’indirizzo per le tabelle di traduzione degli indirizzi virtuali in indirizzi fisici.

Il codice a 32 bit ed il bit D:

Il 386 può gestire solamente 2 tipi di dato : a 8 bit oppure quello di default, che può essere a 16 o a 32 bit. Per specificare quale tipo di dati si vuole utilizzare si deve agire sul bit limite16_19[6] che per i segmenti di codice è indicato con D (default). Se D=1 il processore interpreta le istruzioni a 8 e a 32 bit, altrimenti a 8 e a 16 bit. Un esempio chiarirà le idee :

la sequenza di byte 89 D8 indica MOV AX,BX (provate col debug del DOS), ma se il bit D=1 il processore interpreterà come MOV EAX,EBX ! esiste un prefisso (byte 66h) detto “modificatore di dimensione” che inverte il comportamento di default, cioè se volete utilizzare l’istruzione MOV AX,BX in un segmento con D=1 la codifica corretta è 66 89 D8,

ed il 66 iniziale informa la CPU che la prossima istruzione è a 16 bit, e non a 32 come si aspetta. In modo reale succede la cosa inversa : 66 89 D8 significa MOV EAX,EBX poiché in RMODE la dimensione di default è 16 bit. Questo per dire che se usate segmenti a 32 bit, dovreste evitare le istruzioni a 16bit, e viceversa, poiché il prefisso 66 va messo su ogni istruzione che non rispetta la dimensione di default, e alla fine ci potrebbe essere un aumento delle dimensioni del codice inaccettabile (diciamo sul 20% se uno fa tutto alla rovescia).

Non esiste solo il DPMI :

Il DPMI consente ai programmi di utilizzare il modo protetto abbastanza facilmente, tuttavia sono presenti altri modi per entrare in PMODE. Il VCPI (Virtual Control Program Interface)  ad esempio, ma è molto più complicato da utilizzare. Comunque è più facile trovare un PC col supporto VCPI che DPMI, poiché l’EMM386 per emulare la memoria espansa pone il processore in modo V86 (perciò tutto il DOS viene eseguito in V86) e consente ai programmi di utilizzare i servizi VCPI. A differenza del DPMI il VCPI può consentire ai programmi di entrare nel livello 0 (infatti windows si avvia anche con EMM386, mentre non lo fa se avete un server DPMI in memoria) però bisogna gestirsi da soli la GDT e la IDT.

Naturalmente è possibile utilizzare il modo protetto senza nessuna interfaccia particolare, quando ci si trova in RMODE e non sono presenti né DPMI né VCPI è possibile entrare in PMODE abilitando CR0[PE], creandosi le tabelle GDT e IDT e gestendosi tutto a mano.

Per tornare in RMODE basta resettare CR0[PE], ripristinare il valore originale della IDT (base 0 limite FFFh) e caricare nei registri di segmento dei valori corretti per il modo reale (base giusta e limite 64K). Se non impostate il limite dei selettori in modo corretto avrete il cosiddetto “flat real”, ovvero potrete accedere a tutta la memoria utilizzando un offset a 32 bit, ma in modo reale !

Conclusioni:

Se siete arrivati fino a qui capendo tutto, complimenti ! siete pronti per il PMODE !

Se volete informazioni più dettagliate potete procurarvi le specifiche del 386 al sito INTEL.

Io ho iniziato leggendo quello, e non è stato facile, spero che con questa mia introduzione risulterà più comprensibile. Ho evitato di descrivere alcune caratteristiche particolarmente ostiche, e comunque poco usate sotto DOS, ad esempio ho descritto solo qualche tipo di descrittore, non ho parlato dei task, ed ho fatto solo alcuni accenni al modo V86, inoltre ho evitato di descrivere i meccanismi della paginazione, troverete tutto questo in testi più avanzati (vedi bibliografia).

Naturalmente i suggerimenti sono sempre i benvenuti, sicuramente avrò commesso qualche errore (il primo è quello di avere scritto questo documento con Word e quindi la versione HTML un pò ne risente) scrivetemi pure in email.

Bibliografia:

1.    dpmispec.txt, file di testo di 170K circa reperibile in molti siti internet o BBS, descrive in modo dettagliato le specifiche del DPMI v0.9

2.    dpmi100.txt, file di testo di 130K, aggiornamento alla versione 1.0 delle specifiche DPMI

3.    386intel.txt, file di testo di circa 850K, descrive in modo dettagliato tutte le caratteristiche del 386, non riporta però nessun esempio utile alla comprensione.

4.    DOS32, dos extender in modo protetto, molto facile da utilizzare, con numerosi esempi ed un debugger. Lo trovate un po’ ovunque.

5.    PMODE, il famoso dos extender di Tran, riporta anche i sorgenti (che sono incomprensibili, a parere mio). Lo trovate ovunque.

6.    Ralf Brown interrupt list, in particolare i file opcodes.lst e 86bugs.lst. tutta la lista occupa circa 2Mb (copmpressi) e contiene veramente di tutto, da non perdere. Il sito internet ufficiale è http://www.pobox.com/~ralf/files.html

7.     Games Programmers Encyclopedia, contiene un corso di assembler, e la lista di tutte le istruzioni dei processori INTEL (fino al 486), il sito internet ftp ufficiale è teeri.oulu.fi directory /pub/msdos/programming/gpe

8.    “il microprocessore 80386/387” Stephen P.Morse, Eric J.Isaacson e Douglas J. Albert, ed Tecniche Nuove. Indispensabile a chi vuole approfondire la conoscenza del 386, descrive  tutte le caratteristiche in modo chiaro e dettagliato spiegando spesso l’origine storica di alcune caratteristiche che potrebbero sembrare oscure. Non riporta programmi pronti all’uso, ma non è troppo difficile scriverne seguendo le indicazioni riportate. Strutturato in 7 capitoli per un totale di 360 pagine : Introduzione, l’organizzazione della macchina, le istruzioni fondamentali, l’aritmetica in virgola mobile, la segmentazione e la compatibilità, il punto di vista del sistema operativo, il calcolo in virgola mobile ad alta velocità. L’ultimo capitolo riguarda il coprocessore weitek e non il 387. Costa 39.000, la truduzione richiederebbe più cura.


Ultima modifiche 02/07/1997.