Menu

Budowa systemu

Budowa komputera

Przykładowe systemy

Programowanie

Linki



Global Descriptor Table

Global Descriptor Table (GDT) jest głównym elementem opisującym pamięć w trybie chronionym. Wraz z GDT, należy wprowadzić pojęcie selektor, czyli wartość ładowana do rejestrów segmentacyjnych, określająca z którego wpisu w GDT należy skorzystać oraz przywileje, których żądamy przy dostępie do pamięci. Struktura selektora:

Selektor

Requested Privilege Level - Żądany Poziom Przywilejów, określa jak wysokie przywileje chcemy uzyskać przy dostępie do pamięci - im niższy numer tym większe przywileje. Table Indicator - Wskaźnik Tabeli, określa, czy korzystamy z GDT, czy z jego lokalnego odpowiednika - LDT (o nim będzie mowa w dalszych artykułach). Jak można się domyślić Index oznacza po prostu numer wpisu w GDT, z tym że numer ten zaczyna się od bitu 3, a więc musi być wielokrotnością cyfry 8. W języku C (którego będę używał do przedstawiania przykładów) można załadować selektor w ten sposób:

// Definiujemy funkcję assemblerową LoadES ładującą selektor do rejestru ES
void LoadES(unsigned short selector)
{
    __asm__("mov %0, %%es"
                   ::"r"(selector));
}

// Nasza funkcja powodująca załadowanie ES obliczonym selektorem
void Selektor(unsigned short przywileje, unsigned short tabela, unsigned short indeks)
{
    LoadES((indeks << 3) + (tabela << 2) + przywileje);
}

W funkcji LoadES została wykorzystana możliwość wstawiania kodu assemblerowego do kodu C (inline assembly), więcej na ten temat na stronie [2]. Funkcja Selektor otrzymuje w parametrach indeks wpisu w tabeli, żądany poziom przywilejów oraz informację czy wpis ma być wzięty z GDT czy LDT. Operator << w tym przypadku oznacza przesunięcie bitowe w lewo o daną liczbę bitów.

Dla przypomnienia, rejestrami segmentacyjnymi nazywamy rejestry CS (segment kodu), DS (segment danych), ES, FS, GS (segmenty dodatkowe), SS (segment stosu).

Należy także wprowadzić termin baza oraz limit. Baza jest to adres w pamięci rozpoczynający dany obszar, natomiast limit określa wielkość obszaru, ale w specyficzny sposób. Adres otrzymany przez dodanie limitu (wyrażonego w bajtach) do adresu bazowego jest ostatnim bajtem należącym do określanego obszaru. Tak więc rzeczywisty rozmiar obszaru równy jest Baza + Limit + 1.

GDT (jak również LDT) jest tablicą (o zmiennej długości) deskryptorów pamięci. W przypadku GDT pierwszym elementem musi być zawsze deskryptor zerowy, w którym wszystkie pola wypełnione są zerami. Adres pamięci, pod którym znajduje się GDT, jest przechowywane w rejestrze procesora GDTR, więc po pierwszym utworzeniu struktury GDT, należy rejestr ten zaktualizować za pomocą kodu Assemblera:

LGDT wskaznik_do_struktury_opisującej_polozenie_GDT

Struktura deskryptora segmentu:

Deskryptor

Deskryptor jest 64-bitową (8 bajtową) strukturą. Pierwsze spojrzenie może trochę odstraszać, ale jej wypełnienie nie jest aż takie trudne. Definiuje on pojedyńczy segment pamięci, a więc jego początek, rozmiar oraz rodzaj. Znaczenie najważniejszych pól deskryptora:

G - ziarnistość limitu (z ang. granularity):
- 1: limit wyrażony w jednostce po 4kb, 0: limit wyrażony w bajtach
D/B - domyślny rozmiar operacji procesora:
- 1: 32-bitowy, 0: 16-bitowy
P - obecność segmentu:
- 1: segment obecny, 0: segment nieobecny
DPL - maksymalne uprzywilejowanie segmentu możliwe do zażądania selektorem
- 0-3 - im niższy numer tym większe przywileje
S - rodzaj deskryptora:
- 1: danych lub kodu, 0: systemowy (deskryptor przerwania, zadania, bramki, itd.)
TYPE - rodzaj segmentu
- w zależności od pola S, określa rodzaj segmentu lub deskryptora systemowego (więcej na temat rodzajów segmentu w dokumentacji Intela [1] - Część 3A, rozdział 3.4.5.1)

Ważnym polem GDT jest bit 23 definiujący tzw. ziarnistość limitu. Jeden segment umożliwia zaadresowanie do 4 gigabajtów pamięci operacyjnej (w przypadku ziarnistości limitu 4kb), dzięki czemu możliwe jest stosowanie adresowania płaskiego, ukrywającego segmentację. W przypadku, gdy jednak zdecydujesz się na dzielenie pamięci na segmenty, będziesz musiał dla każdego z nich utworzyć oddzielny deskryptor. System musi posiadać zawsze przynajmniej 2 deskryptory (jeden zerowy i drugi użyteczny). Struktura opisana w języku C wygląda następująco:

struct gdt_entry
{
    unsigned short limit_low;
    unsigned short base_low;
    unsigned char base_middle;
    unsigned char access;
    unsigned char granularity;
    unsigned char base_high;
} __attribute__((packed));

Baza jest wartością 32-bitową, rozbitą na 16-bitową część dolną (bity od 0-15), oraz dwie 8-bitowe części wyższe (bity 16-23 oraz 24-31). Procesor sumuje odpowiednio te 3 pola i uzyskuje pełną wartość bazy. Podobnie, chociaż troche inaczej jest w przypadku limitu. Został on rozbity na część niższą 16-bitową (bity 0-15) oraz 8-bitową część (pole granularity, bity 16-19), której 4 wyższe bity nie należą już do limitu, bowiem określają typ segmentu. Bity 0-7 pola access oraz 4-7 pola granularity określają różne parametry segmentu. Dokładny opis wszystkich bitów znajduje się w dokumentacji Intela [1].

Starczy tej teorii, teraz trochę praktyki. Przygotowanie segmentów przedstawione zostanie na przykładzie adresowania płaskiego, a więc utworzymy deskryptor zerowy oraz dwa deskryptory opisujące całą przestrzeń adresowania, jeden dla segmentu kodu, drugi dla danych. Mając zdefiniowaną strukturę deskryptora, możemy zdefiniować GDT jako odpowiednią tablicę 3 elementową.

struct gdt_entry gdt[3];

Aby nie pisać dwa razy tego samego kodu, z pomocą przyjdzie nam napisanie odpowiedniej funkcji, która za pomocą podanych przez nas parametrów utworzy w naszym przyszłym GDT odpowiedni wpis.

void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran)
{
    /* Ustawianie adres bazowy segmentu */
    gdt[num].base_low = (base & 0xFFFF);
    gdt[num].base_middle = (base >> 16) & 0xFF;
    gdt[num].base_high = (base >> 24) & 0xFF;

    /* Ustawianie limtu segmentu */
    gdt[num].limit_low = (limit & 0xFFFF);
    gdt[num].granularity = ((limit >> 16) & 0x0F);

    /* Ustaw rodzaj deskryptora oraz informacje o segmencie */
    gdt[num].granularity |= (gran & 0xF0);
    gdt[num].access = access;
}

Funkcja ta po otrzymaniu numeru wpisu w GDT, adresu bazowego, limitu oraz dwóch parametrów definiujących rodzja deskryptora i segmentu, rozbije odpowiednio wszystkie dane i umieści je na swoim miejscu w deskryptorze. Warto zauważyć że parametr gran odpowiada bitom 16-23 w deskryptorze, tak więc jego dolne 4 bity (0-4) zostaną pominięte z pomocą wykonania na nim operacji bitowego AND. Teraz należy napisać funkcję, która odpowiednio wywoła powyższą funkcję i zaktualizuje rejestr GDTR. Należy także wprowadzić pomocniczą strukturę wskaźnika do GDT - gdt_ptr.

