Den Kernel von der Diskette/CD/HDD in den RAM laden
Zu Beginn habe ich schon erklärt, dass der Bootloader immer exakt 512 Bytes groß sein muss, d.h. er kann auch nicht größer sein. Also kann nicht der gesamte Betriebssystemkern im Bootloader liegen.
Der Kern muss also “händisch” vom Speichermedium in den Arbeitsspeicher geladen werden. Genau das werde ich in diesem Artikel erklären.
Eine gute Sache ist zunächst einmal, dass das BIOS bereits speichert, von welchem Medium es den Bootloader geladen hat. Sollte der Kernel auf dem gleichen Medium liegen (was er in unseren einfachen Beispielen immer tun wird), kann man diese Information gleich weiternutzen. Wie wir in Ralf Brown’s Interrupt List (RBIL) sehen, muss die Laufwerksnummer im Register DL abgelegt werden. Praktischerweise legt das BIOS die Information auch schon dort ab. Falls man DL oder DX jedoch überschreiben wird, muss man es sich vorher in den Speicher sichern.
Es geht wieder mit dem üblichen Aufbau los, indem alle Segmente initialisiert werden. Daraufhin folgt dann die Sicherung des DL-Registers mit der Laufwerksnummer.
[BITS 16]
[ORG 0x7C00]
xor ax, ax ; setze ax auf 0 = erstes Segment
mov ss, ax
mov ds, ax
mov es, ax
mov [disk_number], dl
; diese Zeilen stehen am Ende der Datei
[disk_number] db 0
times 510-($-$$) db 0
dw 0xaa55
Zurücksetzen des Laufwerks
Bevor man lesend auf das Laufwerk zugreift, setzt man es im Bootloader noch einmal auf die Ausgangsstellung zurück. Dies führt zu einer Rekalibrierung des Schreiblesekopfes (“Forces controller to recalibrate drive heads (seek to track 0)”, RBIL Int 13, AH=00h).
Wer den beiden Links zur RBIL gefolgt ist, hat vielleicht schon gesehen, dass Laufwerkszugriffe über die Unterbrechung 13h
ablaufen.
Für das Zurücksetzen muss dabei AH auf 0 gesetzt sein und in DL die breits erwähnte Laufwerksnummer stehen. Da sie bisher nicht verändert wurden, braucht man sie auch nicht nochmal aus dem Speicher lesen.
Das Zurücksetzen des Laufwerks kann auch fehlschlagen, sodass wir es notfalls nochmal versuchen. Bei einem Fehler wird in AH ein Wert ungleich 0 zurückgegeben und das Carry-Flag gesetzt. In diesem Beispiel gehe ich zunächst mal davon aus, dass kein defektes Laufwerk vorkommt, sodass das Zurücksetzen irgendwann klappt und keine Endlosschleife auftritt.
Hinter dem mov-Befehl zum Sichern der Laufwerksnummer fügt man also noch an:
disk_reset:
xor ah, ah ; ah=0, reset drive head
int 13h
jc disk_reset
Der letzte Befehl springt im Fehlerfall (Carry-Flag gesetzt) wieder zum Anfang zurück und es wird versucht, das Laufwerk noch einmal zurückzusetzen.
Lesen vom Laufwerk
Das Lesen vom Laufwerk ist auch nicht wesentlich schwieriger, es müssen nur mehr Werte gesetzt werden.
-
AH: auf 02h zum Lesen
-
AL: Anzahl der Sektoren, die man lesen will
-
CH: untere Bits der Zylinder-Nummer
-
CL: Sektor-Nummer und obere Bits der Zylinder-Nummer
-
DH: Kopf-Nummer
-
DL: Laufwerksnummer
-
ES:BX: Zieladresse, wohin man kopieren will
Die meisten Daten können bei einem einfachen Beispiel mit einer Diskette auf 0 gesetzt werden, aber für eine Festplattenadressierung sind sie wichtig. Wer mehr wissen will, kann sich über die CHS-Adressierung informieren.
Wichtig für dieses Beispiel sind AL, CL, DL und ES:BX. DL wurde immer noch nicht verändert, muss also nicht vom Speicher gelesen werden. Unter dem Code zum Zurücksetzen des Laufwerks kommt hinzu:
disk_read:
mov ah, 0x02 ; mit Interrupt 13h: Lesen!
mov al, 10 ; Lies 10 Sektoren
mov cl, 2 ; ab Sektor 2
mov bx, 0x8000 ; Speichere Daten an Speicherstelle 0x8000
mov ch, 0
mov dh, 0
int 13h
jc disk_read
jmp 0x0000:0x8000 ; Springe zum neuen Programm, Hurra!
Damit ist der Bootloader auch schon vollständig und man kann größere Programme schreiben, die nachgeladen und anschließend ausgeführt werden. Da der Zwischenspeicher von DL nie gebraucht wurde, kann er auch gelöscht werden. Man spart sich somit einen Hauptspeicherzugriff.
Der komplette Code nochmal:
[BITS 16]
[ORG 0x7C00]
xor ax, ax ; setze ax auf 0 = erstes Segment
mov ss, ax
mov ds, ax
mov es, ax
disk_reset:
xor ah, ah ; ah=0, reset drive head
int 13h
jc disk_reset
disk_read:
mov ah, 0x02 ; mit Interrupt 13h: Lesen!
mov al, 10 ; Lies 10 Sektoren
mov cl, 2 ; ab Sektor 2
mov bx, 0x8000 ; Speichere Daten an Speicherstelle 0x8000
mov ch, 0
mov dh, 0
int 13h
jc disk_read
jmp 0x0000:0x8000 ; Springe zum neuen Programm, Hurra!
times 510-($-$$) db 0
dw 0xaa55
Das nachgeladene Programm
Natürlich wird jetzt noch ein Programm benötigt, das dann auch an Stelle 0x8000 geladen werden soll. Hierzu dient vorerst mal der alte Bootloader in abgewandelter Form. Es soll also wieder ein Text ausgegeben werden.
Geändert wurde die Codestelle zu Beginn, weil dieser Code an 0x8000 platziert wird. Außerdem muss der nachgeladene Code nicht mehr exakt 512 Bytes groß sein und mit einer bestimmten Markierung enden.
[BITS 16]
[ORG 0x8000]
xor ax, ax ; setze ax auf 0 = erstes Segment
mov ss, ax
mov ds, ax
mov es, ax
mov si, welcome_msg
call print_string
jmp $
print_string:
mov ah, 0eh ; Ausgabe bei Interrupt 10h
.next_char:
lodsb ; liest ein Byte aus und speichert es in AL
cmp al, 0 ; wurde das Nullbyte gelesen?
je .done ; falls ja: Fertig
int 10h ; ansonsten: gib aktuelles Zeichen in AL aus
jmp .next_char ; und mache beim nächsten Zeichen weiter
.done:
ret
welcome_msg db 'Hello World!', 0
Wichtig ist hingegen, dass die Diskette auch tatsächlich so viele Sektoren enthält, wie nachgeladen werden sollen, sonst kann es zu einem Fehler kommen und das BIOS streiken.
Daher erstellt man am besten gleich eine komplett leere Diskette mit dd
. Die beiden Images werden dann einfach einandergehängt (sodass der Bootloader später im ersten Sektor steht und der neue Code ab dem zweiten Sektor folgt) und anschließend in die “Diskette” kopiert.