Betriebssystementwicklung
Aus Wikibooks
Inhaltsverzeichnis |
[Bearbeiten] Vorwort
[Bearbeiten] Zusammenfassung des Projekts
- Zielgruppe: Erfahrene Programmierer
- Lernziele: Programmierung eines Betriebssystems für die i386-Architektur und besseres Verständnis für die Arbeitsweise eines PCs
- Buchpatenschaft / Ansprechperson: *niemand*
- Sind Co-Autoren gegenwärtig erwünscht? Co-Autoren sind sehr erwünscht, vor allem für betriebssystemspezifische Belange wie die Wahl des Compilers
- Richtlinien für Co-Autoren: Möglichst nicht an eine spezielle Sprache binden, verschiedene Möglichkeiten Aufzeigen. Nur vereinzelte Code-Beispiele zur Anschaulichkeit, kein komplettes Beispielprojekt. Erklärung eher theoretischer Aspekte in Hinsicht auf deren praktische Anwendung.
- Projektumfang und Abgrenzung zu anderen Wikibooks: Im Gegensatz zum Buch Betriebssystemtheorie soll hier eher das praktische Wissen für die Programmierung eines eigenen Betriebssystems vermittelt werden. Aufgrund der Tatsache, dass es zu viele unterschiedliche Computerarchitekturen gibt, um auf sie alle einzugehen, beschränkt sich dieses Buch auf die Entwicklung für einen x86er- oder x86-64er-Prozessor, also für einen handelsüblichen PC.
[Bearbeiten] Voraussetzungen
Wer sich an dieses Buch heranwagt, sollte in jedem Fall in den Grundlagen der Betriebssystemtheorie bewandert sein, da diese essentiell für eine praktische Umsetzung in Form eines eigenen Betriebssystems ist. Sehr hilfreich kann es hierbei sein, sich mit einem freien Betriebssystem zu beschäftigen, etwa Linux oder BSD, da diese sehr tiefe Einblicke in die Funktionalität eines Betriebssystems liefern. Hilfreich hierfür ist etwa das Buch Betriebssystemtheorie oder das erste Kapitel des openBooks Linux - Das distributionsunabhängige Handbuch.
Neben Kenntnissen in der Systemtheorie ist auch die kompetente Beherrschung einer kompilierten Programmiersprache vonnöten, da nur eine solche unter der Abschaltung systemspezifischer Eigenheiten in der Lage ist, freistehenden Binärcode zu erzeugen, der ohne Betriebssystem bzw. Interpreter lauffähig ist. Zwar ist es möglich, bereits auf sehr tiefer Ebene einen Interpreter in das Betriebssystem zu integrieren, dennoch muss die Basis aus freistehendem, betriebssystemunabhängigem Binärcode bestehen. Interpretersprachen wie Java, Python oder die .NET-Sprachen scheiden damit aus. Die deutlich am häufigsten verwendete Sprache für die Systemprogrammierung ist C (manchmal auch C++), auf dem nahezu alle UNIX-Derivate (damit auch MacOS X) und Windows aufbauen. Ebenfalls verwendet wurde auch Pascal für Mac OS/2, das frühere Mac-Betriebssystem, das mittlerweile allerdings durch das BSD-ähnliche OS X ersetzt wurde.
Etwas, um das man auch heute bei der Betriebssystemprogrammierung noch nicht herumkommt, ist Assembler. Auch wenn moderne Betriebssysteme zu einem Großteil (gewöhnlich weit über 90% des Codes) in einer höheren Programmiersprache geschrieben sind, können Hardware-Funktionalitäten auf tiefer Ebene, die für ein Betriebssystem benötigt werden, doch nur mit Assembler realisiert werden, wie etwa das Laden des Kernels oder das auslösen von Interrupts. Da die Syntax der meisten Assembler ebenso wie ihr Befehlssatz relativ simpel ist, zählt hierfür vor allem die Hardware-Kenntnis, insbesondere über die CPU und ihre Register. Zu den Teilen des Betriebssystems, die in Assembler entwickelt werden müssen, existieren jedoch im Internet sehr viele Tutorials und Code-Beispiele, die das ganze selbst für jene, die Assembler nicht beherrschen oder sich nicht allzu sehr damit außeinandersetzen wollen, relativ einfach machen.
[Bearbeiten] Werkzeuge
Aus der Wahl der Programmiersprache ergeben sich letztendlich die benötigten Werkzeuge. Diese unterscheiden sich von Plattform zu Plattform. Deshalb findet sich im folgenden eine Liste von Werkzeugen und Tools für verschiedene Programmiersprachen, die wiederum nach dem verwendeten System geordnet sind. All diese Programme sind Open Source Software und können somit frei verwendet werden. Sie bieten sich sogar für eine eventuelle Portierung auf das eigene Betriebssystem an, sobald dieses sich in einem fortgeschritteneren Status befindet.
[Bearbeiten] Assembler
Da Assembler auf jeder Plattform und unabhängig von der Wahl der Programmiersprache immer benötigt wird, wird mit der Beschreibung der Einrichtung eines Assemblers begonnen.
[Bearbeiten] Linux/UNIX
Auf einem Linux/UNIX-System kommen hauptsächlich zwei Assembler in Frage, die beide freie Software und für die meisten UNIX-Derivate verfügbar sind. Der Erste ist der GNU Assembler, kurz gas. gas verwendet die AT&T-Syntax und wird von der GNU Compiler Collection intern benutzt. In C/C++ eingebundene Inline-Assemblerdirektiven entsprechen der Syntax von gas.
Die zweite Möglichkeit ist der Netwide Assembler, kurz nasm. Dieser folgt der einfachereren Intel-Syntax und existiert auch für Windows, weswegen er in Tutorials bevorzugt verwendet wird. Ein weiterer Vorteil ist das mächtige Makro-System, das bei richtigem Einsatz eine enorme Einsparung von Aufwand erlaubt. Aus diesen Gründen sind auch die Assembler-Beispiele dieses Buches in der nasm-Syntax verfasst. Das Buch x86 Assembly aus dem englischen Wikibooks stellt die Unterschiede zwischen gas- und nasm-Syntax gut dar. Der Code bleibt jedoch prinzipiell der gleiche.
Durch das folgende Kommando erzeugt man mit nasm eine betriebssystemunabhängige Binärdatei (natürlich ist diese nur solange betriebssystemunabhängig wie keine systemspezifischen Funktionsaufrufe getätigt wurden) im Executable and Linkable Format, kurz elf. Diese Binärdatei kann nun in den Kernel gelinkt werden.
nasm -f elf assembler.asm assembler.o
Will man eine eigenständige (nicht mehr zu linkende) Datei erzeugen, muss man das durch den Schalter -f definierte Format elf durch bin ersetzen. Das ist etwa erforderlich, wenn man einen eigenen Bootloader schreibt.
[Bearbeiten] C/C++
Die meisten aktuellen Betriebssysteme sind zu einem Großteil in C oder C++ geschrieben, da diese Sprachen sehr hardwarenah sind, aber gleichzeitig auch alle Eigenschaften einer Hochsprache aufweisen. Besonders bei C++ gestaltet sich die Einrichtung und Verwendung für ein Betriebssystem schon etwas schwieriger, da es standardmäßig einige plattformspezifische Features direkt in die Sprache integriert hat, die erst einmal deaktiviert werden müssen. Wie einige dieser Features auch in das eigene Betriebssystem integriert werden können, wird hier sehr gut beschrieben (englisch). Mit den folgenden Kommandos sollte es auch auf modernen 64bit-Systemen möglich sein, einen 32bit-Kernel zu kompilieren.
[Bearbeiten] Linux/UNIX
Der ultimative Compiler unter Linux/UNIX ist zweifellos der gcc aus der GNU Compiler Collection. Dieser ist auf so gut wie jeder Linux-Distribution der Standard-Compiler und auf nahezu alle UNIX-Derivate portiert. Außerdem unterstützt er eine Vielzahl an Features und Optimierungsmöglichkeiten und erlaubt eine präzise Konfiguration, weswegen auch ausschließlich auf diesen eingegangen werden muss.
Für eine betriebssystemunabhängige Binärdatei müssen dem Compiler mehrere Parameter übergeben werden. Unter C muss signalisiert werden, dass es sich um eine freistehende Bibliothek handelt, startfiles müssen abgeschaltet werden und die Standardbibliothek sowie andere standardmäßig hinzugelinkten Bibliotheken ausgeschlossen. Der Befehl lautet dann folgendermaßen:
gcc -ffreestanding -fno-builtin -nostdlib -nostartfiles -nodefaultlibs -c -o ausgabedatei.o eingabedatei.c
Bei C++ kommen noch Exceptions und runtime type information hinzu, die beide standardmäßig nicht verfügbar sind. Durch die Parameter -fno-rtti und -fno-exceptions werden diese deaktiviert:
g++ -ffreestanding -fno-builtin -nostdlib -nostartfiles -nodefaultlibs -fno-rtti -fno-exceptions
Diese Kommandos erzeugen aber lediglich nicht ausführbare Objektdateien (ebenso wie der Assembler dies tut). Damit diese zu einem einzigen Kernel verschmelzen, müssen sie noch gelinkt werden. Unter Linux/UNIX ist hierbei ld die erste Wahl, der ebenfalls Teil der GNU Compiler Collection ist und von gcc intern benutzt wird. Das von ld standardmäßig vorgegebene Linkerscript ist jedoch nicht für die Zwecke eines Betriebssystems geeignet und muss deshalb durch ein eigenes ersetzt werden. Ein passendes könnte etwa so aussehen:
/* linkerscript.ld */ /* Original file taken from Bran's Kernel Development */ /* tutorials: http://www.osdever.net/bkerndev/index.php. */ OUTPUT_FORMAT(elf32-i386) OUTPUT_ARCH(i386:i386) ENTRY(start) SECTIONS { .text 0x100000 : { code = .; _code = .; __code = .; *(.text) . = ALIGN(4096); } .data : { data = .; _data = .; __data = .; *(.data) *(.rodata) . = ALIGN(4096); } .bss : { bss = .; _bss = .; __bss = .; *(.bss) . = ALIGN(4096); } end = .; _end = .; __end = .; }
Mit dem folgenden Kommando wird dann der Linker aufgerufen:
ld -T linkerscript.ld -o kernel.bin objektdatei.o [objektdatei2.o [objektdatei3.o [...]]]
[Bearbeiten] Testen
[Bearbeiten] Unter Linux
Um den logischen Aufbau des Buches nicht zu stören, wird schon an dieser Stelle beschrieben, wie das fertig kompilierte Betriebssystem getestet werden kann. Dazu muss es zuerst einmal in ein Diskettenimage verpackt werden. Die einfachste Möglichkeit dazu ist es, ein bereits vorhandenes Diskettenimage zu bearbeiten und für die eigenen Zwecke zu verwenden. Ein geeignetes Image wäre beispielsweise das hier: [1].
Um dieses Image zu bearbeiten, muss es zuerst gemountet werden, beispielsweise in /media/floppy. Das Shell-Kommando dazu sieht folgendermaßen aus:
mount Pfad_zum_Image /media/floppy -o loop
Dies bewirkt, dass der Inhalt des Images sich jetzt im Ordner /media/floppy befindet, wo er beliebig verändert werden kann.
Zuerst müssen alle Dateien außer dem Ordner grub gelöscht und die eigene ausführbare Kerneldatei eingefügt werden. Danach muss die vorhandene menu.lst durch die eigene überschrieben werden (siehe Abschnitt GRUB verwenden). Um die Änderungen zu übernehmen, muss man das Image einfach unmounten.
Mit dem fertigen Image kann nun zweierlei angestellt werden:
[Bearbeiten] Image auf Diskette schreiben
Die erste Möglichkeit ist es, das Image auf eine Diskette zu schreiben und damit eine Bootdiskette zu erzeugen. Dazu wird das Programm dd verwendet.
Zuerst muss die Diskette gemountet werden. Ist dies geschehen, kann das Image folgendermaßen auf die Diskette geschrieben werden:
dd if=Pfad_zum_Image of=/dev/fd0
Bei einem Neustart mit der Diskette im Laufwerk sollte nun von dieser aus gebootet werden.
[Bearbeiten] Image in einem Emulator testen
Die eben beschriebene Variante hat jedoch zwei entscheidende Nachteile. Zum einen muss der Rechner andauernd neugestartet werden und zum anderen können Programmierfehler im schlimmsten Falle Hardware zerstören. Deshalb ist es sicherer, einen Emulator zu verwenden, der einen virtuellen PC bereitstellt. Hier wird auf den QEMU-Emulator eingegangen, eine Alternative wäre beispielsweise Bochs.
QEMU kann von dieser Website bezogen werden und lässt sich nach dem üblichen Schema für die eigene Plattform installieren. Um damit das Image zu starten, reicht das Kommando
qemu -fda Pfad_zum_Image
aus. Es öffnet sich daraufhin ein kleines Fenster, in dem das Betriebssystem erscheint. Mehr Infos zu QEMU gibt es im Wikibook "QEMU und KVM".
Ein weiterer, sehr verbreiteter Emulator ist bochs (sprich engl. box), der allerdings eine weitreichendere Konfiguration erfordert, bevor er verwendet werden kann. Eine (englische) Dokumentation findet sich auf der offiziellen Website des Projektes. Eine Beispiel-Konfigurationsdatei, die ein Diskettenimage mit dem Dateinamen floppy.img beim Aufruf des Kommandozeilenbefehls bochs im selben Verzeichnis ausführt, sieht folgendermaßen aus:
megs: 128 cpu: count=1, ips=43000000 romimage: file=$BXSHARE/BIOS-bochs-latest, address=0xe0000 vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest vga: extension=none floppya: 1_44=icyx.img, status=inserted boot: floppy display_library: x log: bochs.log clock: sync=realtime mouse: enabled=0 keyboard_mapping: enabled=1, map=$BXSHARE/keymaps/x11-pc-de.map
Diese Datei muss einfach als bochsrc.txt abgespeichert werden und gibt bochs alle Informationen, die es benötigt, um von der Diskette zu starten.
[Bearbeiten] Der Bootvorgang
[Bearbeiten] Das BIOS
Im Buch Betriebssystemtheorie kaum beschrieben ist der Bootvorgang. Deswegen wird hier genauer darauf eingegangen. Dazu muss erst einmal klargestellt werden, was passiert, nachdem bei einem Rechner der Anschaltknopf gedrückt wurde. Zuerst wird das BIOS, das Basic Input/Output System geladen, die Firmware des PCs. Es initialisiert die Hardware, führt einige Checks durch (etwa denn Power on self test) und stellt einige BIOS-Interrupts zur Verfügung, mit denen sich etwa der Grafikspeicher oder die Festplatte ansteuern lässt. Das BIOS sucht nun im ersten Sektor (den ersten 512 Bytes) jedes Datenträgers, der in der von Seiten des BIOS eingestellten Boot-Device-Liste vorkommt, nach einem Bootloader, der durch einen bestimmten Code gekennzeichnet wird. Findet es einen Bootloader, wird dieser aufgerufen und übernimmt die Kontrolle.
[Bearbeiten] Der Bootloader
Wie erkennt BIOS nun den Bootloader? Es sucht im ersten Sektor jedes Datenträgers nach einer ausführbaren Datei, die mit der sogenannten Magicnumber AA55hex endet. Diese Datei wird dann aufgerufen und lädt für gewöhnlich den Kernel des Betriebssystems.
Dadurch, dass die Datei den ersten Sektor ausfüllen muss, hat sie exakt 512 Bytes groß zu sein. Aufgrund dieser Beschränkung wird normalerweise Assembler für den Bootloader verwendet, der schlanken Code erzeugt und den direktesten und damit einfachsten Weg darstellt, den Betriebssystemkernel zu laden. Es ist theoretisch auch möglich, einen Bootloader in einer Compilersprache wie C zu schreiben, was aber sehr aufwändig und schwierig ist, außer man setzt einen sogenannten 2-Stage-Bootloader ein. Die erste Stufe (oben beschrieben) startet die zweite Stufe, die überall auf der Festplatte liegen kann. Dies hat zu Beispiel den Vorteil, dass man sich kaum um die 512 Byte Beschränkung kümmern braucht.
Sie sind jedoch nicht gezwungen, einen eigenen Bootloader zu schreiben. Es gibt viele gute Bootloader, auf die Sie zurückgreifen können. Von diesen ist GRUB empfehlenswert, der GRand Unified Bootloader. Wie man einen Kernel mit diesem bootbar macht, wird später beschrieben. Zuerst einmal erfolgt eine kurze Anleitung, was ein selbstgeschriebener Bootloader können sollte und wie man ihn realisieren könnte. Es empfiehlt sich auch denjenigen, die nicht vorhaben, einen eigenen Bootloader zu schreiben, diesen Abschnitt durchzulesen, da er das allgemeine Verständnis verbessert.
[Bearbeiten] Einen eigenen Bootloader schreiben
Wenn Sie selbst einen Bootloader schreiben wollen, werden Sie sich sicher fragen, was genau ein Bootloader alles tun muss. Im Prinzip gar nichts, außer 512 Bytes groß sein, sich im ersten Sektor zu befinden und mit der Magicnumber zu enden. Aber ein solcher Bootloader wäre sinnlos, da er ja nichts tut. Zuerst einmal muss der Bootloader an die Speicheradresse 0x7C00 geladen werden. Der NASM stellt dafür beispielsweise die Assemblerdirektive ORG bereit. Dann muss ein Stack an der Adresse 0x9000 initialisiert werden, indem das Stacksegmentregister diese Adresse als Wert erhält und das Stackpointerregister auf 0 gesetzt wird. Währenddessen sollten Interrupts deaktiviert sein.
Damit wäre das Programm geladen. Danach geht es darum, die Kernel-Datei zu finden. Dabei gibt es zwei Möglichkeiten: Entweder man macht sich die Eigenschaften des jeweilig verwendeten Dateisystems zu Nutze und nimmt dabei eine gewisse Plattformabhängigkeit in Kauf, oder man arbeitet mit BIOS Interrupts, die beispielsweise das Lesen von Sektoren erlauben, und unabhängig vom verwendeten Dateisystem sind. Im Folgenden wird auf die Variante der BIOS Interrupts näher eingegangen.
Das BIOS übergibt die Nummer des Bootlaufwerkes über das dl-Register. Dieses Laufwerk wird nun mit Hilfe von BIOS-Interrupts Sektor für Sektor ausgelesen. Der darin befindliche Kernel wird in den Arbeitsspeicher geladen und durch einen jump zur Ausführung gebracht. damit ist die Arbeit des Bootloaders getan und das eigentliche Betriebssystem steuert den Rechner nun.
[Bearbeiten] GRUB verwenden
Anfängern wird häufig davon abgeraten, einen eigenen Bootloader zu schreiben, da dies sehr fehleranfällig ist und auch einen gewissen Grad an Erfahrung erfordert. Ein Vorteil dieser Praktik ist zwar, dass man nebenbei einiges über den Bootvorgang des PCs und BIOS-Interrupts lernt, das Verwenden von GRUB ist aber deutlich komfortabler und, da GRUB der wohl am häufigsten verwendete Bootloader ist (wenn man von der Zahl der Betriebssysteme ausgeht, die ihn verwenden und nicht von den Anwendern), auch portabler.
GRUB ist hierbei ein sogenannter multistage-Bootloader. Wie nach der Lektüre des vorherigen Abschnittes bekannt ist, durchsucht das BIOS jeweils nur den ersten Sektor, die ersten 512 Bytes eines jeden bootbaren Gerätes nach dem Bootloader. Da diese 512 Bytes jedoch kaum ausreichen, um einen so komplexen und umfangreichen Bootloader wie GRUB vollständig zu beherbergen, greift man auf das sogenannte multistage-Prinzip zurück. Dabei wird ein genau 512 Bytes großes Programm, stage1, in den Bootsektor abgelegt. Diese Datei bootet nun jedoch nicht selbst den Kernel, sondern lädt erst einmal den eigentlichen Bootloader, stage2, der beliebig groß sein kann, da er außerhalb des Bootsektors liegt.
stage2 erkennt eine bootbare Kerneldatei dann dadurch, dass sie den Multiboot-Header enthält. Dabei handelt es sich um einen Bereich im Speicher des Programms, das, an 4 Bytes ausgerichtet, die Magicnumber, eine bestimmte Zahl, die den Kernel GRUB erkenntlich macht, Flags und eine Prüfsumme enthält. Ein Beispielcode dafür wäre z.B.:
;für mehr Informationen schauen Sie sich am besten die GRUB-Dokumentation an multiboot_header: dd 0x1BADB002 ;Magicnumber dd 0x0 ;Flags (können verändert werden) dd (-0x1BADB002) ;Prüfsumme
Dieses Label kann an jeder beliebigen Stelle im Quelltext des Programmes stehen, das - wie ein Bootloader auch - in Assembler geschrieben werden MUSS, da noch kein Stack initialisiert ist, was C/C++-Programme jedoch benötigen. Folglich ist es die Aufgabe des Programmes, den Stack zu initialisieren, der normalerweise an Adresse 0x200000 beginnt, und die in C/C++ geschriebene Hauptfunktion des Kernels aufzurufen. Die assemblierte Datei wird dann zum Kernel hinzugelinkt, der fortan für GRUB erkennbar ist. Beim nächsten Boot ruft GRUB also den Kernel mit der multiboot structure als Parameter auf, einer Struktur, die einige Informationen über den Kernel, seinen Ort im Speicher und eventuelle GRUB-Module wie eine initrd enthält. Zu Beginn muss man sich um diese Informationen jedoch kaum kümmern, sie werden erst später, bei der Implementierung einer eigenen Initial RAM Disk interessant.
Momentan wäre der kompilierte Kernel allerdings nur aus der Kommandozeile von GRUB bootbar, da noch ein Eintrag in der menu.lst fehlt, die Konfigurationsdatei von GRUB. Im Bootmenü wäre das Betriebssystem damit praktisch unsichtbar. Um das zu ändern, sollte die Datei menu.lst (im Ordner grub zu finden) editiert bzw. angelegt werden. Ein simpler Eintrag sieht folgendermaßen aus:
timeout Sekunden_bis_autostart title Name_des_Betriebssystems kernel Pfad_zum_Kernel
[Bearbeiten] Erste Schritte
[Bearbeiten] Textausgabe
Die erste Aufgabe eines Betriebssystemkernels ist zweifellos die Möglichkeit bereitzustellen, Text auf dem Bildschirm auszugeben. Das geht jedoch nicht so leicht, wie es sich anhört. Es existiert nämlich noch keine Standardbibliothek - die wird erst vom Betriebssystem bereitgestellt. Folglich kann nicht einfach eine vorgefertigte Funktion wie printf() (C) oder die ostream-Klasse (C++) verwendet werden. Die Ausgabefunktion muss selbst geschrieben werden. Die Frage ist nun, wie man das anstellt. Im Realmode könnte man über BIOS-Interrupts Text ausgeben, da, wie wir bereits wissen, GRUB jedoch von Anfang an im Protected Mode bootet, kommt diese Möglichkeit nicht in Frage, auch, da diese bei komplexeren Aufgaben zu aufwändig zu bedienen wäre.
Die andere und weit empfehlenswertere Möglichkeit ist es, einen Texttreiber für den VGA-Controler der Grafikkarte zu schreiben. Das ist bei weitem nicht so schwer, wie es sich anhört. Wenn man die Grundprinzipien verstanden hat, ist die Textausgabe eine der einfachsten Aufgaben eines Betriebssystems. Der Texttreiber sollte in etwa folgende Funktionalitäten bereitstellen:
- ein Zeichen/String auf den Bildschirm schreiben
- den Text scrollen
- den Bildschirm leeren
- den hardwareimplementierten Cursor versetzen
[Bearbeiten] Zeichen ausgeben
Um diese Funktionalitäten verfügbar zu machen, muss man zuerst einmal verstehen, wie die Textausgabe überhaupt realisiert wird. Das funktioniert nach einem relativ simplen Prinzip: Im Grafikspeicher jeder Grafikkarte gibt es einen genau 4.000 Byte großen Bereich, der sich Textspeicher nennt und sich wie ein Teil des Arbeitsspeichers addressieren lässt. In diesem Bereich ist Platz für 2.000 ASCII-Zeichen, jedes zwei Byte groß. Dabei ist jedoch nur
das erste Byte für den Zeichencode reserviert, das zweite Byte enthält Informationen über die Vorder- und Hintergrundfarbe des Zeichens, die in jeweils einem 4-Bit-Abschnitt definiert werden, der 16 verschiedene Werte erlaubt. Dabei entsprechen die oberen 4 Bit der Hintergrund- die unteren 4 der Schriftfarbe. Das Attributbyte (der Name des Bytes, das die Farben definiert, im Gegensatz zum Zeichenbyte, in dem der Zeichencode gespeichert ist) liegt wiederum unterhalb des Zeichenbytes (siehe Abbildung).
Diese 2.000 Zeichen teilen sich auf 25 Zeilen zu je 80 Zeichen auf. Dabei wird kein Extra-Zeichen für einen Zeilenumbruch benötigt, alles, was über eine Zeile hinausgeht, wird in die nächste geschrieben. Deshalb verwendet man normalerweise zwei globale Variablen (bzw. eine globale Struktur, die diese beiden Variablen beinhaltet), eine für die y-Position des Cursors (also die aktuelle Zeile) und eine für die x-Position des Cursors (die aktuelle Spalte). Verwendet man nun einen 1 Byte großen Zeigerdatentyp, der mit 0xB8000 auf die Basisaddresse des Textspeichers zeigt, kann man mit folgender Formel die aktuelle Cursor-Position auf ein Byte im Videospeicher projizieren:
Addresse: 0xB8000 + (position_y * 80 + position_x) * 2
Dadurch erhält man die Adresse des Zeichenbytes an der aktuellen Cursorposition. Für das Attributbyte muss die Adresse einfach um eins inkrementiert werden.
Die Verwendung der Positionsvariable bringt viele Vorteile mit sich, etwa eine vereinfachte Behandlung von Zeilenumbrüchen: Es muss einfach nur die position_y-Variable inkrementiert werden und das nächste auszugebende Zeichen wird in die nächste Zeile geschrieben. Eine Funktion zur Ausgabe eines einzelnen Zeichens sähe etwa so aus:
- Berechne die aktuelle Zeichenadresse aus den Positionsvariablen
- Wenn das angegebene Zeichen nicht druckbar ist, realisiere es anderweitig
- Generiere ein Attributbyte (etwa 0x0F für schwarz-weiß)
- Schreibe das Zeichenbyte an die Zeichenadresse und inkrementiere sie
- Schreibe das Attributbyte an die neue Adresse
- Aktualisiere die Positionsvariablen
- Setze den Hardware-Cursor
- Prüfe, ob gescrollt werden muss
[Bearbeiten] Scrolling
Hierin werden gleich zwei noch nicht behandelte Funktionalitäten benötigt: Scrolling und das Setzen des Hardware-Cursors. Das Scrolling lässt sich recht einfach realisieren: Es wird einfach in einer Schleife jedes Zeichen mit dem Zeichen an der selben Position in der nächsten Zeile überschrieben. Ein kurzes C-Beispiel zur Demonstration:
for(i = 0;i < 24 * 80 * 2;i++) // videospeicher ist ein Zeiger auf die Adresse 0xB8000 videospeicher[i] = videospeicher[i + 80];
[Bearbeiten] Hardware-Cursor
Das Setzen des Hardware-Cursors gestaltet sich schon etwas schwieriger, da hierbei der VGA-Controller direkt über den I/O-Bus angesprochen werden muss. Dazu verwendet man den im vorherigen Abschnitt beschriebenen x86-Assembler-Befehl outb (für engl. outbyte), mit dem man über die I/O-Ports des VGA-Controlers zuerst das Kommando zum Setzen des Cursors und schließlich die Position des Cursors sendet. Schritt für Schritt läuft das ganze folgendermaßen ab:
- Über die Formel position_y * 80 + position_x wird die (zwei Byte große) Positionsvariable berechnet
- Über den Kommandoport 0x3D4 wird das Kommando 14 gesendet, das dem VGA-Controler sagt, dass wir als nächstes über den Datenport das untere Byte der Cursor-Positionsvariable (Bits 0 bis 7 wenn von rechts nach links gelesen wird, wie das auf dem PC als Little-Endian-System üblich ist)
- Über den Datenport 0x3D5 werden die unteren Bits der Positionsvariable (= cursorLocation >> 8) gesendet
- Über den Kommandoport 0x3D4 wird das Kommando 15 gesendet, das dem VGA-Controler die nächsten 8 Bit ankündigt.
- Über den Datenport 0x3D5 wird das obere Byte (= cursorLocation) gesendet
[Bearbeiten] Bildschirm leeren
Bleibt noch das Leeren des Bildschirms, das nach allem extrem einfach ist: Es wird schlicht und ergreifend jedes Byte im Videospeicher in einer Schleife mit 0 initialisiert, was den Bildschirm schlagartig leert.
[Bearbeiten] Attributbyte
| Nummer | Farbe |
|---|---|
| 0 | schwarz |
| 1 | dunkelblau |
| 2 | dunkelgrün |
| 3 | dunkeltürkis |
| 4 | dunkelrot |
| 5 | dunkellila |
| 6 | dunkelgelb/orange |
| 7 | hellgrau |
| 8 | dunkelgrau |
| 9 | hellblau |
| A | hellgrün |
| B | helltürkis |
| C | hellrot |
| D | helllila |
| E | hellgelb |
| F | weiß |
Bisher nur kurz erwähnt wurde das Attributbyte, das die Farbe eines jeden Zeichens auf dem Bildschirm definiert. Da jedes Attributbyte zweigeteilt ist und je vier Bit für Schrift- und Hintergrundfarbe bereitstellt, lässt sich daraus schlussfolgern, dass der VGA-Controler nativ 16 Farben unterstützt. Eine Tabelle der Hexadezimalen Farbwerte folgt:
Das Attributbyte, das für gewöhnlich standardmäßig verwendet wird, ist schwarz-weiß, also 0x0F, manchmal auch 0x07, hellgrau auf schwarz. Zu beachten ist, dass (bei einem hexadezimalen Wert) die erste Ziffer den Hintergrund und die zweite den Vordergrund darstellt. 0xF0 würde also einen schwarzen Schriftzug auf weißem Hintergrund bedeuten.
Im Folgenden ein simples Beispiel, das den Text Hello World! in hellroter Schrift auf dem Bildschirm ausgibt:
void kmain(void) { char *video = 0xB8000; char *str = "Hello World!"; unsigned short i; while(str[i] != '\0') { *video = str[i]; ++video; *video = 0x0C; } return; }
[Bearbeiten] Weiterführende Informationen
[Bearbeiten] Links
[Bearbeiten] Tutorials
- lowlevel - Mit vielen Tutorials
- osdever.net.tc - Sammlung von Dokumenten
- How Stuff Works - Erklärt, wie ein Betriebssystem funktioniert (en)
- BonaFide OSDevelopment - Eine DER Seiten rund um Betriebssystementwicklung! (en)
- OSRC - Sehr viele Tutorials (en)
- Neu entstehendes Kernel Tutorial auf Proggen.org
- OSDev.org - Ein Wiki und ein Forum zum Thema (en)
[Bearbeiten] Open-Source-Betriebssysteme
- MenuetOS - Komplett in x86-32-Bit-Assembler (FASM) geschrieben! (en)
- V2_OS - Ein weiteres Betriebssystem (en)
[Bearbeiten] Microkernel
[Bearbeiten] L4
- L4Hq - L4 Headquarters, Community-Seite für L4-Projekte
- L4Ka - Implementierungen L4Ka::Pistachio und L4Ka::Hazelnut
- Fiasco – Eine freie C++-Implementierung für x86- und ARM-Prozessoren
- UNSW - Portierung auf die Architekturen wp:Alpha und wp:MIPS
- L4Linux - Linux auf dem L4 Microkernel
- DROPS - The Dresden Real-Time Operating System Project
- VFiasco - Verified Fiasco Project
- L3 - Vorgänger-System zu L4, beim TÜV Süd im Einsatz