struct gdt_ptr
{
    unsigned short limit; // Limit GDT
    unsigned int base; // Adres bazowy GDT
} __attribute__((packed));

Mając zdefiniowaną strukturę wskaźnika do GDT, możemy zadeklarować jego rzeczywistą instancję w sposób następujący:

struct gdt_ptr _gdtptr;

Funkcja przygotowująca oraz instalująca GDT:

void gdt_install()
{
    /* Ustaw wskaźnik */

    _gdtptr.limit = (sizeof(struct gdt_entry) * 3) - 1;
    _gdtptr.base = (unsigned int)&gdt;

    /* Deskryptor zerowy */

    gdt_set_gate(0, 0, 0, 0, 0);

    /* Deskryptor segmentu kodu:
        G = 1, D/B = 1, P = 1, DPL = 0, S=1, TYPE = 0x0A
        Adres bazowy: 0x0, Limit: 0xFFFFFFFF */

    gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);

    /* Deskryptor segmentu danych:
        G = 1, D/B = 1, P = 1, DPL = 0, S=1, TYPE = 0x02
        Adres bazowy: 0x0, Limit: 0xFFFFFFFF */

    gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);

   /* Zaktualizuj rejestr GDTR */

    _loadgdt();
}

Komentarza może wymagać sposób wypełniania wskaźnika do GDT. Limit jest ustalany na podstawie pomnożenia pojedyńczej wielkości deskryptora przez liczbę utworzonych deskryptorów (3) i odjęcie jednego bajta (ponieważ jest to limit, a nie rozmiar). Baza jest po prostu zapisywana poprzez pobranie adresu lokalizacji w pamięci naszej tablicy deskryptorów. Pozostało nam jeszcze zaktualizować rejestr GDTR. Należy to zrobić w assemblerze, bądź poprzez specjalną funkcję wykorzystującą assembler inline. Wykorzystam ten pierwszy sposób, ponieważ kod będzie czytelniejszy.

global _loadgdt ;oznaczenie funkcji _loadgdt jako globalnej
extern _gdtptr ;_gdtptr nie znajduje się w tym pliku

_loadgdt:
    lgdt [_gdtptr]
; Ładujemy GDT adresem naszego wskaźnika
    mov ax, 0x10
; 0x10 jest selektorem segmentu danych (indeks: 2, tablica: 0, przywileje: 0)
    mov ds, ax
; Poprzednie selektory staną się niepoprawne, więc należy załadować nowe
    mov es, ax
    mov fs, ax
    mov gs, ax             
    mov ss, ax
; Aby zaktualizować rejestr CS, musimy dokonać skoku do nowego
; segmentu określonego selektorem 0x08 i offsetem c_code (adres etykiety)
    jmp 0x08:c_code   
; 0x08 jest selektorem segmentu kodu (indeks: 1, tablica: 0, przywileje: 0)

c_code:
    ret
; Zakończenie procedury, powrót do kodu C

Należy także pamiętać o wstawieniu na początku kodu C definicji zewnętrznej funkcji _loadgdt():

extern void _loadgdt();

Oba skompilowane pliki, wlinkowane w wynikowy obraz jądra systemowego, z powodzeniem utworzą odpowiedni GDT.

Kompletny kod źródłowy:

gdt.c - Główny kod w C
gdt.asm - Kod pomocniczy w Assemblerze

Dokumentacja

[1] IntelR 64 and IA-32 Architectures Software Developer's Manuals

[2] Informacje na temat Inline Assembly



Valid XHTML 1.1 License Poprawny CSS!
© 2007 by Tomek Figa na licencji Creative Commons Uznanie autorstwa-Użycie niekomercyjne 2.5 Polska.