==Der Bootprozess==
Bevor wir anfangen, Code zu schreiben, zunächst ein kleiner Blick auf den Bootprozess. Aus Sicht des Kernel könnte dieser Abschnitt den Titel "was bisher geschah" tragen.
Bei einem CPU-Reset (der beispielsweise beim Anschalten passiert) wird der gesamte Prozessorzustand neu gesetzt. Insbesondere schaltet der Prozessor in den Real Mode und springt an die lineare Adresse 0xffff0 (z.B. cs:ip = f000:fff0). An dieser Adresse liegt as BIOS. Das BIOS initialisiert die Hardware, führt einen Selbsttest durch und wenn es fertig ist, löst es einen Interrupt 0x19 aus. Der Handler lädt den Bootsektor von der Festplatte oder Diskette nach linear 0x7c00 (z.B. 07c0:0000) und springt an diese Adresse.
Der Bootloader ist anschließend dafür zuständig, den Kernel des Betriebssystems in den Speicher zu laden und an dessen Einsprungspunkt zu springen.
==Multiboot==
Für unser Betriebssystem werden wir [[GRUB]] als Bootloader benutzen. GRUB ist einfach zu benutzen, weit verbreitet und hat einen großen Funktionsumfang.
Es gibt Leute, die der Meinung sind, man sollte seinen Bootloader selbst schreiben, damit man die Sache wirklich selbst gemacht hat. Diese Leute benutzen dann in ihrem Bootloader die Funktionen des BIOS, fangen also nur an einer anderen Stelle an, auf den Code anderer aufzusetzen. Dazu müssen sie auf Dauer viel Zeit in die Funktionalität des Bootloaders investieren, wenn sie nich in ihrem Betriebssystem mit Einschränkungen leben wollen. Bootloader sind nette Projekte, aber wir konzentrieren uns hier auf das Betriebssystem.
[[Multiboot]] ist der Name der Spezifikation, die ein Kernel einhalten muss, um von GRUB geladen werden zu können. Der verlinkte Artikel enthält dazu mehr Informationen.
==Der Hello-World-Kernel==
"Hello World" ausgeben zu wollen stellt uns vor zwei Schwierigkeiten: Zum ersten müssen wir wissen, wie man Text auf den Bildschirm bekommt - und zum zweiten müssen wir dafür sorgen, dass der entsprechende Code überhaupt ausgeführt wird.
Fangen wir mit dem einfachen an: Direkt nach dem Booten ist ein PC im Textmodus. Im Textmodus werden auf dem Bildschirm die Zeichen dargestellt, die im Grafikspeicher ab der Adresse 0xb8000 liegen. Dabei braucht jedes Zeichen zwei Bytes: Das erste Byte ist das Zeichen selbst. Das zweite Byte ist das sogenannte Attributbyte, das die Farbe und Hintergrundfarbe des Texts festlegt - mehr Details stehen bei Bedarf im Artikel [[Textausgabe]]. Wir legen also folgendermaßen los (die Datei heißt bei mir init.c):
SRCS = $(shell find -name '*.[cS]')
OBJS = $(addsuffix .o,$(basename $(SRCS)))
CC = gcc
LD = ld
ASFLAGS = -m32
CCFLAGS = -m32 -Wall -g
LDFLAGS = -melf_i386 -Ttext=0x100000
kernel: $(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CCFLAGS) -c -o $@ $^
%.o: %.S
$(CC) $(ASFLAGS) -c -o $@ $^
clean:
rm $(OBJS)
.PHONY: clean
Erwähnenswert ist vor allem die Option -Ttext=0x100000 für ld. Das ist die Adresse, an die der Kernel geladen werden soll und an der er ausgeführt wird. GRUB kann einen Kernel nicht an eine niedrigere Adresse als 1 MB laden und 0x100000 sind genau 1 MB.
Die Werte für CC und LD müssen ggf. angepasst werden, so dass sie auf einen Compiler bzw. Linker zeigen, der i386-ELF-Dateien erzeugt. Mit dem Crosscompiler unter Windows könnte es beispielsweise sein, dass für CC i586-elf-gcc und für LD i586-elf-ld eingetragen werden muss.
Nach einem Aufruf von make solltest Du jetzt eine Datei kernel vorfinden. Jetzt müssen wir ihn nur noch irgenwie ausführen. Dazu gibt es mehrere Möglichkeiten:
* qemu kann ab Version 0.11 Multiboot-Kernel direkt laden. qemu -kernel kernel bringt unseren Kernel zur Ausführung und schreibt ein wunderschönes Hello World in die linke obere Ecke. Den Rest des Bildschirms zu löschen haben wir leider vergessen, so dass das über irgendwelche BIOS-Meldungen geschrieben wird.
* Jeder beliebige Emulator kann von Disketten-Images booten, z.B. qemu mit qemu -fda floppy.img. Wir brauchen also ein Diskettenimage, das GRUB und unseren Kernel enthält. Dazu gibt es folgende Artikel in diesem Wiki:
** [[GRUB-Image erstellen]] (für Linux)
** [[GRUB-Image erstellen (Windows)]]
* Ein echter PC und eine echte Floppy geht natürlich auch. Dazu entweder das Image erstellen und auf Diskette schreiben (z.B. mit dd oder rawrite) oder direkt eine GRUB-Diskette nehmen und den Kernel darauf kopieren
Wenn wir nicht mit qemu 0.11 direkt booten, sondern ein Diskettenimage (bzw. eine echte Diskette) benutzen, begrüßt uns nach dem Booten GRUB (jedenfalls, wenn keine menu.lst angelegt wurde). Der Kernel kann dann folgendermaßen gestartet werden:
kernel /kernel boot==Multiboot-Header in einer eigenen Sektion== Der Kernel scheint zwar wunderbar zu funktionieren, aber er enthält einen Fehler. Der Fehler ist im Moment nicht dramatisch, aber irgendwann wird er unerwartet zuschlagen - wir sollten es deshalb lieber gleich richtig machen. Wie bereits erwähnt muss der Multiboot-Header der Kernels in den ersten 8 kB der Datei liegen. Das funktioniert im Moment mehr oder weniger zufälligerweise. Die Sektion .text landet zwar am Anfang der Binary, aber in welcher Reihenfolge die Dateien hineingeschrieben werden, bleibt dem Linker überlassen. Wir können es aber erzwingen, dass .text mit dem Multiboot-Header anfängt: Wir legen ihn zunächst in seine eigenen Sektion und sagen dem Linker dann, wie er es zusammenbauen soll. Dazu ändern wir den Anfang der start.S folgendermaßen ab:
/* Bei _start soll die Ausfuehrung losgehen */
ENTRY(_start)
/*
* Hier wird festgelegt, in welcher Reihenfolge welche Sektionen in die Binary
* geschrieben werden sollen
*/
SECTIONS
{
/*
* . ist die aktuelle Position in der Datei. Wir wollen den Kernel wie gehabt
* an 1 MB laden, also muessen wir dort die erste Sektion hinlegen
*/
. = 0x100000;
/*
* Der Multiboot-Header muss zuerst kommen (in den ersten 8 kB).
* Die Standardsektionen einfach hintereinander weg einbinden.
*/
.text : {
*(multiboot)
*(.text)
}
.data ALIGN(4096) : {
*(.data)
}
.rodata ALIGN(4096) : {
*(.rodata)
}
.bss ALIGN(4096) : {
*(.bss)
}
}
Anschließend muss der Linker auch noch wissen, dass er die Datei benutzen soll. Daher wird in der Makefile das -Ttext=0x100000 zu einem -Tkernel.ld
==printf==
Nach sehr viel sehr detailliert besprochenem Code kommen wir zu einigen Aufgaben, die ich dem geneigten Leser gern zur Übung überlassen würde. Es handelt sich dabei um Funktionen zur Textausgabe - denn man kann keine Fehler finden, wenn man sich keine Informationen anzeigen lassen kann. Wir brauchen mindestens:
* Eine Funktion zum Löschen des Bildschirms (nicht zwingend, sieht aber einfach schöner aus)
* Eine Funktion um Text auszugeben
* Eine Funktion um Zahlen in dezimaler Schreibweise auszugeben
* Eine Funktion um Zahlen und Pointer in hexadezimaler Schreibweise auszugeben
In den folgenden Teilen gehe ich davon aus, dass die unteren drei Aufgaben von einer Funktion kprintf() übernommen werden, die für die genannten Fälle genau so funktioniert, wie die normale printf()-Funktion. Die Funktionen kommen am besten in ihre eigene Quellcodedatei (z.B. console.c) und werden in den Beispielen in der Headerdatei console.h erwartet.
Für die Implementierung von printf benötigt man stdarg.h, um die variable Parameterliste auszuwerten. gcc bietet dafür bereits Builtins an (bzw. eine komplette stdarg.h Header, wenn man einen [[Cross-Compiler]] benutzt), so dass die Headerdatei nur folgendes enthalten muss: