[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home] [volume] [parte]


Capitolo 609.   GDT

Nei microprocessori x86-32, per poter accedere alla memoria quando si sta operando in modalità protetta,(1) è indispensabile dichiarare la mappa dei segmenti di memoria attraverso una o più tabelle di descrizione. Tra queste è indispensabile la dichiarazione della tabella GDT, ovvero global description table, collocata nella stessa memoria centrale.(2)

La tabella GDT deve essere predisposta dal sistema operativo, prima di ogni altra cosa; di norma ciò avviene prima di far passare il microprocessore in modalità protetta, in quanto tale passaggio richiede che la tabella sia già presente per consentire al microprocessore di conoscere i permessi di accesso. Tuttavia, se si utilizza un programma per l'avvio del sistema operativo, si potrebbe trovare il microprocessore già in modalità protetta, con una tabella GDT provvisoria, predisposta in modo tale da consentire l'accesso alla memoria senza limitazioni e in modo lineare.(3) Per esempio, questo è ciò che avviene con un sistema si avvio aderente alle specifiche multiboot, come nel caso di GRUB 1. Ma anche così, il sistema operativo deve comunque predisporre la propria tabella GDT, rimpiazzando la precedente.

Negli esempi che appaiono nel capitolo, si fa riferimento alla predisposizione di una tabella GDT, a partire da un sistema che è già in modalità protetta, essendo consentito di accedere a tutta la memoria, linearmente, senza alcuna limitazione.

609.1   Privilegi

Negli esempi che vengono mostrati, i privilegi corrispondono sempre all'anello zero, onde evitare qualunque tipo di complicazione. Tuttavia, è evidente che un sistema operativo comune deve invece gestire in modo più consapevole questo problema.

609.2   Organizzazione e contenuti della tabella GDT

Inizialmente conviene considerare la tabella GDT in modo semplificato, organizzata a righe e colonne, come si vede nello schema successivo, dal momento che nella realtà i dati sono spezzettati e sparpagliati nello spazio a disposizione. Le righe di questa tabella possono essere al massimo 8 192 e ogni riga costituisce il descrittore di un segmento di memoria, il quale, eventualmente, può sovrapporsi ad altre aree.(4)

tabella GDT semplificata

Come si vede, gli elementi dominanti delle voci che costituiscono la tabella, ovvero dei descrittori di segmento, sono la «base» e il «limite». Il primo rappresenta l'indirizzo iniziale del segmento di memoria a cui si riferisce il descrittore; il secondo rappresenta in linea di massima l'estensione di questo segmento.

Il modo corretto di interpretare il valore che rappresenta il limite dipende da due attributi: la granularità e la direzione di espansione. Come si può vedere il valore attribuibile al limite è condizionato dalla disponibilità di soli 20 bit, con i quali si può rappresentare al massimo il valore FFFFF16, pari a 104857510. L'attributo di granularità consente di specificare se il valore del limite riguarda byte singoli o se rappresenta multipli di 4 096 byte (ovvero 100016 byte). Evidentemente, con una granularità da 4 096 byte è possibile rappresentare valori da 0000000016 a FFFFF00016.

Figura 609.2. Confronto tra un limite da interpretare in modalità normale (a sinistra), rispetto a un limite da interpretare secondo un attributo di espansione in basso.

base e limite

La direzione di espansione serve a determinare come si colloca l'area del segmento; si distinguono due casi: da base, fino a base+(limite×granularità) incluso; oppure da base+(limite×granularità)+1 a FFFFFFFF16. Il concetto è illustrato dalla figura già apparsa.

609.3   Struttura effettiva della tabella GDT

Nella realtà, la tabella GDT è formata da un array di descrittori, ognuno dei quali è composto da 8 byte, rappresentati qui in due blocchi da 32 bit, come nello schema successivo, dove viene evidenziata la porzione che riguarda l'indicazione dell'indirizzo iniziale del segmento di memoria:

base

L'indirizzo iniziale del segmento di memoria va ricomposto, utilizzando i bit 16-31 del primo blocco a 32 bit; quindi aggiungendo a sinistra i bit 0-7 del secondo blocco a 32 bit; infine aggiungendo a sinistra i bit 24-31 del secondo blocco a 32 bit. Anche il valore del limite del segmento di memoria risulta frammentato:

