Bisher haben wir die GDT direkt in unserem Assembler-Code angelegt. Da unser Kernel jetzt in C programmiert ist, wollen wir auch die GDT in C anlegen.

In C können wir uns die einzelnen Datenstrukturen direkt bitweise oder byteweise nachbauen. Ich habe mich für einen byteweisen nachbau entschieden, aber wer die Flags alle einzeln haben will, kann natürlich auch Bitfields verwenden.

Wir erinnern uns, dass die GDT aus zwei Teilen besteht. Einmal dem GDT-Pointer und dein einzelnen Einträgen der GDT. Der Pointer gibt den Speicherbereich an, an welchem die GDT-Einträge beginnen, und deren Länge in Byte - 1. Jeder einzelne GDT-Eintrag gibt ein Segment an und enthält dessen Start-, Länge, Zugriffsrechte und einige Flags. Genauere Erklärungen befinden sich im älteren Artikel.

Mit diesem Wissen legen wir uns zwei Datenstrukturen an, die das wiedergeben. Damit der C-Compiler uns die Einträge nicht ausrichtet und damit leeres Padding erzeugt, deklarieren wir beide struct als __attribute__((packed)).

#include <stdint.h>

struct gdt_entry {
    uint16_t limit_lower;
    uint16_t base_lower;
    uint8_t base_middle;
    uint8_t access_rights;
    uint8_t flags_limit_higher;
    uint8_t base_higher;
} __attribute__((packed));

struct gdt_description {
    uint16_t limit;
    uint32_t base;
} __attribute__((packed));

Außerdem müssen wir schonmal Speicher für unsere Strukturen reservieren, denn dynamischen Speicher können wir noch nicht allokieren. Einerseits haben wir noch keine Funktion dafür, andererseits hätten wir noch nicht einmal ein Datensegment von dem wir allokieren könnten (der Bootloader hat uns mit seiner provisorischen GDT schon eines eingerichtet, aber wer weiß, ob es dem entspricht, das wir uns gleich mit der neuen GDT einrichten wollen!).

Zudem machen wir auch gleich eine Funktion öffentlich bekannt, mit der man die neue GDT einrichten kann.

#define GDT_ENTRIES 3

struct gdt_entry gdt[GDT_ENTRIES];
struct gdt_description gdt_desc;

void gdt_setup();

Damit ist unsere Header-Datei für die GDT auch schon fertig! Und die c-Datei wird auch nicht viel schwieriger, weil wir dort auch nur Bit-Operationen schreiben müssen, um menschenlesbare Daten in die zersplitterte GDT-Struktur zu schreiben. Die Idee ist also, dass wir eine Funktion haben, die Parameter für Base, Limit, Zugriffsrechte und Flags entgegennimmt und daraus die richtigen Strukturen/Bitfolgen erstellt. Da manche Elemente nur 20 Bit umfassen, es dafür in C aber keine eigene Datenstruktur gibt, habe ich dann den nächstgrößeren Typ gewählt. Das macht meines Erachtens kein Problem, da ich die oberen Bits einfach ignoriere.

#include <stdint.h>
#include "gdt.h"

void gdt_set_entry(unsigned int pos, uint32_t base, uint32_t limit,
        uint8_t access_rights, uint8_t flags)
{                                              
    gdt[pos].base_lower = base & 0xffff;
    gdt[pos].base_middle = (base >> 16) & 0xff;
    gdt[pos].base_higher = (base >> 24) & 0xffff;

    gdt[pos].limit_lower = limit & 0xffff;
    gdt[pos].flags_limit_higher = ((flags << 4) & 0xf0) | ((limit >> 16) & 0x0f);
    gdt[pos].access_rights = access_rights;
}

In der Realität würde man sich die paar Bytes für jeden GDT-Eintrag im Vorhinein ausrechnen und dann hart kodieren, aber für ein Lern-Betriebssystem fand ich es wesentlich praktischer, wenn man die GDT so leicht im Code abändern kann.

Natürlich müssen wir auch die vorher angekündigte gdt_setup implementieren. Diese setzt unseren GDT-Pointer korrekt und füllt die drei Einträge der GDT. Zuletzt ruft sie auch noch ein kleines bisschen Assembler-Code auf, der die neue GDT der CPU bekanntmacht und anschließen alle Segmentregister neu setzt und einen Far-Jump durchführt. Dies geschieht über die Funktion gdt_install, die wir wie in einem anderen Tutorial wieder als extern deklarieren müssen.

extern void gdt_install();

void gdt_setup()
{ 
    gdt_desc.limit = GDT_ENTRIES * sizeof(struct gdt_entry) - 1;
    gdt_desc.base = (uint32_t) (uintptr_t) gdt;

    gdt_set_entry(0, 0, 0, 0, 0);
    gdt_set_entry(1, 0, 0xfffff, 0x9a, 0xcf);
    gdt_set_entry(2, 0, 0xfffff, 0x92, 0xcf);

    gdt_install();
}

Der Assembler-Code zum Setzen der GDT und der Segmentregister ist mit unserem bisherigen Wissen auch kein Hexenwerk mehr, die meisten Sachen haben wir ja bereits früher schon einmal gemacht:

extern gdt_desc
global gdt_install
gdt_install:
    lgdt [gdt_desc]
    jmp 08h:reload_cs
reload_cs:
    mov eax, 10h
    mov ds, eax
    mov es, eax
    mov fs, eax
    mov gs, eax
    mov ss, eax
    ret

Da Global Descriptor Table’s eine x86-spezielle Sache zu sein scheinen, habe ich diesen Code in meinem Projekt in einen Ordner arch/x86 einsortiert. Dorthin gehört auch der Assembler-Code für den Multiboot-Header und den initialen Stack. Zwar sind Stack und Multiboot-Header selbst nicht x86-spezifisch, aber der Assemblercode natürlich schon.

Geänderte Segmentregister und GDT-Pointer

An dieser Stelle noch ein paar hilfreiche Tipps zum Debuggen mit Bochs. Man kann in Bochs sogenannte Magic Breakpoints anlegen. Das sind Instruktionen ohne Wirkung, an deren Stelle Bochs einen Breakpoint anspringen wird. Dazu muss man lediglich folgende Zeile Code irgendwo als Assembler verpacken:

xchg bx, bx

Ich habe das beispielsweise direkt vor der lgdt-Anweisung gemacht, um so den alten und den neuen Stand der GDT anzuzeigen. Dies wiederum funktioniert über den Befehl info gdt. Und die Segmentregister sowie den GDT-Pointer kann man sich mit sreg ausgeben lassen.

Der aktuelle Code-Stand ist wie immer auf Github (Commit: aab5207bbd3c17a735529e96624e47eedfee596f).

I do not maintain a comments section. If you have any questions or comments regarding my posts, please do not hesitate to send me an e-mail to stefan@eliteinformatiker.de.