Resource icon

Ein eigenes kleines Betriebssystem

1 Zum Geleit

Ich werde in diesem Tutorial nicht beschreiben, wie man ein komplettes Betriebssystem programmiert, das mit Windows oder gar Linux gleichziehen kann. Das wäre auch etwas zu viel für diese Seite – und außerdem würde das auch kein Einsteiger-Tutorial mehr bleiben. ;)
Vorkenntnisse in Assembler sind für dieses Tutorial sicher von Vorteil, wenn auch nicht zwingend notwendig. In jedem Fall solltet Ihr aber wissen, wie ein Computer arbeitet.

Um die Beispielcodes aus diesem Tutorial zu benutzen braucht Ihr erst mal ein paar kleine Programme. Die direkten Links kenn ich nicht, aber bei einer Suche mit Google werdet Ihr da mehr als genug finden. Es werden benötigt:

  • Netwide Assembler (NASM)
  • RaWrite oder irgendein anderes Programm, mit dem man Images auf Disketten schreiben kann
  • Eine leere Diskette
  • Gesunder Menschenverstand und Kaffee ;)

Ein nicht unerheblicher Teil des nötigen Codes aus diesem Tutorial wird in Assembler geschrieben. Da man mit Assembler viel machen (und noch mehr kaputt machen) kann, übernehme ich für eventuelle Schäden an Eurem Computer keine Verantwortung. Wer seinen selbstgeschriebenen Bootloader auf die Festplatte schreibt und deswegen nicht mehr an sein richtiges System kommt, der ist selber Schuld!


2 Grundlagen

Um zu verstehen, wie ein Betriebssystem arbeitet, muss man erst mal wissen, was in einem Computer genau passiert. Dazu geh ich jetzt einmal kurz auf das ein, was passiert, wenn man seinen Computer startet. Dabei bezieh ich mich natürlich nur auf die heute verbreiteten 80x86-Prozessoren und deren BIOS-Versionen.
Beim Anschalten eines Computers wird als erstes das BIOS gestartet, welches die Hardware initialisiert. Sobald die wichtigen Komponenten (CPU, RAM, etc.) gefunden wurden, ruft das BIOS das eigentliche Betriebssystem auf. Das spätere Betriebssystem kommuniziert dann mit dem BIOS um Befehle an die CPU zu schicken.
Im BIOS kann eingestellt werden, von welchem Laufwerk zuerst gebootet werden soll. Wenn von dem Laufwerk nicht gebootet werden kann, wird versucht, von dem nächsten Laufwerk zu booten. Und so weiter. Bei den meisten BIOS-Versionen kann man drei Laufwerke in eine Reihenfolge setzen. Für unser Betriebssystem setzen wir das Diskettenlaufwerk nach vorne.
Das soll aber erst mal genug Theorie sein – wir fangen jetzt mal an, das eigentliche Betriebssystem zu programmieren.


3 Ein erster Kernel

Eigentlich wollte ich das eigentliche Betriebssystem ja ganz gerne in C schreiben, aber da die Header-Dateien jeweils an ein bestimmtes Betriebssystem gebunden sind, können wir in unserem Kernel keine Funktionen einbinden. Wir schreiben unseren Kernel also mit Assembler.
Der "Kernel" kann zwar eigentlich nur eine Meldung anzeigen und den Computer neu starten, aber das ist auch schon etwas. Der Code für unser ganzes Betriebssystem sieht folgendermaßen aus:
Code:
mov ax, 1000h
mov ds, ax
mov es, ax

start:               ; Hier fängt unser eigentliches "Betriebssystem" an
mov si, nachricht    ; Wir zeigen einfach nur einen String an
call schreiben       ; "schreiben" gibt den String am Bildschirm aus

call lesen           ; "lesen" wartet bis eine Taste gedrückt wurde
jmp reset            ; Danach wird die Funktion "reset" aufgerufen
Die Funktionen "schreiben", "lesen" und "reset" müssen wir allerdings noch selber schreiben. Und die Variable "nachricht" muss ja auch irgendwo stehen. Also muss hinter den Code von oben noch folgendes:
Code:
nachricht db "Eine Taste drücken, um neu zu starten...",13,10,0
Diese Zeile speichert einen String in der Variable "nachricht", die im eigentlichen Programm benutzt wird. Um den Inhalt der Variable auch tatsächlich auf dem Monitor auszugeben, definieren wir die Funktion "schreiben":
Code:
schreiben:
lodsb
or al, al
jz short schreiben_d
mov ah, 0x0E
mov bx, 0x0007
int 0x10
jmp schreiben

schreiben_d:
retn
Dann natürlich noch eine Funktion, die einen Tastendruck abfängt:
Code:
lesen:
mov ah, 0
int 016h
ret
Und zum Schluss schicken wir noch einen Befehl zum neu starten an den Prozessor:
Code:
reset:
db 0Eah
dw 0000h
dw 0FFFFh
Diesen Code speichern wir irgendwo und nennen die Datei kernel.asm. Diese Datei kompilieren wir nicht zu einer normalen ausführbaren Datei, sondern nur zu einer rohen Binärdatei. Der Aufruf für NASM ist dabei wie folgt:
Code:
nasm –f bin –o kernel.bin kernel.asm


4 Ein Bootmanager

Die alles entscheidende Frage, die jetzt aufkommen dürfte, ist sicher "Wie kann ich meinen Kernel jetzt booten?". Die Antwort darauf lautet zwar nicht 42, aber dafür 512. ;)
Im zweiten Teil hab ich schon erklärt, dass das BIOS von einem bestimmten Datenträger bootet, und das führe ich jetzt weiter aus:
Die Diskette (und überhaupt jeder andere Datenträger auch) auf dem unser Betriebssystem liegt, ist in Sektoren unterteilt. Jeder Sektor ist genau 512 Bytes groß. Wenn das BIOS auf dem ersten Sektor eines Datenträgers eine 512 Bytes große Binärdatei findet, die mit 0x055AAh aufhört, dann stellt diese Datei den Bootsektor dar und wird vom BIOS in die Speicheradresse 0x7C00 geladen.

Mit anderen Worten: Wir brauchen ein 512 Bytes großes Programm, das unseren Kernel aufruft und im ersten Sektor der Diskette liegt. Und dieses Programm schreiben wir uns jetzt.
Als erstes legen wir fest, dass das Programm in der Speicheradresse 0x7C00 startet:
Code:
org 0x7C00
Danach startet der eigentliche Bootloader. Zuerst basteln wir uns einen Stack, dessen Adresse wir auf 0x9000 legen. Den Stackpointer setzen wir dabei auf 0. Während wir unseren Stack zusammenbauen, dürfen wir KEINE Interrupts verwenden!
Code:
start: 
cli                  ; Keine Interrupts verwenden!
mov ax, 0x9000       ; Adresse des Stack speichern
mov ss, ax           ; Stackadresse festlegen
mov sp, 0            ; Stackpointer auf 0 setzen
sti                  ; Jetzt lassen wir wieder Interrupts zu
Wenn wir unseren Stack haben, speichern wir das Laufwerk, von dem aus gebootet worden ist...
Code:
mov [bootdriv], dl
Und jetzt rufen wir die Funktion auf, die unseren Kernel lädt...
Code:
call load
Wenn unsere "Shell" geladen worden ist, springen wir dort hin...
Code:
mov ax, 0x1000       ; 0x1000 ist die Speicheradresse unserer Shell
mov es, ax
mov ds, ax
push ax
mov ax, 0
push ax
retf
Dann definieren wir noch ein paar Funktionen und Variablen...
Code:
bootdriv db 0         ; Das Bootlaufwerk  
loadmsg db "Lade VitaXia...",13,10,0

; Mit dieser Funktion geben wir einen String aus
putstr:
lodsb
or al,al
jz short putstrd
mov ah,0x0E
mov bx,0x0007
int 0x10
jmp putstr
putstrd:
retn 

; Mit dieser Funktion laden wir unsere Shell vom Bootlaufwerk
load:
push ds
mov ax, 0
mov dl, [bootdriv]
int 13h
pop ds
jc load

load1:
mov ax,0x1000
mov es,ax
mov bx, 0
mov ah, 2
mov al, 5
mov cx, 2
mov dx, 0
int 13h
jc load1
mov si,loadmsg 
call putstr
retn
Und ganz zum Schluss sorgen wir dafür, dass unser Bootsektor nicht größer ist als 512 Byte und am Ende den Wert 0x0AA55h steht.
Code:
times 512-($-$$)-2 db 0
dw 0AA55h
Diesen Assembler-Code nennen wir boot.asm und speichern wir im gleichen Verzeichnis wie den Code unseres Kernels. Dann assemblieren die Datei mit NASM ebenfalls zu einer rohen Binärdatei:
Code:
nasm –f bin –o boot.bin boot.asm


5 Und jetzt?

Jetzt, wo wir einen "Kernel" und einen Bootloader haben, wollen wir das natürlich auch ausprobieren. Dazu kopieren wir erst mal beide Binärdateien zusammen in eine Image-Datei:
Code:
copy boot.bin+kernel.bin vitaxia.img
Als letzten Schritt schreiben wir dieses Image mit RaWrite auf eine Diskette. Alle Daten auf der Diskette gehen dabei verloren und formatiert ist die Diskette dann auch nicht mehr!
Diese Diskette legen wir ein und starten den Computer neu. Danach müsste das eigene Betriebssystem gestartet werden.

Das ganze ist natürlich nur ein kleines Beispiel, wie man ein Betriebssystem programmieren kann. Wenn man den Kernel erst mal gebootet hat, kann man später auch mit C oder C++ weiter programmieren. Das Problem ist einfach nur, dass die Funktionen printf() und scanf() nicht Bestandteil der Sprache selber sind, sondern in der Headerdatei stdio.h stehen. Und in dieser sind die Funktionen abhängig von einem bestimmten Betriebssystem.
Man wird also erst mal nicht um Assembler herum kommen. Zumal damit auch ein schnellerer und besserer Zugriff auf die Hardware möglich ist.

Ich werde später noch genaueres darüber schreiben, wie man die richtigen Aufgaben eines Betriebssystems realisiert. Aber bis dahin wird das hier als Einstieg erst mal reichen müssen. Kritik bitte per PM an mich. Ansonsten viel Spass damit. :)
  • Gefällt mir
Reaktionen: EdwardBlack
Autor
Dario Linsky
Aufrufe
5.007
First release
Last update
Bewertung
0,00 Stern(e) 0 Bewertungen