Co to tedy je GDT a IDT
Můžeme jim říkat popisovače tabulek, představme si je jako pole obsahující tzv. příznaky (flags) a další speciální bity popisující operaci, kterou má segmentační systém vzít v úvahu, pokud se jedná o GDT nebo operace určující vektor přerušení, pak je to IDT.
GDT neboli Global Descriptor Table
Architektura x86 má dvě metody, kterými lze chránit paměťový prostor – jsou jimi segmentace a stránkování. Pokud mluvíme o segmetaci, každý přístup do paměti je vázán na určitý segment. V praxi to znamená, že se adresa daného bodu v paměti přičte k tzv. base adrese patřičného segmentu, poté se překontroluje jeho velikost. Segment si lze představit jako okno v adresním prostoru, přičemž ho aplikace nevidí, celý adresní prostor se jeví jako běžná lineární paměť.
Stránkování je poměrně odlišné – adresní prostor je rozdělen na tzv. stránky, které mají každá svou velikost (většinou je to 4 kB, lze ale změnit). Každá ze stránek představuje blok paměti, který je namapován na fyzickou paměť. Stránkování se také využívá pro čel virtuální paměti – každý proces v systému si např. může myslet, že je na adrese odpovídající 128 MB, přičemž fyzicky má každý z nich adresu jinou (jinak by se navzájem přepsaly).
U 32bit x86, respektive ia-32 procesorů lze namapovat virtuální paměť až do výše 4 GB, přičemž ve skutečnosti může mít daný stroj menší paměť RAM.Obě metody ochrany paměti mají své výhody i nevýhody, stránkování je však užitečnější – o tom ale jindy. Jedna z výhod, kterou však stránkování poskytnout nemůže je tzv. ring (0 – 3), kde se jedná o oprávnění pro běžící procesy vůči adresnímu prostoru. Ring na úrovni 0 je nejvíce privilegovaný, takže se bude určitě hodit pro naše jádro. Ring 1 a 2 se v dneštních operačních systémech příliš nevyskytuje – nemají k tomu důvod, avšak najdeme takové, které toho využívají (některé OS s mikrojádry). Poslední ring na úrovni 3 je nejméně privilegovaný, takže se uživá pro user-space procesy. Jednoduše si pak software nebude moci dělat, co se mu zlíbí.
Jak nastavit GDT ?
Pro začátečníka může být tato operace obtížná, pokud se ale seznámíme s postupem „nahození“ Global Descriptor Tabulky v ukázkovém kódu, bude to daleko jednodušší. Nejdříve představím datovou strukturu, která koresponduje s x86 – tato struktura má samozřejmě všude stejný „tvar“, takže ji lze definovat takto:
/* Struktura obsahuje hodnoty pro nastavení jedné položky z GDT */ typedef struct { unsigned short limit; /* konec adresního prostoru (prvních 16bitů) */ unsigned short base_f; /* začátek adresního prostoru (1/3) (16bitů) */ unsigned char base_s; /* začátek adresního prostoru (2/3) (8bitů) */ unsigned char attrib; /* nastavení příznaku přístupu (včetně ring) */ unsigned char gran; /* nastavení masky granularity (8bitů) */ unsigned char base_t; /* začátek adresního prostoru (3/3) (8bitů) */ } __attribute__ ((packed)) gdt_entry_t;
Vidíme před sebou jeden záznam, který tvoří onu GDT – my jich budeme potřebovat 5. První bude nulový, bez kterého by se mohly dít zajímavé věci (více i386 dokumentace). Druhý segment kódu a třetí segment dat. Další dva budou opět pro kód a data s tím rozdílem, že budou pracovat v ring na úrovni 3 – tj. pro uživatelské procesy. Všechny krom prvního (který je opravdu nulový) budou přístupné v celém adresním prostoru (0×0 až 0×ffffffff), limit lze také definovat pomocí ~0 (což dává nejvyšší možnou hodnotu pro daný typ proměnné).
Proměnná gran je velikosti 1 bajt a představuje masku k nastavení granularity. Slouží tedy k nastavení několika klíčových parametrů segmentu, podobně jako proměnná attrib (např. nastavení ringu nebo typu segmentu).Teď nastala otázka, jak oznámit procesoru, aby naši GDT použil – naštěstí existuje instrukce lgdt, ta funguje tak, že jí předáme ukazatel na adresu, kde se nachází struktura gdt_ptr_t. Vypadá takto:
/* Struktura představuje ukazatel na GDT tabulku, nutný pro instrukci lgdt */ typedef struct { unsigned short limit; /* velikost GDT tabulky */ unsigned int base; /* adresa GDT tabulky */ } __attribute__ ((packed)) gdt_ptr_t;
Ta instrukci lgdt sdělí, kde že se ta tabulka nachází a jak je dlouhá (velikost v bajtech –1). Proměnná base obsahuje adresu na začátek pole struktur gdt_entry_t. Instrukce lgdt je volána v proceduře gdt_flush, viz. start.s.
IDT neboli Interrupt Descriptor Table
Občas se nám může hodit tzv. přerušení, ať už vnější nebo vnitřní – to si lze představit jako nečekaný impuls procesoru, který říká, že se něco stalo. Máte např. uživatelský program, ve kterém nemůžete přistupovat na adresy jako 0×b0000, apod. ale máte za cíl vypsat text na obrazovce. Jak to tedy udělat ? Použijeme přerušení :) – jeho instrukce se jmenuje int a jako parametr se používá hodnota, která označuje jeho druh.
V moderních systémech se používají desítky až stovky přerušení, přitom každé dělá něco jiného. Dokonce i skoro každý hardware generuje přerušení v závislosti na událostech, takže je nutné je nějakým způsobem obstarat. Pokud se vrátíme zpět k uživatelskému programu a usmyslíme si, že přerušení s ID 0×80 bude vypisovat znak, který předáme pomocí registru, může ho náš program směle zavolat. V ten moment procesor přeskočí na jaderný kód a tam se požadavek programu obslouží. Po vykonání samozřejmě vše vrátí do původního stavu (registry) a pokračuje se v kódu uživatelském.
Jak nastavit IDT ?
Nastavení je velmi podobné tomu z GDT, nyní nenastavujeme segmenty, ale jednotlivé přerušení. Každý záznam v tabulce zaregistruje funkci, která se vykoná, když se zavolá konkrétní přerušení. Architektura x86 má však vyhrazené prvních 32 přerušení pro své potřeby. Jedná se o stavy jako např. dělení nulou nebo důležitý general protection fault (GPF), který nám říká, že došlo v paměti k chybě. Je tedy nutné, abychom všechny tyto chybové stavy zaregistrovali a patřičně na ně zareagovali.
/* Tato struktura popisuje položku pro IDT */ typedef struct { unsigned short base_l; /* Adresa funkce, na kterou má procesor skočit při přerušení (1/2) */ unsigned short sel; /* Offset pro nastavení jaderného segmentu */ unsigned char zero; /* Sem patři nula */ unsigned char attrib; /* Nastavení příznaků */ unsigned short base_h; /* Adresa funkce, na kterou má procesor skočit při přerušení (2/2) */ } __attribute__ ((packed)) idt_entry_t; /* Tato struktura se používá pro specifikaci IDT tabulky k volání instrukce lidt */ typedef struct { unsigned short limit; /* Délka IDT tabulky */ unsigned int base; /* Adresa IDT tabulky */ } __attribute__ ((packed)) idt_ptr_t;
První struktura opět znamená jeden záznam v IDT, druhá se naopak použije pro specifikaci adresy a délky celé tabulky pro instrukci zvanou lidt – volá se prostřednictvím procedury idt_flush. Nahlédneme-li do souboru int.s, vidíme proceduru isr_common_stub, která je volána pokaždé, nastane-li přerušení s hodnotou v rozmezí 0 až 31. Její činnost spočívá v uložení všech registrů (např. uživatelský program) a zavolání obslužné funkce definované v isr.c. Její povinností je vypořádat se s příp. chybou. Nám bude prozatím stačit, když oznámíme na obrazovce pomocí jádra informaci o přerušení, viz. funkce isr_handler (). Nakonec procedura isr_common_stub zpět obnoví registry aby nedošlo k chybě v kódu, který přerušení vyvolal.
Shrnutí
GDT i IDT máme nastaveny, jak ale poznáme, že to je právě tak, jak by mělo být? Prvním příznakem je spuštění samotného systému, pokud se bavíme o GDT. Druhým příznakem, jestli je řeč o IDT, je fakt, že při zavolání instrukce přerušení z kódu jádra, nám hezky oznámí co se stalo. Dnes jsme si tedy vytvořili užitečnou ochranu našeho jádra proti poškození paměti či dalším nežádaným vlivům. Úplně vše co bych chtěl zmínit zde bohužel není – pomalu už to je na přepis i386 dokumentace, doporučuji však omrknout již zmiňovanou granularitu, ringy, atributy tabulek ID, GD a také segmentaci. V ukázce můžete shlédnout následující kus kódu (main.c):
int r = 1 / 0;
Věřím, že každý po tomto dílu přijde na to, co přesně se na tomto řádku stane a také to, že do funkčního jádra nepatří. Ukázkový kód lze stáhnout na ZeXOS.org.