Donnerstag, 9. August 2007

Bootsektor schreiben mit GNU Assembler

Wird mal wieder Zeit die Regler auf Geek zu stellen ;) (man merke an das ich gerade japanischen Hardcoretechno höre dessen Text wie folgt geht : uauauauauaä uauauauauä.)

Ok zum Thema. Der GNU Assembler ist ja nicht jedermanns bester Freund da er die AT&T Syntax verwendet und darum greifen viele lieber zu NASM. Aber ich persönlich bevorzuge auf jeden Fall die GNU Lösung da sich das ganze sehr schön in die GNU Compiler Collection einbindet. (Für alle die es noch nicht wissen sollten, gcc -s foo.c wandelt den C-Code in foo.s, den Assembler Code um ;) ). Aber über Geschmack lässt sich ja bekanntlich nicht streiten, ich werde hier auf jeden Fall den GNU As verwenden.

Das Objekt unserer Begierde ist heute also ein Bootsektor, also die ersten 512 Byte unseres Mediums. Hier ist die Partitionstabelle und ein kleines Programm welches unser Betriebssystem starten soll angesiedelt, dazu kommt noch eine Magic Number die dem BIOS sagt das es sich hier auch wirklich um einen Bootsektor handelt.

Die Partitionstabelle lassen wir jetzt einfach mal außen vor und befassen uns damit wie wir ein Programm in den Bootsektor packen das dann auch wirklich läuft. Auch wenn ich jetzt gesteinigt werde, wird das ganze in ein "Hello World" Programm ausarten ;). Da wir uns im Real Mode der CPU befinden und weniger als 512 Bytes zur Verfügung haben lohnt sich auch nicht wirklich mehr. Also erst mal zum Quellcode von bootsektor.s :

.code16
.text
.globl entry
entry:
mov %cs,%ax
mov %ax,%ds
mov %ax,%ss
mov $0x400,%sp

mov $.LC0,%si
call print
cli
hlt
.LC0:
.string "Hello World"
.globl print
print:
lodsb
or %al,%al
jz .endprint
mov $0x0e,%ah
mov $0x0007,%bx
int $0x10
jmp print
.endprint:
ret
.section .sig
.word 0xaa55

Wir richten also alle unsere Segment Selektoren (CS,DS und SS) und den Stack ein und packen die Adresse unseres Strings in das SI Register. Danach rufen wir print auf (call ist das was uns in C Funktionen aufrufen lässt) und laden das erste Byte aus SI in das Low Byte von AX (AL). Ist dieses gleich 0 wird das Z-Flag gesetzt (wegen or %al,%al) und jz springt nach endprint, wenn nicht wird der Aufruf des Interrupts 0x10 vorbereitet. Dieser Interrupt hat mehrere Funktionen wir nutzen in diesem Beispiel die Funktion 0x0e die für das ausgeben eines Zeichens verwendet wird. Der Funktionswert muss im High Byte des AX Registers gespeichert werden (AH). Im Low Byte von BX speichern wir die Farbe unseres Zeichens (0x07 für weiß). Jetzt wird der Interrupt 0x10 aufgerufen, der kümmert sich auch darum das SI weitergeschoben wird zum nächsten Zeichen.

Zu guter letzt definieren wir ein eigenes Segment welches wir "sig" (soll für Signatur stehen) nennen. Das ist unsere Magic Number für das BIOS, diese muss ganz ans Ende unseres Bootsektors.

Ok, der Quellcode ist fertig. Nun wandeln wir unseren Code erst mal in ein Object File um welches wir danach linken. Dies geschieht wie folgt :

gcc -c bootsektor.s

Jetzt müssen wir nur noch linken damit die Adressen in unserem Bootsektor auch passen. Das BIOS lädt den Sektor in den Speicher an die Adresse 0x7c00 das müssen wir unserem Programm mitteilen damit es überhaupt Lauffähig ist. Außerdem muss unsere "Magic Number" auch an die passende Stelle geschoben werden. Das machen wir mit einem Linker Script (link.ld) :


OUTPUT_FORMAT(binary)
ENTRY(entry)
SECTIONS
{
.boot_sector 0x7c00 :
{
*(.text)
*(.data)
. = 510; *(.sig)
}
.bss :
{
*(.bss)
}
}

Mit "ld -T link.ld -o bootsektor bootsektor.o" wird unser Object File in ein 512 Byte großes Binary umgewandelt umgewandelt den man mit "dd if=bootsektor of=/dev/fd0" auf eine Diskette schreiben kann oder (viel besser, denn wer hat heute noch Disketten ;) ) mit qemu -fda bootsektor -boot a mit qemu ausführen kann. Um das Linker Skript besser zu verstehen sollte man sich einfach mal das GNU Linker Manual anschauen. ;)

Bootsektoren schreiben ist eigentlich ziemlich sinnlos, aber es ist witzig das ganze mal gemacht zu haben da es einem interessante Einblicke vermittelt. Wenn man wirklich ein bootfähiges Programm erstellen will (also einen Kernel oder ähnliches) empfiehlt sich die Verwendung von GRUB und einer Multiboot-ELF. Dazu aber vielleicht irgendwann später ;)