Nichts hat in der letzten Zeit unsere Welt bezüglich ihrer Geschwindigkeit so verändert wie der Computer bzw. der Mikrochip und hier explizit der x86 Prozessor, welcher die heutigen einfachen PCs ermöglichte und auch die Grundlage der Befehlssätze (z. T. bis heute) liefert. Bedenkt man, dass dieser erst 1978 die Welt erblickte und betrachtet man die Entwicklung seither, ist diese rasant. Denkt man heute an einen Commodore zurück oder auch an ein 56k Modem mit dem charakteristischen Einwahltönen, so scheint dies heutzutage wie aus lang vergangenen Tagen.

Wie aber sieht ein Programmcode aus der Sicht eines Mikrochips aus? Diese Frage stellte ein Freund und tatsächlich schreibt man seine Programme und weiß, dass der Interpreter diese übersetzt, aber ein klares Bild, wie denn ein Programm in binärer (bzw. hexadezimaler) Schreibweise ausschaut, hat man nicht vor Augen.

Die Idee entstand, einen der ersten X86 Prozessoren zu öffnen als auch ein einfaches “Betriebssystem” zu schreiben, welches tatsächlich einen X86 Prozessor initialisiert und die berühmten Worte der Programmierwelt “Hello (new) World!” ausgibt.

Der Code verwendet Multiboot um sich weiteren Code für den Bootloader zu sparen. Wer sich weiter damit auseinandersetzten möchte findet mit dem “bochs” Emulator eine gute Platform um den Code, nach der Kompilierung, auszuführen.

Der Code

start.s

Dieser Code initialisiert den Prozessor. Hierbei werden einige Konfigurationsregister geschrieben z.B. einfacher Grafik Modus und schließlich die Hauptfunktion des “Betriebssystems” aufgerufen.

.set MB_MAGIC,              0x1BADB002
.set MB_FLAG_PAGE_ALIGN,    1 << 0
.set MB_FLAG_MEMORY_INFO,   1 << 1
.set MB_FLAG_GRAPHICS,      1 << 2
.set MB_FLAGS,              MB_FLAG_PAGE_ALIGN | MB_FLAG_MEMORY_INFO | MB_FLAG_GRAPHICS
.set MB_CHECKSUM,           -(MB_MAGIC + MB_FLAGS)

.section .multiboot
.align 4
/* Multiboot section */
.long MB_MAGIC
.long MB_FLAGS
.long MB_CHECKSUM
.long 0x00000000 /* header_addr */
.long 0x00000000 /* load_addr */
.long 0x00000000 /* load_end_addr */
.long 0x00000000 /* bss_end_addr */
.long 0x00000000 /* entry_addr */

/* Request linear graphics mode */
.long 0x00000000
.long 0
.long 0
.long 32

.section .stack, "aw", @nobits
stack_bottom:
.skip 32768 /* 32KiB */
stack_top:

.section .text

.global start
.type start, @function

.extern init

start:
    mov $stack_top, %esp

    and $-16, %esp

    pushl %esp
    pushl %eax 
    pushl %ebx 

    cli
    call init

    cli
hang:
    hlt
    jmp hang

init.c

Diese Funktion ist dafür zuständig den Text “Hello new World” auszugeben. Sie wird über die “call init” der Datei start.s aufgerufen und stellt den Einsprungpunkt für das eigentliche “Betriebssystem” dar.

void init(void)
{
    init_gdt();
    char* video = (char*) 0xb8000;
    const char hw[] = "Hello new World!";
    int i;
    
    for (i = 0; hw[i] != '\0'; i++) {

        // Zeichen i in den Videospeicher kopieren
        video[i * 2] = hw[i];

        // 0x07 = Hellgrau auf Schwarz
        video[i * 2 + 1] = 0x07;
    }
}

kernel.ld

Dies ist keine Programmiersprache im eigentlichen Sinne, sondern eine Anweisung für den Compiler für die Positionen der einzelnen Programmteile im Speicher.

ENTRY(_start)

SECTIONS
{
    . = 0x100000;

    .text : {
        *(multiboot)
        *(.text)
    }
    .data ALIGN(4096) : {
        *(.data)
    }
    .rodata ALIGN(4096) : {
        *(.rodata)
    }
    .bss ALIGN(4096) : {
        *(.bss)
    }
}

Makefile

Die Makefile ist schließlich das Kochrezept für den Compiler.

SRCS = $(shell find -name '*.[cS]')
OBJS = $(addsuffix .o,$(basename $(SRCS)))

CC = gcc
LD = ld

ASFLAGS = -m32
CFLAGS = -m32 -Wall -g -fno-stack-protector -nostdinc
LDFLAGS = -melf_i386 -Tkernel.ld

kernel: $(OBJS)
	$(LD) $(LDFLAGS) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $^

%.o: %.S
	$(CC) $(ASFLAGS) -c -o $@ $^

clean:
	rm $(OBJS)

.PHONY: clean

Die Übersetzung

Wird nun der obige Programmcode kompiliert erhält man unten stehenden Zahlen / Buchstaben Salat. Dies ist die hexadezimale Darstellung des Maschinencodes. Die binäre Darstellung wäre Seitenlang. Eine Umrechnung von hexadezimaler in binär ist aber natürlich ohne Probleme und weitere Kompilierung möglich, da es nur eine unterschiedliche Darstellung ist.

Jeder Zeile gibt eine Speicheradresse mit einer Offset (Spalte) an. Somit ist dies eine Repräsentation des Speichers der in die CPU geladen wird. Natürlich ist dies deutlich komplexer. Ebenfalls sieht man, dass bis zur Adresse 00001000 leere Werte vorherrschen und erst danach der eigentliche Programmcode beginnt. Wie es auch in der kernel.ld definiert wurde.

Das “Betriebssystem” in hexadezimaler Schreibweise

Leave A Comment

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert