Die Global Descriptor Table von C aus anlegen
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))
.
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.
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.
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.
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:
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.
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.