limit

Il limite del segmento di memoria va ricomposto, utilizzando i bit 0-15 del primo blocco a 32 bit, aggiungendo a sinistra i bit 16-19 del secondo blocco a 32 bit. Nel disegno successivo si illustrano gli altri attributi, considerando che si tratti di un descrittore di memoria per codice o dati; in altri termini, il bit 12 (il tredicesimo) del secondo blocco a 32 bit deve essere impostato a uno:

attributi

A proposito del bit che rappresenta il tipo di espansione o la conformità, in generale va usato con il valore zero, a indicare che il limite rappresenta l'espansione del segmento, a partire dalla sua origine, oppure che l'interpretazione del codice è da intendere in modo «conforme» per ciò che riguarda i privilegi. Il bit di accesso in corso (il bit numero 8, nel secondo blocco da 32 bit) viene aggiornato dal microprocessore, ma normalmente solo se all'inizio appare azzerato.

Va ricordato che i microprocessori x86-32 scambiano l'ordine dei byte in memoria. Pertanto, gli schemi mostrati sono validi solo se l'accesso alla memoria avviene a blocchi da 32 bit, perché diversamente occorrerebbe tenere conto di tali scambi. Per questa stessa ragione, il descrittore di un segmento di memoria è stato mostrato diviso in due blocchi da 32 bit, invece che in uno solo da 64, dato che l'accesso non può avvenire simultaneamente per modificare o leggere un descrittore intero.

Quando si deve predisporre una tabella GDT prima di essere passati al funzionamento in modalità protetta, ovvero quando non ci si può avvalere di un sistema di avvio che offre una modalità protetta provvisoria, occorre ragionare a blocchi da 16 bit, non essendoci la possibilità di usare istruzioni a 32. Pertanto, ognuno dei blocchi descritti va invertito, come si può vedere nel disegno successivo:

blocchi a 16 bit

609.4   Tabella GDT elementare

Una tabella GDT elementare, con la quale si voglia dichiarare tutta la memoria centrale (al massimo fino a 4 Gibyte), in modo lineare e senza distinzione di privilegi tra il codice e i dati, richiede almeno tre descrittori: un descrittore nullo iniziale, obbligatorio; un descrittore per il segmento codice che si estende su tutta la superficie della memoria; un altro descrittore identico, ma riferito ai dati. In pratica, a parte il descrittore nullo iniziale, servono almeno due descrittori, uno per il codice e l'altro per i dati, sovrapposti, entrambi attribuiti all'anello zero (quello principale). Questa è di norma la situazione che viene proposta negli esempi in cui si dimostra il funzionamento di un kernel elementare; nel disegno della figura 609.7 si può vedere sia in binario, sia in esadecimale.

Figura 609.7. Esempio di tabella GDT elementare.

gdt esempio

609.5   Costruzione di una tabella GDT

Per costruire una tabella GDT è complicato usare una struttura per tentare di riprodurre la suddivisione degli elementi di un descrittore di segmento; pertanto, qui viene proposta una soluzione con una suddivisione che si riduce a due blocchi da 32 bit:

#include <stdint.h>
typedef struct {
    uint32_t  w0;
    uint32_t  w1;
} gdt_descriptor_t;

La funzione successiva riceve come argomento un array di descrittori di segmento, con l'indicazione dell'indice a cui si vuole fare riferimento e degli attributi che gli si vogliono associare. Va però osservato che i nomi dei parametri access e granularity rappresentano una semplificazione, nel senso che access si riferisce agli attributi che vanno dal segmento presente in memoria fino al segmento in corso di utilizzo, mentre granularity va dalla granularità fino ai bit riservati e disponibili:

