Seitenleiste

Tutorial

Infodatenbank

Community

Der erste Start

Zunächst einmal möchten wir uns in diesem Abschnitt einen Kernel schreiben, der nichts weiter tut als starten. Klingt nicht besonders aufregend und anspruchsvoll.

Aber ich kann dir versichern: Anspruchsvoll ist es schon ein bisschen. Vielleicht mehr, als mancher denkt.

Aufregend wird es dennoch nicht aussehen. Genauer gesagt werden wir sogar sehr wenig sehen…

Wie starten wir?

Wie wir starten ist schnell geklärt. Es ist nur wichtig, dass du verstehst, wie was warum gemacht wird.

Wir lassen unseren Kernel von GRUB starten. GRUB lädt unseren Kernel in den Speicher und wechselt dann zu dieser Stelle im Speicher, sodass der Prozessor den Code ausführt. In unserem Code befindet sich ein Abschnitt, der GRUB zeigt, das unser Kernel ein Kernel ist und geladen werden möchte.

Der erste Code

Wirklich (!) nur ein bisschen Assembler

Erstellen wir also eine erste Datei (basic-kernel-asm.asm):

[BITS 32]
 
[GLOBAL start]
start:
    mov esp, _sys_stack ; ##1
    jmp c_kernel_entry
 
 
; In den C-Kernel wechseln
c_kernel_entry:
    [EXTERN _c_kernel_main]
    call _c_kernel_main
    hlt
 
; Unser Stack
SECTION .bss
    resb 8192
_sys_stack: 
 
; Hier ist der Multibootheader. Wird gebraucht, um mit GRUB zu booten
SECTION .multiboot_data
ALIGN 4
multiboot_header:
    dd 0x1BADB002     ; Magic Nummer
    dd 0x0            ; Flags
    dd (-0x1BADB002)  ; Prüfsumme

In der ersten Zeile erklären wir NASM, dass wir 32 Bit Code benutzen. 32 Bit Code ist Code, der nur im Protected Mode ausgeführt wird. Den Wechsel in den Protected Mode hat GRUB schon für uns übernommen. Machen wir uns darüber also keine weiteren Gedanken.

Die nächste Anweisung markiert den Punkt „start“ als GLOBAL. Damit ist es auch außerhalb der Datei sichtbar. Wir brauchen das, um unsere Datei richtig zusammenlinken zu können.

In der Zeile ##1 legen wir die Adresse unseres Stacks auf den Stackpointer. Unseren Stack definieren wir am Ende der Datei. Wir reservieren dafür 8 KB ( 8192 Byte ). Da der Stack „nach unten“ (von der Adresse her) wächst, definieren wir den „Startpunkt des Stacks“ ( _sys_stack ) erst nach der Reservierung.

Anschließend springen wir zum Label c_kernel_entry ein Stück weiter unten im Code.

Nach dem Label rufen wir vorerst nur unsere Mainfunktion auf. Diese schreiben wir in einem Moment gleich noch. Sollten wir durch einen unglücklichen Umstand zurück an diesen Punkt kommen, dann halten wir den Prozessor mit hlt an.

Ganz am Ende befindet sich der Multiboot Header. Dieser muss sich im Code befinden, andernfalls lädt GRUB unseren Kernel nicht. Außerdem muss sich der Header auf einer 16 Bytegrenze im „vorderen Bereich“1) der fertigen Kerneldatei befinden. Das erreicht man mit der Anweisung ALIGN 4 und durch das Schieben in eine eigene Section, die wir später beim Linken an den Beginn unserer Kerneldatei linken lassen. Die 4 bedeutet 2^4 = 16. 2)

Der erste C Code

Nachdem wir nun unseren grundlegenden Assemblercode geschrieben haben, sollten wir ein bisschen C Code schreiben, um zu testen, ob wir das auch ausführen können.

Unser C Code wird vorerst nichts besonderes tun. Er wird einfach nur ein Zeichen auf den Bildschirm schreiben und dann in einer Endlosschleife bleiben.

void _c_kernel_main(void)
{
   //Wir geben ein Zeichen aus.
   //Dazu schreiben wir an eine bestimmte Adresse im Speicher.
   unsigned short *video_buffer = (unsigned short *)0xB8000;
   *video_buffer = 'A' | 0x7;
   for(;;);
}

Zusammenbasteln

Nun müssen wir aus unserem Code natürlich auch noch etwas machen. Compiler und Co kommen jetzt zum Einsatz.

Kompilieren

Zuersteinmal müssen wir unseren Code kompilieren. Ich verwende dazu den GCC. Wer einen anderen Compiler verwenden möchte, soll sich nicht behindert fühlen.

gcc -m32 -fno-builtin -fno-leading-underscore -fno-strict-aliasing -fno-stack-protector -Wall -O0 -c -g SOURCEDATEI.c

Gehen wir diese Parameter nacheinander durch:

Parameter Wirkung & Warum
-m32 Adressen im Code werden als 32 Bit Adressen gesehen. Hat den Grund, da wir für eine 32 Bit CPU programmieren.
-fno-builtin Es werden keine Bibliotheken verwendet. Wichtig, da Bibliotheken in unserem Kernel nicht funktionieren werden.
-fno-leading-underscore Kein „_“ vor die Funktionsnamen setzen. Erleichtert uns den Aufruf von C-Funktion aus Assemblercode heraus. (Wir müssten sonst __c_kernel_main schreiben)
-fno-strict-aliasing FIXME(Weiß ich selbst gerade nicht)
-fno-stack-protector Deaktiviert den Stackschutzmechanismus. Wir werden uns selbst um unseren Stack kümmern (müssen).
-Wall Alle Warnungen ausgeben. Führt zu vielen Warnungen, aber besser wir wissen, wo ein Fehler stecken könnte, als das wir ihn erst ewig suchen müssten.
-O0 Optimierungsgrad auf 0 stellen. Optimierungsaufgaben werden wir selbst erledigen müssen(!)…
-c Nur kompilieren, nicht linken. Das wird ld in ein paar Momenten für uns erledigen. (Wichtig!)
-g FIXME (Genaue Funktion nachsehen)

Assemblieren

Zum Assemblieren benutze ich nasm. Andere Assembler sollten kein Hindernis sein, es muss nur auf die richtige Syntax geachtet werden. Wichtig ist auch, dass das Ausgabeformat ELF32 ist.

nasm -f elf ASMCODE.asm

Der Parameter -f elf legt das Ausgabeformat auf ELF fest. Ansonsten benutze ich keine weiteren Parameter.

Linken

Nachdem wir nun unseren gesamten Code compiliert haben, sollten wir die einzelnen Objektdateien auch zusammenlinken.

Ich verwende dazu den Linker ld, der eigentlich bei jeder Linuxdistribution vorhanden ist.

Um den Linker zu steuern werden wir ein Linkerscript verwenden.

Eine Erklärung dazu ist im nächsten Abschnitt zu finden

1)
Genauer Bereich nicht bekannt
2)
NOTE: Sicher…?