Bootsektor mit GCC

Gleich als Einstimmung auf die Art der weiteren Posts gibt es von mir etwas relativ exotisches, das keinen praktischen Nutzen hat: Einen Bootsektor, der komplett in C geschrieben ist. Es werden nur der GCC und LD aus den Binutils verwendet. Es gibt dabei zwei Herausforderungen:

  1. Mit möglichst wenig Code in den Protected Mode zu gelangen, denn der GCC ist nur für den Protected Mode geeignet.
  2. Den Linker dazu zu bringen, eine 512 Byte große Datei zu erzeugen.

In diesem Post setze ich voraus, dass eine gewisse Kenntnis im Umgang mit dem GCC und LD vorhanden sind, sowie was ein Bootsektor ist und welche Bedingungen dort herrschen. Die wichtigsten Dinge kurz zusammengefasst: Der GCC erzeugt Code für den 32-Bit Protected Mode, im Bootsektor stehen uns (weniger als) 512 Bytes zur Verfügung und wir befinden uns im 16-Bit Real Mode.

In den Protected Mode

Es sind 4 Schritte notwendig um vom Urzustand nach dem Einsprung in den Bootsektor eine Umgebung zu schaffen, in der der vom GCC erzeugte Code zuverlässig laufen kann:

  1. Eine GDT laden
  2. In den Protected Mode schalten
  3. Die Daten-, Code- und Stackselektoren laden
  4. Den Stackpointer setzen

Die Aussage, dass ich nur den GCC und LD verwende, war nicht ganz korrekt: Für jeden dieser vier Schritte müssen wir zwangsläufig mit Inline-Assembler arbeiten. Wir nutzen also auch noch den GNU Assembler GAS, der allerdings intern vom GCC aufgerufen wird. Da wir uns im Real Mode befinden, müssen wir dem Assembler mit .code16 befehlen 16-Bit Code zu erzeugen. Danach folgt der oben genannte Ablauf, um in den Protected Mode zu gelangen, und ein far jump zum 32-Bit Code. Das darauf folgende .code32 sorgt dafür, dass ab dieser Stelle wieder 32-Bit Code erzeugt wird:

asm("\n\t"
".code16\n\t"

"cli\n\t"
"xor %ax, %ax\n\t"
"mov %ax, %ds\n\t"
"lgdt _gdt\n\t" /* 1. */
"mov %cr0, %eax\n\t" /* 2. */
"inc %ax\n\t"
"mov %eax, %cr0\n\t"
"mov $0x10, %ax\n\t" /* 3. */
"mov %ax, %ds\n\t"
"mov %ax, %es\n\t"
"mov %ax, %ss\n\t"
"mov $0x7bfc, %esp\n\t" /* 4. */
"ljmpl $0x08, $_bmain\n\t" /* Sprung zum C-Code */
".code32\n");

Erzeuge einfach eine leere Datei, nenne sie boot.c und füge diesen Assembler Code ein. Füge alle weiteren Codes unter diesem ein, damit dieser Code zu erst ausgeführt wird.

Die Labels _gdt und _bmain werden im C-Teil definiert. In diesem Code beginnen sie im Inline-Assembler-Teil mit Unterstrichen, da ich diesen Code mit MinGW getestet habe. (Wenn du nicht DJGPP oder MinGW nutzt, musst du die Unterstriche entfernen. In einem späteren Post werde ich vielleicht mal erläutern, warum die Verwendung von MinGW und DJGPP eine schlechte Idee ist.) In dem nun folgenden C-Teil benutzen wir also statt _gdt und _bmain nur noch gdt bzw. bmain, meinen jedoch die selben Variablen.

In dem C-Teil müssen wir noch die GDT definieren. Dazu bietet sich folgendes struct an:

struct __attribute__ ((packed))
{
struct __attribute__ ((packed))
{
unsigned short limit;
void * base;
unsigned short zero;
};
unsigned long long code_desc;
unsigned long long data_desc;
}
gdt =
{
{ sizeof(gdt) - 1, &gdt, 0 }, // gdtr/null descriptor
0x00cf9a000000ffffULL, // code descriptor
0x00cf92000000ffffULL, // data descriptor
};

Hier benutze ich die Technik, dass ich GDT und GDTR verschachtele. Das GDTR steht also an der Stelle des ungenutzten Null-Deskriptors, und die Adresse von gdt gibt somit sowohl die Adresse der GDT als auch die Adresse des GDTR an. Der Sinn dahinter ist einfach Platz zu sparen. Würde ich das GDTR aus der GDT rausnehmen, würden wir 6 Bytes mehr verbrauchen. Bei nur 512 Bytes (eigentlich 508, siehe unten), die uns zur Verfügung stehen, ist diese einfache Optimierung auf jeden Fall gerechtfertigt. Für die Code und Datendeskriptoren habe ich mir kein eigenes struct gebastelt, sondern einfach unsigned long longs genommen, weil diese einfachen Deskriptoren klar sein sollten, und ich einfach auf kürzeren Code stehe.

Das __attribute__ ((packed)) sorgt dafür, dass der GCC nicht irgendwelche Zwischenräume zwischen den Mitglieden der structs macht, um den Code zu „optimieren“. In diesem Fall wäre das nicht nur Verschwendung, sondern würde für einen Absturz sorgen, da die CPU davon ausgeht, dass es dort keine Zwischenräume gibt.

Für bmain nehmen wir erstmal eine einfache Hello-World-Funktion:

void bmain(void)
{
char * msg = "Hello, world!";
short * video = (short *)0xb8000;
while(*msg)
{
*video++ = *msg++ | 0x0700;
}

while(1);
}

Alles zusammen linken

Um zu überprüfen, ob alles richtig ist, können wir den Quelltext kompilieren, linken und in eine Binär-Datei umwandeln. Kompilieren ist noch einfach:

gcc -c boot.c

Für das Linken brauchen wir folgendes Linker-Skript names boot.ld:

SECTIONS
{
.boot_sector 0x7c00 : {
*(.text)
*(.data) /* daten */
*(.rdata) /* read-only daten */
*(.rodata) /* read-only daten von mingw */
}
.bss : {
*(.bss)
}
}

Ich habe hier eine neue Sektion namens .boot_sector eingeführt, die erwartet, an die Adresse 0x7c00 geladen zu werden. In diese Sektion kommt alles Initialisiertes, also der Code (.text) und die Daten (.data, .rdata, .rodata). Die nicht initialisierten Daten (.bss) kommen einfach dahinter. Durch folgenden Aufruf von ld wird das Linker-Skript angewandt:

ld -T boot.ld -o boot boot.o

Und mit objcopy wird das ganze in eine Binärdatei umgewandelt:

objcopy -O binary boot

Man kann auch direkt mit ld die Binärdatei erzeugen, und somit objcopy weglassen. Allerdings ist das ld von MinGW total vermurkst und deswegen verwende ich die Methode mit objcopy, die mit allen GCC-Varianten funktionieren sollte.

Jetzt fehlt noch das Wichtigste, das einen Bootsektor ausmacht: Die Bootsignatur 0xaa55. Dieses beiden Bytes müssen sich genau an dem Offset 510 des Bootsektors befinden, also 2 Bytes vor dem Ende. Um diese dort zu platzieren, benötigen wir LD. Wichtig ist dabei allerdings zu beachten, dass wir uns in 32-Bit Code befinden und es deswegen nicht so einfach ist, etwas an einer Adresse zu platzieren, die nicht durch 4 teilbar ist. Der Einfachheit halber platziere ich statt eines words 0xaa55 an die Adresse 510, das dword 0xaa550000 an die Adresse 508. Dass dies auch die korrekte Bootsignatur erzeugt, liegt daran, dass die x86-Archtektur eine Little-Endian-Architektur ist. Wenn ich also das word 0xaa55 an die Adresse 510 schreibe, landet das byte 0x55 bei 510 und das byte 0xaa bei 511. Wenn ich das dword 0xaa550000 an die Adresse 508 schreibe, landet das niederwertigste byte 0x00 bei 508, das zweite byte (von rechts) 0x00 bei 509, und die bytes 0x55 und 0xaa wieder bei 510 bzw. 511.

Ein dword mit dem Wert 0xaa550000 ist in C einfach zu definieren:

static unsigned int boot_signature = 0xaa550000;

Um dies allerdings noch zu platzieren, müssen wir es in eine Extra-Sektion tun. Diese kann einen beliebigen Namen haben, ich habe .boot_signature gewählt:

static unsigned int boot_signature __attribute__ ((section(".boot_signature"))) = 0xaa550000;