#include <stdint.h>
static void
gdt_descriptor_set (gdt_descriptor_t *gdt,
                    int               descr,
                    uint32_t          base,
                    uint32_t          limit,
                    uint32_t          access,
                    uint32_t          granularity)
{
    //
    // Azzera la voce selezionata.
    //
    gdt[descr].w0 = 0;
    gdt[descr].w1 = 0;
    //
    // Trasferisce l'ampiezza del segmento (limit).
    //
    gdt[descr].w0 = gdt[descr].w0 | (limit & 0x0000FFFF);
    gdt[descr].w1 = gdt[descr].w1 | (limit & 0x000F0000);
    //
    // Trasferisce l'indirizzo iniziale del segmento (base).
    //
    gdt[descr].w0 = gdt[descr].w0 | ((base << 16) & 0xFFFF0000);
    gdt[descr].w1 = gdt[descr].w1 | ((base >> 16) & 0x000000FF);
    gdt[descr].w1 = gdt[descr].w1 | (        base & 0xFF000000);
    //
    // Trasferisce gli attributi di accesso e altri attributi vicini.
    //
    gdt[descr].w1 = gdt[descr].w1 | ((access << 8) & 0x0000FF00);
    //
    // Trasferisce la granularità e altri attributi vicini.
    //
    gdt[descr].w1 = gdt[descr].w1 | ((granularity << 20) & 0x00F00000);
}

Per usare questa funzione occorre prima dichiarare l'array di descrittori di segmento. L'esempio seguente serve a riprodurre la tabella elementare della figura 609.7:

...
    static gdt_descriptor_t gdt[3];  // Tabella GDT con tre voci.
...
    gdt_descriptor_set (gdt, 0, 0,       0,    0,   0); // Obbligatorio.
    gdt_descriptor_set (gdt, 1, 0, 0xFFFFF, 0x9A, 0xC); // Codice conforme.
    gdt_descriptor_set (gdt, 2, 0, 0xFFFFF, 0x92, 0xC); // Dati.
...

Nell'esempio, l'array gdt[] viene creato specificando l'uso di memoria «statica», nell'ipotesi che ciò avvenga dentro una funzione; diversamente, può trattarsi di una variabile globale senza vincoli particolari.

609.6   Attivazione della tabella GDT

La tabella GDT può essere collocata in memoria dove si vuole (o dove si può), ma perché il microprocessore la prenda in considerazione, occorre utilizzare un'istruzione specifica con la quale si carica il registro GDTR (GDT register) a 48 bit. Questo registro non è visibile e si carica con l'istruzione LGDT, la quale richiede l'indicazione dell'indirizzo di memoria dove si articola una struttura contenente le informazioni necessarie. Si tratta precisamente di quanto si vede nel disegno successivo:

puntatore a tabella gdt

In pratica, vengono usati i primi 16 bit per specificare la grandezza complessiva della tabella GDT e altri 32 bit per indicare l'indirizzo in cui inizia la tabella stessa. Tale indirizzo, sommato al valore specificato nel primo campo, deve dare l'indirizzo dell'ultimo byte della tabella stessa.

Dal momento che la dimensione di un descrittore della tabella GDT è di 8 byte, il valore del limite corrisponde sempre a 8×n-1, dove n è la quantità di descrittori della tabella. Così facendo, si può osservare che gli ultimi tre bit del limite sono sempre impostati a uno.

Nel disegno è stato mostrato chiaramente che il primo campo da 16 bit va considerato in modo separato. Infatti, si intende che l'accesso in lettura o in scrittura vada fatto lì esattamente a 16 bit, perché diversamente i dati risulterebbero organizzati in un altro modo. Pertanto, nel disegno viene chiarito che il campo contenente l'indirizzo della tabella, inizia esattamente dopo due byte. In questo caso, con l'aiuto del linguaggio C è facile dichiarare una struttura che riproduce esattamente ciò che serve per identificare una tabella GDT:

#include <stdint.h>
typedef struct {
    uint16_t  limit;
    uint32_t  base;
} __attribute__ ((packed)) gdtr_t;

L'esempio mostrato si riferisce all'uso del compilatore GNU C, con il quale è necessario specificare l'attributo packet, per fare in modo che i vari componenti risultino abbinati senza spazi ulteriori di allineamento. Fortunatamente, il compilatore GNU C fa anche la cosa giusta per quanto riguarda l'accesso alla porzione di memoria a cui si riferisce la struttura.

Avendo definito la struttura, si può creare una variabile che la utilizza, tenendo conto che è sufficiente rimanga in essere solo fino a quando viene acquisita la tabella GDT relativa dal microprocessore:

...
    gdtr_t gdtr;
...

Per calcolare il valore che rappresenta la dimensione della tabella (il limite), occorre moltiplicare la dimensione di ogni voce (8 byte) per la quantità di voci, sottraendo dal risultato una unità. L'esempio presuppone che si tratti di tre voci in tutto:

...
    gdtr.limit = ((sizeof (gdt_descriptor_t)) * 3) - 1;
...

L'indirizzo in cui si trova la tabella GDT, può essere assegnato in modo intuitivo:

...
    gdtr.base  = (uint32_t) &gdt[0];
...

609.7   Verifica della tabella GDT

Le prime volte che si fanno esperimenti per ottenere l'attivazione di una tabella GDT, sarebbe il caso di verificare il contenuto di questa, prima di chiedere al microprocessore di attivarla. Infatti, un piccolo errore nel contenuto della tabella o in quello della struttura che contiene le sue coordinate, comporta generalmente un errore irreversibile. D'altra parte, proprio la complessità dell'articolazione delle voci nella tabella rende frequente il verificarsi di errori, anche multipli.

Ammesso di poter lavorare in una condizione tale da poter visualizzare qualcosa con una funzione printf(), la funzione seguente consente di vedere il contenuto di una tabella GDT, partendo dall'indirizzo della struttura che rappresenta il registro GDTR da caricare, ovvero dallo stesso indirizzo che dovrebbe ricevere il microprocessore, con l'istruzione LGDT:

#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
static void
gdt_show (gdtr_t *gdtr)
{
    gdt_descriptor_t *gdt     = (gdt_descriptor_t *) gdtr->base;    
                 int  entries = (gdtr->limit + 1) 
                                / (sizeof (gdt_descriptor_t));
                 int  descr;
             //    
       uint32_t  base;
       uint32_t  limit;
       uint32_t  access;
       uint32_t  granularity;
    //
    printf ("gdt base: 0x%08X  limit: 0x%04X\n", gdtr->base, gdtr->limit);
    //
    printf ("       base       limit    access granularity\n");
    //
    for (descr = 0; descr < entries; descr++)
      {
        base = limit = access = granularity = 0;
        //
        // Indirizzo del segmento di memoria.
        //
        base = base | ((gdt[descr].w0 >> 16) & 0x0000FFFF);
        base = base | ((gdt[descr].w1 << 16) & 0x00FF0000);
        base = base | ((gdt[descr].w1      ) & 0xFF000000);
        //
        // Estensione del segmento di memoria.
        //
        limit = limit | (gdt[descr].w0 & 0x0000FFFF);
        limit = limit | (gdt[descr].w1 & 0x000F0000);
        //
        // Attributi di accesso e di tipo.
        //
        access = access | ((gdt[descr].w1 >> 8) & 0x000000FF);
        //
        // Attributi di granularità.
        //
        granularity = granularity | ((gdt[descr].w1 >> 20) & 0x0000000F);
        //
        // Visualizza la voce della tabella.
        //
        printf ("gdt[%i] 0x%08" PRIX32 " 0x%06" PRIX32
                " 0x%04" PRIX32 " 0x%04" PRIX32 "\n",
                descr, base, limit, access, granularity);
    }
}

Stando agli esempi già fatti, si dovrebbe vedere una cosa simile al testo seguente:

gdt base: 0x00106044  limit: 0x0017
       base       limit    access granularity
gdt[0] 0x00000000 0x000000 0x0000 0x0000
gdt[1] 0x00000000 0x0FFFFF 0x009A 0x000C
gdt[2] 0x00000000 0x0FFFFF 0x0092 0x000C

Il valore 1716 corrisponde a 2310, pertanto, in questo caso, la tabella inizia all'indirizzo 0010604416 e termina all'indirizzo 0010605B16 compreso; inoltre la tabella occupa complessivamente 24 byte.

609.8   Istruzioni per l'attivazione

Per rendere operativo il contenuto della tabella GDT, va indicato al microprocessore l'indirizzo della struttura che contiene le coordinate della tabella stessa, attraverso l'istruzione LGDT (load GDT). Negli esempi seguenti si utilizzano istruzioni del linguaggio assemblatore, secondo la sintassi di GNU AS; in quello seguente, in particolare, si suppone che il registro EAX contenga l'indirizzo in questione:

...
    lgdt (%eax)         # EAX contiene l'indirizzo della struttura.
...

A questo punto, la tabella non viene ancora utilizzata dal microprocessore e occorre sistemare il valore di alcuni registri:

...
    mov  $0x10, %eax    # Selettore di segmento.
    mov  %ax, %ds
    mov  %ax, %es
    mov  %ax, %fs
    mov  %ax, %gs
    mov  %ax, %ss
...

I registri in cui si deve intervenire sono DS, ES, FS, GS e SS, ma per assegnare loro un valore, occorre passare per la mediazione di un altro registro che in questo caso è AX. Il registro DS (data segment) e poi tutti gli altri citati, devono avere un selettore di segmento che punti al descrittore del segmento dati attuale, con la richiesta di privilegi adeguati e la specificazione che trattasi di un riferimento a una tabella GDT. Il disegno della figura successiva mostra come va interpretato il valore dell'esempio.

Figura 609.20. Selettore del segmento dati che riguarda sia DS con gli altri registri affini per l'accesso ai dati, sia SS, per la gestione della pila dei dati.

segmento dati

Come si può vedere nel disegno, il valore 1016 assegnato ai registri destinati ai segmenti di dati, contiene l'indice 210 per la tabella GDT, con la richiesta di privilegi pari a zero (ovvero il valore più importante). Il descrittore con indice due della tabella GDT è esattamente quello che è stato predisposto per i dati.

Subito dopo deve essere specificato il valore del registro CS (code segment) che in questo caso deve corrispondere a un selettore valido per il descrittore del segmento predisposto nella tabella GDT per il codice. In questo caso il valore è 0816, come si può vedere poi dalla figura successiva. Tuttavia, non è possibile assegnare il valore al registro e per ottenere il risultato, si usa un salto incondizionato a un'etichetta poco distante, ma con l'indicazione dell'indirizzo di segmento:

...
    jmp $0x08, $flush
flush:
...

Figura 609.22. Selettore del segmento codice.

segmento codice

Il listato successivo rappresenta una soluzione completa per l'attivazione della tabella GDT, a partire dall'indirizzo della struttura che ne contiene le coordinate:

.globl  gdt_load
#
gdt_load:
    enter $0, $0
    .equ gdtr_pointer,  8          # Primo argomento.
    mov  gdtr_pointer(%ebp), %eax  # Copia il puntatore in EAX.
    leave
    #
    lgdt (%eax)         # Carica il registro GDTR dalla posizione
    #                   # a cui punta EAX.
    #
    mov  $0x10, %eax
    mov  %ax, %ds
    mov  %ax, %es
    mov  %ax, %fs
    mov  %ax, %gs
    mov  %ax, %ss
    jmp $0x08, $flush
flush:
    ret

Il codice mostrato costituisce una funzione che nel linguaggio C ha il prototipo seguente:

gdt_load (void *gdtr);

Va osservato che l'istruzione LEAVE viene usata prima di passare all'istruzione LGDT; diversamente, se si tentasse si mettere dopo l'etichetta a cui si salta nel modo descritto (per poter impostare il registro CS), l'operazione fallirebbe.

609.9   Riferimenti


1) La modalità protetta è quella che consente di accedere alla memoria oltre il limite di 1 Mibyte.

2) Alla tabella GDT possono essere collegate delle tabelle LDT, ovvero local description table, con il compito di individuare delle porzioni di memoria per conto di processi elaborativi singoli.

3) Per accesso lineare alla memoria si intende che l'indirizzo relativo del segmento corrisponde anche all'indirizzo reale della memoria stessa. In inglese si usa il termine flat memory.

4) Per rappresentare i numeri da 0 a 8 191 servono precisamente 13 bit. Nei selettori di segmento si usano i 13 bit più significativi per individuare un descrittore.


Appunti di informatica libera 2008 --- Copyright © 2000-2008 Daniele Giacomini -- <appunti2 (ad) gmail·com>


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome gdt.htm

[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home]

Valid ISO-HTML!

CSS validator!

Gjlg Metamotore e Web Directory