Da diese Variable nur vom Linker, aber nicht vom C-Compiler genutzt wird, müssen wir ihn daran hindern, sie weg zu optimieren. Das geht mit dem Attribut used:

static unsigned int boot_signature __attribute__ ((section(".boot_signature"))) __attribute__ ((used)) = 0xaa550000;
Die Sektion .boot_signature können wir nun mit *(.boot_signature) im Linker-Skript referenzieren:

SECTIONS
{
.boot_sector 0x7c00 : {
*(.text)
*(.data) /* daten */
*(.rdata) /* read-only daten */
*(.rodata) /* read-only daten von mingw */
. = 508; *(.boot_signature)
}
.bss : {
*(.bss)
}
}

Die neue Zeile platziert die Sektion .boot_signature an das Offset 508, was wie oben bereits erläutert unsere Bootsignatur gibt. Jetzt einfach noch mal alles neu bauen, und fertig ist der Bootsektor in C:

gcc -c boot.c
ld -T boot.ld -o boot boot.o
objcopy -O binary boot

Die Datei boot kann man nun in einen Bootsektor einer Diskette schreiben und wenn man davon bootet wird man mit einem „Hello, world!“ begrüßt.

Fazit

Der Nutzen eines solchen Bootsektors ist, wie bereits gesagt, eher gering, da man sich durch den Sprung in den Protected Mode sofort den Zugang zu den BIOS-Funktionen versperrt, die man benötigt um weiteren Code nachzuladen. Wer in den ca. 430 noch zur Verfügung stehenden Bytes einen Diskettentreiber schreiben kann, der wird natürlich auch damit umgehen können. Dem sterblichen Rest würde ich in der Regel von einem selbstgeschriebenen Bootsektor abraten und einen Bootloader wie GRUB, von dem übrigens bald Version 2 ansteht, empfehlen.

Update: Hab vergessen die Interrupts zu deaktivieren.

Advertisements

9 Responses to Bootsektor mit GCC

  1. Nils sagt:

    Hi!
    Netter Artikel, obwohl mir das C nicht so ganz klar ist, der Übergang in den PM schon gar nicht. :/

    Im Übrigen scheint der PC beim Booten mit diesen Bootsektor einfach neuzustarten, ohne irgendeine Ausgabe?

    Wär nett wenn du denn Einsprung in den PM, Stichwort Deskriptorentabelle, mal etwas näher erläutern könntest. 😉

    MfG, Nils

  2. superschurke sagt:

    Hi Nils,

    der Artikel war eigentlich nicht als Einstieg in die Protected Mode Programmierung gedacht. Dafür ist der verwendete Weg bereits zu speziell. Die Zielgruppe waren eher Leute, die bereits problemlos via Assembler in den Protected Mode gelangen und von da aus dann in C weiter programmieren. Das ganze ist eher eine Spielerei, als etwas Sinnvolles zum OS-Development.

    Ich muss zugeben, dass ich den Code nicht auf einem richtigen PC getestet habe, sondern nur im Emulator Bochs. Ich werde mal nachforschen, was da Probleme macht.

    Die GDT ist eigentlich eine einfache GDT mit einem Code- und einem Daten-Selektor mit Basis 0 und einem 4 GB Limit, in einer vom Quellcode her sehr komprimierten Form (einem 64 Bit Integer). Normalerweise wird die GDT in einer etwas übersichtlicheren Form dargestellt, mit den einzelnen Elementen voneinander getrennt. Hier habe ich darauf verzichtet, damit er Artikel nicht unnötig lang wird.
    Grundsätzliche Anleitungen/Erläuterungen/Tutorials zum Protected Mode habe ich eigentlich nicht vor zu machen. Es gibt davon reichlich im Internet, allerdings mit schwankender Qualität. Das beste ist solange welche durchzuprobieren bis du eins findest, das für dich funktioniert.

  3. superschurke sagt:

    So ich hab die Ursache gefunden: Bochs aktiviert offensichtlich nicht die Interrupts, wenn es zum Bootsektor springt. Normale PCs hingegen schon. Weil ich keine Handler installiert habe startet der PC neu. Solche Sachen sind der Grund dafür, dass ich die Weltherrschaft noch nicht erlangt habe. 😉

    Die Lösung war also einfach ein cli in den Assemblerteil zu tun.
    Danke für den Hinweis und das Testen. 🙂

  4. stefan sagt:

    Hallo!

    Tja, ich gehöre eigentlich gar nicht zu deiner Zielgruppe. 😉 Ich bin zu zufällig auf deine Webseite gestoßen, bei der Suche wie ich Code in meinem Mikrocontroller an eine ganz bestimmte stelle schiebe. Ich denke, dass ich die Lösung im Linker Script suchen muss, komme aber nicht so ganz weiter. Kannst du mir vielleicht einen guten Link empfehlen?

    Grüße

    Stefan

  5. superschurke sagt:

    Hi,

    freut mich, dass hier jemand mal vorbei schaut. 🙂

    Also ich habe mich nur sehr oberflächlich mit dem Linker beschäftigt und meine Hauptquelle war diese Seite: http://www.gnu.org/software/binutils/manual/ld-2.9.1/html_chapter/ld_3.html Ich denke für dich könnten die Abschnitte „Section Placement“ und eventuell auch „Memory Layout“ (das hab ich mir allerdings nicht genauer angeschaut) interessant sein

    Ich hoffe das hilft dir etwas weiter.

  6. Christian sagt:

    Hallo, ich muss sagen, die Seite ist wirklich sehr interessant. Beschäftige mich auch gerade mit dem Thema OS-Development. Finde es echt toll, dass es noch Hilfe für solche Themen gibt.

    Weiter So!

  7. Jan sagt:

    Hi, ein sehr interessanter Artikel! Man findet eher selten so einfache und trotzdem gute Informationen im Internet.
    Außerdem ist es auch auf eine sehr einfache Weise erklärt und ich würde dir sofort widersprechen, dass der Artikel nichts für Leute ist, die sich mit Bootsektorprogrammierung in ASM auskennen, wenn du den kleinen Assemblercode, an dem ja schon Kommentare markiert sind, noch erläutern würdest. Das würde auch Leuten helfen, die sich nur bedingt mit anderen Assemblern auskennen.

  8. Absolute Klasse! Ich stehe gerade vor dem Problem einen Bootloader schreiben zu müssen (es soll jedesmal ein komplettes Windows aus Partition 2 in Partition 1 kopiert werden und dann direkt aus Partition 1 gestartet werden – ist ne kundenspezifische Embedded-Board-Geschichte, das Windows wird nicht heruntergefahren, weil direkt an einem Musikautomaten, bei welchem jedesmal brutal der Strom abgestellt wird)

    Ich hatte schon einen Bootloader in Assembler geschrieben, aber die Programmierung dauert halt ewig und es ist anfällig…
    Das C-Beispiel hat auf Anhieb funktioniert und ich werde wohl mit C fortfahren!

    Frage: ich muss zum Starten des Windows wieder in den Real-Mode schalten und den Bootloader starten – gibt es eine einfache Möglichkeit dies aus dem C-Programm zu tun? (evtl. mit inline Assembler?)

    Viele Grüsse!
    Alexander

  9. superschurke sagt:

    Hi Alexander,

    tut mir Leid, dass ich so spät antworte, aber ich hätte dieses Blog längst vergessen, wenn nicht eine Hinweismail, dass hier jemand geantwortet hat, in den Tiefen meines Postfaches gelandet wäre 😉

    Ja, zum Starten von Windows musst du wieder in den Real Mode schalten, da der Windows Bootloader diese Umgebung erwartet. Unter anderem greift er auf BIOS-Funktionen zu. Das Zurückschalten in den Real Mode ist relativ aufwändig, und aus dem Stehgreif kann ich da keinen Code liefern.

    Ich rate dir dazu, bei deiner Assemblerlösung zu bleiben. Mein Kuriosum funktioniert vielleicht, aber zweckmäßig ist das Teil für gar nichts, außer um „Hallo“ auf dem Bildschirm auszugeben. Du müsstest unter anderem einen Treiber für die Festplatte schreiben, damit du Windows kopieren kannst. Der Treiber passt aber garantiert nicht in den Bootsektor mehr. Das bedeutet du musst weiteren Code von der Festplatte nachladen, was ohne Festplattentreiber nicht geht. Die einzige Möglichkeit ist das BIOS zu benutzen, und das ist im Protected Mode nicht zu gebrauchen Deswegen solltest du einfach im Real Mode bleiben, und es auch mit Assembler weiter versuchen.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: