Um das Betriebssystem tOSh herunterzuladen, kannst du dies über den Bereich Resources dieser Website tun.

Teile

Diese Artikelserie wird aus folgenden Teilen bestehen:

Turbo Debugger - Not Enough MemoryTurbo Debugger - Not Enough Memory

Stage I - Vom Real Mode zum Protected Mode

Wir befinden uns im Real Mode. Das Letzte, was wir gemacht haben, war, zu einer anderen Speicheradresse zu springen, wo wir einen weiteren Teil unseres Binärcodes hätten kopieren können, den wir stage1 nennen werden. Diesen haben wir aus einem Sektor der Floppy genommen, auf der wir den gesamten Bootloader kompilieren und speichern, wie wir im ersten Teil dieser Serie gesehen haben, und jetzt ist es an der Zeit zu sehen, was wir mit der Kontrolle über unsere CPU machen. Das erinnert mich an einige gaucheske Sextillen unseres geliebten Diego Silicio, Gaucho der Kathode:

Atendiendo la obligada,
no se haga el distraído
no se olvide la pavada,
ni se pierda el protegido,
que es cuestión de buen formato,
¡memorí laiaut bendito!

Übersetzung:

Was zu tun ist, sei bedacht,
tu nicht, als wärst du abgelenkt,
vergiss den Kleinkram nicht,
und verlier den Protected Mode nicht aus dem Blick,
denn es geht um gutes Format,
memorí laiaut – Gott sei’s gedankt!

Erklärung: Das ist eine Sextilla gauchesca, also ein sechszeiliger Vers im Stil der argentinischen Gaucho-Dichtung. Der Witz entsteht dadurch, dass technische Begriffe in den Rhythmus und Reim einer Gaucho-Strophe gezwängt werden. Besonders „memorí laiaut“ ist eine absichtlich gauchesk verfremdete Aussprache von „memory layout“. Dieser Wortwitz lässt sich im Deutschen kaum erhalten, deshalb bleibt das Original neben der Übersetzung stehen.

Um besser zu verstehen, was wir machen werden und wie wir die CPU so konfigurieren, dass sie den Mode wechselt, müssen wir unser Memory Layout (memorí laiaut) verstehen und wissen, wo wir fast willkürlich entscheiden –und das ist sehr, sehr wichtig, der "fast" willkürliche Teil–, wo wir alles in den Speicher kopieren werden: genau dieses stage1.bin, das wir von der Diskette in den Speicher laden und zu dem wir springen, woraufhin wir mit der letzten Instruktion des Bootloaders loslaufen: jmp 0x9000.

Zuerst muss man erklären, dass das Memory Layout von x86-Rechnern bereits definiert ist und dass es Speicheradressen gibt, die Overlays mit Komponenten und Peripheriegeräten haben, über die wir mit ihnen kommunizieren. Das bedeutet, dass wir nicht einfach irgendwelche Speicherbereiche verwenden können, weil sie von anderen Geräten und von der CPU für die Kommunikation untereinander verwendet werden, vom BIOS bei 0040:0000h und auch für die Schnittstelle zum Benutzer. Als Beispiel nehmen wir die Tabelle aus dem Dokument Physical Memory Layout of the PC und mischen sie mit unserem Layout. Die Idee ist zunächst, zu vermeiden, diese Speicherbereiche zu überschreiben, die uns nicht gehören, und andererseits eine Lücke zu finden, in die wir unseren Code für stage1.bin und später unser kernel.bin in den Speicher schreiben können. Wir sollten genug Platz für alles haben, ohne irgendetwas zu überschreiben.

linearer Speicherbereich Adressbereich im Real Mode Speichertyp Verwendung
0- 3FF 0000:0000-0000:03FF RAM Interrupt Vector Table (IVT) des Real Mode
400- 4FF 0040:0000-0040:00FF RAM BIOS Data Area (BDA)
500- 9FBFF 0050:0000-9000:FBFF RAM freier konventioneller Speicher (unterhalb von 1 MB)
0x9000 + (512bytes * 4 Sektoren) 0900:0000 – 0900:07FF RAM (stage1.bin) hier setzen wir uns rein, unterhalb des ersten Megabytes Speicher
0x10000 + (512bytes * n Sektoren) SCHEISS DRAUF! RAM (kernel.bin) bis hierhin sollten wir bereits im [Protected Mode] sein
0x80000 SCHEISS DRAUF! RAM (stack) Der Stack, der vom Kernel und unseren Programmen verwendet wird
9FC00- 9FFFF 9000:FC00-9000:FFFF RAM Extended BIOS Data Area (EBDA)
A0000- BFFFF A000:0000-B000:FFFF Video RAM VGA-Framebuffer
C0000- C7FFF C000:0000-C000:7FFF ROM Video-BIOS (typischerweise 32K groß)
C8000- EFFFF C800:0000-E000:FFFF NICHTS  
F0000- FFFFF F000:0000-F000:FFFF ROM Motherboard-BIOS (typischerweise 64K groß)
100000- FEBFFFFF   RAM freier erweiterter Speicher (ab 1 MB)
FEC00000- FFFFFFFF verschiedene   Motherboard-BIOS, PnP-NVRAM, ACPI usw.

Unter Berücksichtigung all dessen können wir uns jetzt ein wenig Code ansehen, stage1.asm:

; ----------------------------------------------------
; tOSh Operating System, Huh?                 (c) 2019
; stage1.asm                                  by toshi   
; $ nasm -D KERNEL_FIRST_SECTOR=$KERNEL_FIRST_SECTOR \
;   -D KERNEL_SECTORS=$kernel_sectors \
;   -f bin -l stage1.lst stage1.asm -o stage1.bin
; ----------------------------------------------------

bits 16
org 0x9000                  ; we copied sector 2+ here!
[map symbols stage1.map]    ; create a stage1.map file for offsets checking

KERNEL_OFFSET equ 0x10000
STACK_ADDRESS equ 0x80000

Alles, was assembliert wird, wird in 16 bits relativ zu org 0x9000 assembliert. Zusätzlich erstellen wir eine Symbol-Map, mit der wir die Offsets der Funktionen sehen können, falls wir sie während der Entwicklung benötigen, und zwar mit der Direktive map symbols.

Von den verschiedenen Vorteilen finde ich am nützlichsten, dass wir die equ, Funktionsnamen und ihre Speicherpositionen nach dem Assemblieren im Blick behalten können, selbst wenn stage1.asm mehrere %includes enthält.

map symbols stage1.map erzeugt eine Datei, die ungefähr so aussieht:

- NASM Map file ---------------------------------------------------------------

Source file:  stage1.asm
Output file:  stage1.bin

-- Symbols --------------------------------------------------------------------

---- No Section ---------------------------------------------------------------

Value     Name
00010000  KERNEL_OFFSET
00080000  STACK_ADDRESS
00000008  CODE_SEGMENT
00000010  DATA_SEGMENT
000B8000  VRAM


---- Section .text ------------------------------------------------------------

Real              Virtual           Name
            9023              9023  prepare_protected_mode
            903B              903B  load_kernel
            9052              9052  load_kernel.next_sector
            9070              9070  load_kernel.destination_ok
            908A              908A  load_kernel.done
            908F              908F  load_error
            909A              909A  kernel_sectors_remaining
            909C              909C  error_hang
            90A7              90A7  init_protected_mode
            90E7              90E7  protected_mode_msg
            9108              9108  kernel_load_fail_msg
            9121              9121  kernel_load_msg
            9148              9148  gdt_start
            9150              9150  gdt_code_segment
            9158              9158  gdt_data_segment
            9160              9160  gdt_descriptor
            9166              9166  gdt_end
            9166              9166  enable_A20
            9196              9196  a20wait
            919D              919D  a20wait2
            91A4              91A4  asm_print_protected
            91AA              91AA  asm_print_protected.loop
            91BD              91BD  asm_print_protected_end
            91BF              91BF  asm_print_hex
            91D0              91D0  asm_print_hex.loop
            91E2              91E2  asm_print_hex.digit
            91E5              91E5  asm_print_hex.write

Nachdem wir den Hintergrund auf Grau geändert haben -das kannst du im Quellcode des OS im Abschnitt Resources sehen - machen wir einen call auf load_kernel, der im Wesentlichen das Kernel-Binary (an dem noch gearbeitet wird) an die willkürlich gewählte Speicheradresse 0x10000 kopiert, die weiter oben durch KERNEL_OFFSET equ 0x10000 definiert ist.

load_kernel:
    ; ---------------------------------------------------------
    ; Load KERNEL_SECTORS sectors from floppy into 0x10000
    ;
    ; Floppy geometry:
    ;   80 cylinders
    ;   2 heads
    ;   18 sectors per track
    ;
    ; Kernel starts at KERNEL_FIRST_SECTOR
    ; check build.sh for sector calculations
    ; ---------------------------------------------------------

    push ax
    push bx
    push cx
    push dx

    ; Destination = 0x10000 + n bytes, ES = 0x1000, BX = copy n bytes offset
    xor bx, bx
    mov ax, 0x1000
    mov es, ax

Eine Sache, die man in dieser Auflistung hervorheben sollte (phuá, "Auflistung" hab ich gesagt, wir sind zurück in den 80ern!), ist, wie vorsichtig man mit etwas sein muss, das ich "Segmentarithmetik" nennen werde, weil ich nicht so recht weiß, wie ich es sonst nennen soll. Jedes Mal, wenn du im 16-Bit-Modus etwas in den Speicher schreiben musst, musst du immer berücksichtigen, in welchem Segment du arbeiten wirst und wie die Instruktionen dieses verwenden oder eben nicht, und außerdem das Verhältnis zwischen der "logischen" und der physischen Speicheradresse, wobei du immer die folgende Formel im Hinterkopf behalten musst:

Physische Adresse = (Segment * 16) + Offset

Das alles hat damit zu tun, wie man mehr als 64 Kilobyte adressieren kann (also 2^16 sind 16 Bit), wenn man physisch ein, zwei oder mehr Megabyte Speicher hat. Nun, die Jungs von Intel haben das so gelöst, und das bringt ziemlich viele Kopfschmerzen mit sich, weil diese Arithmetik dazu führt, dass Adressen nicht eindeutig sind und sich Bereiche überlappen. Ich kopiere den Kernel auf diese Weise in den Speicher und springe danach in den [Protected Mode], wo dieses Problem verschwindet. Und als ich gerade dabei war, ein Binary in den Speicher zu kopieren, habe ich die BIOS-Funktionen verwendet, um den Kernel von der Diskette in den Speicher zu kopieren, weil ich den 286 unterstützen wollte, der einen ziemlich beschissenen Protected Mode hat, und dann wurde es kompliziert und es ist so geblieben.

load_kernel funktioniert, und als ich es endlich geschafft hatte, den Kernel zu kopieren und das Ganze zum Laufen zu bringen, wollte ich es nicht mehr anfassen.

Wenn du ungefähr verstehst, was ich hier genau mache:

    xor bx, bx
    mov ax, 0x1000
    mov es, ax

Wo ich die Kopie vorbereite, wirst du sehen, dass ich mich nicht bei 0x10000 aufhalte, was eigentlich logisch wäre, sondern bei <--> Segment:Offset <--> 0x1000:0x0000 <--> ES:BX. Ich übertreibe hier absichtlich mit der Notation, damit ganz klar "AUFFÄLLT", was ich sagen will, denn das ist GRUNDLEGEND, um zu verstehen, warum Dinge im Speicher niemals dort landen, wo man sie erwartet.

Also, aufgrund von allem, was wir gesagt haben:

Physischer Speicher: 0x10000 (fünf Nullen) <—> 1000:0000 (vier Nullen, vier Nullen)

Und warum kommt das Segment in ES? Wenn du INTerrupts aufrufst oder bestimmte Instruktionen verwendest, benutzen diese SEGmente, um zu arbeiten. Das ist Konvention, und die Details stehen in den Intel-Handbüchern und in Ralf Brown’s Interrupt List.

pues le digo, mi ingeniero
que tiene la segmentación
pa' que no falte ocasión
y no se le pase el viaje
a esos bites ni el anclaje
ni una horrible excepción

Übersetzung:

nun sag ich dir, mein Ingenieur
wozu die Segmentierung gut ist
damit keine Gelegenheit verpasst wird
und die Reise nicht an dir vorbeizieht
diesen bites weder ihre Verankerung
noch eine grässliche Ausnahme

Erklärung: Auch das ist eine Sextilla gauchesca. Der Witz entsteht durch den gauchesken Rhythmus und dadurch, dass technische Begriffe in die Dichtung gezwängt werden. Besonders „bites“ ist eine absichtliche gaucheske Verformung von bytes. „anclaje“ setzt das Bild des Verankerns fort: Die bites/bytes sollen nicht nur vorhanden sein, sondern auch richtig „verankert“ bleiben. Der genaue Reim und der Wortwitz lassen sich im Deutschen nicht vollständig erhalten, deshalb bleibt das Original daneben stehen.

Was nun in der Funktion load_kernel folgt, ist eine Standardroutine zum Kopieren von Sektoren von der Diskette in den Speicher:

    ; Current CHS
    xor ch, ch                  ; cylinder = 0
    mov dh, 0                   ; head = 0
    mov cl, KERNEL_FIRST_SECTOR ; sector

    ; Number of sectors remaining
    mov word [kernel_sectors_remaining], KERNEL_SECTORS

.next_sector:

    cmp word [kernel_sectors_remaining], 0
    je .done

    ; BIOS read: exactly ONE sector
    mov ah, 0x02
    mov al, 1
    mov dl, 0                   ; floppy drive A:

    int 0x13
    jc load_error

    ; ---------------------------------------------------------
    ; Advance destination by 512 bytes
    ; ---------------------------------------------------------

    add bx, 512

    ; If BX wrapped around, advance ES by 0x1000
    ; because 0x1000:0000 -> 0x2000:0000
    jnc .destination_ok

    mov ax, es      ; yuck! 
    add ax, 0x1000  ; we are going to leave all this 
    mov es, ax      ; segment:offset madness very soon!

.destination_ok:

    ; One less sector to load
    dec word [kernel_sectors_remaining]

    ; ---------------------------------------------------------
    ; Advance CHS
    ; ---------------------------------------------------------

    inc cl                      ; next sector

    cmp cl, 19                  ; floppy has sectors 1..18
    jb .next_sector

    ; End of track -> next head
    mov cl, 1
    inc dh

    cmp dh, 2                   ; heads 0 and 1
    jb .next_sector

    ; End of cylinder -> next cylinder
    mov dh, 0
    inc ch

    jmp .next_sector


.done:
    pop dx
    pop cx
    pop bx
    pop ax
    ret


load_error:
    ; BIOS sets carry flag (CF) on error
    ; AX contains BIOS error information

    mov ebx, kernel_load_fail_msg
    call asm_print_protected

    jmp error_hang


kernel_sectors_remaining dw 0


error_hang:
        mov ebx, kernel_load_fail_msg
        call asm_print_protected
        jmp $       ; loop forever 
                    ; Instruction Pointer is self (JMP IP)

Protected Mode vorbereiten

Der Real Mode, in dem wir uns befinden, ist irgendein Scheiß aus den 70ern/80ern, und hier sind wir im Jahr 2019 und schreiben immer noch solchen Code, also werden wir die CPU konfigurieren, um vom 16-Bit-Modus in den 32-Bit-Modus zu wechseln.

Dafür müssen wir mehrere Dinge erledigen:

  • Einen Descriptor in der Global Descriptor Table bzw. GDT erstellen
  • Die A20-Busleitung aktivieren
  • Eine Interrupt Descriptor Table IDT erstellen (Update: Das in Assembly zu halten war ein ziemlicher Krampf, deshalb habe ich es in den Kernel in C verschoben; das kommt in einem anderen Artikel)
  • Nach der Konfiguration all dessen über das Control Register cr0 den [Protected Mode] aktivieren
  • Zu 32-Bit-Code springen
; Ok, we now try to switch to protected mode
prepare_protected_mode:
    cli                     ; disable interrupts

    lgdt [gdt_descriptor]   ; load the gdt_descriptor table in gdt.asm
                            ; with the lgdt (load GDT) instruction

    ; Fast A20 Gate 286+ ? 
    ; using this Fast A20 gate resets the computer on 286
    ;in al, 0x92
    ;or al, 2
    ;out 0x92, al
    call enable_A20         ; so we try to use this neat function instead

    ; lidt instruction execution moved to C kernel! 
    ;lidt [idt_descriptor]

    ;xchg bx, bx             ; magic bochs breakpoint

    ; protected mode 286+ ?
    ; we are NOT supporting 286!!!!!111 t_t
    ; tOSh it's 386+ -ONLY-
    ;    lmsw ax        ; pre-cr0 register is the 'msw' register in 286
    ;    or ax, 0x1     ; and this way you enable
    ;    smsw ax        ; protected mode, but cannot make it work,
                        ; don't know what I missed

    mov eax, cr0
    or  eax, 0x1            ; set the 32-bit mode (Protected Mode)...
    mov cr0, eax            ; into cr0

    ; the CODE_SEGMENT is defined in gdt.asm
    jmp CODE_SEGMENT:init_protected_mode        ; far jump! 

Was ich hier erklären werde, ist eine übertriebene Superultra-Vereinfachung dessen, was man eigentlich aus mindestens zwei der sechs Bände der Intel-Handbücher lesen müsste, in denen alles, was man zum Verständnis der CPU-Konfiguration braucht, bis ins kleinste Detail erklärt wird.

GDT

Die GDT ist eine Datenstruktur, die von Intel-Prozessoren (32/64 Bit) verwendet wird, um Speichersegmente und Berechtigungen zu definieren, sodass in jedem dieser Segmente in allen möglichen Kombinationen geschrieben, gelesen und ausgeführt werden kann. Sie ist ein Array aus 8-Byte-Einträgen, das in [tOSh] ungefähr so aussieht:

; https://wiki.osdev.org/GDT_Tutorial
; https://wiki.osdev.org/Global_Descriptor_Table
bits 32
db 'GDT'    ; GDT mark for bin
gdt_start:
    ; the first entry of the GDT must start with
    ; 8 null bytes

    dd 0x0
    dd 0x0

gdt_code_segment:
    ; this is the entry for the kernel address space
    dw 0xFFFF   ; limit 0:15 - 16 bits - 2 bytes
                ; (all will be pages of 4KiB, so take this into account)
    dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
    db 0x00     ; base  16:23 - 8 bits - 1 byte
                ; Base address will be 0
    db 10011010b ; access byte - 8 bits - 1 byte
                 ; present bit: 1
                 ; privilege, 2 bits, 0 = kernel space, 3 = userspace
                 ; S: Descriptor type: enable for code/data segments, 0 for system segments
                 ; executable bit: 1 
                 ; Direction bit/Conforming bit: 0, code only exec by priv lvl
                 ; RW (code segments read access, data segments always read, set for writing) : 1
                 ; Ac: accessed bit, set to 0 as recommended by osdev
    db 11001111b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
                 ; FLAGS (1st most significant nibble): 
                 ; Granularity: 0 = 1 byte blocks, 1= 4KiB blocks (pages)
                 ; Size bit: 0 = 16 bit protected mode, 1 = 32 bit protected mode
                 ; next to bytes in the nibble must be zero
                 ; 2nd nibble: limit 16:19
                 ; 
    db 00000000b ; base 24:31 - 8 bits - 1 byte

gdt_data_segment:
    ; this is the entry for the kernel address space
    dw 0xffff   ; limit 0:15 - 16 bits - 2 bytes
    dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
    db 0x00     ; base  16:23 - 8 bits - 1 byte
    db 10010010b ; access byte - 8 bits - 1 byte
    db 11001111b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
    db 00000000b ; base 24:31 - 8 bits - 1 byte

; gdt_tss_segment:
;     ; this is the entry for the kernel address space
;     dw 0x0000   ; limit 0:15 - 16 bits - 2 bytes
;     dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
;     db 0x00     ; base  16:23 - 8 bits - 1 byte
;     db 00000000b ; access byte - 8 bits - 1 byte
;     db 00000000b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
;     db 00000000b ; base 24:31 - 8 bits - 1 byte

; gdt_ldt_segment:
;     ; this is the entry for the kernel address space
;     dw 0x0000   ; limit 0:15 - 16 bits - 2 bytes
;     dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
;     db 0x00     ; base  16:23 - 8 bits - 1 byte
;     db 00000000b ; access byte - 8 bits - 1 byte
;     db 00000000b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
;     db 00000000b ; base 24:31 - 8 bits - 1 byte

; gdt_user_segment:
;     ; this is the entry for the kernel address space
;     dw 0x0000   ; limit 0:15 - 16 bits - 2 bytes
;     dw 0x0000   ; base  0:15 - 16 bits - 2 bytes
;     db 0x00     ; base  16:23 - 8 bits - 1 byte
;     db 00000000b ; access byte - 8 bits - 1 byte
;     db 00000000b ; first flags then limit 16:19 / (1 byte) 1 nibble - 1 nibble
;     db 00000000b ; base 24:31 - 8 bits - 1 byte

gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

gdt_end:

; constants to be used by stage1.asm

CODE_SEGMENT equ gdt_code_segment - gdt_start
DATA_SEGMENT equ gdt_data_segment - gdt_start
;TSS_SEGMENT  equ gdt_tss_segment  - gdt_start
;LDT_SEGMENT  equ gdt_ldt_segment  - gdt_start
;USER_SEGMENT equ gdt_user_segment - gdt_start

A20

Die A20-Leitung ist eine physische Repräsentation des Bits Nummer 21 jeder Speicheradresse. Aus Kompatibilitätsgründen mit älteren Prozessoren haben die Intel-CPU-Designer sie "zusammengefrickelt". Bis heute muss man bei modernen Intel-basierten Prozessoren weiterhin diesen Murks konfigurieren, der vor ungefähr 50 Jahren verbrochen wurde.

Klick auf A20 und such dir die komplette Geschichte dieser ganzen Nebenhandlung über Prozessor-Design und Intel raus, denn die ist wirklich nicht zu verachten.

Wir könnten uns die ganze Lore sparen, indem wir die Messlatte unseres Betriebssystems für IBM PS/2+-PC-Kompatible Rechner höher legen und die in osdev: Fast A20 Gate erwähnte Methode verwenden:

in al, 0x92
or al, 2
out 0x92, al

Aber stattdessen binden wir ein und rufen ein a20.asm auf, das ich von irgendeinem Ort in La Mancha kopiert und eingefügt habe, an dessen Namen ich mich nicht mehr erinnern kann.

Hinweis: Das ist eine Anspielung auf den Anfang von Cervantes’ Don Quijote: „An einem Ort in La Mancha, an dessen Namen ich mich nicht erinnern will.“ Im spanischen Original wird diese Formulierung hier absichtlich scherzhaft abgewandelt.

cr0

    mov eax, cr0
    or  eax, 0x1            ; set the 32-bit mode (Protected Mode)...
    mov cr0, eax            ; into cr0

    ; the CODE_SEGMENT is defined in gdt.asm
    jmp CODE_SEGMENT:init_protected_mode        ; far jump! 

Wir setzen Bit 1 des cr0-Control-Registers mit einem OR, um den [Protected Mode] zu aktivieren, und springen direkt zum 32-Bit-Code.

Es gibt noch eine ganze Menge anderer Dinge, die man in der CPU konfigurieren kann, aber die anderen verfügbaren Funktionen und Konfigurationen überlassen wir als Übung dem Leser ;-).

Eine Welt mit 32 Bit

Wir erreichen (endlich!) die Welt der 32 Bit und das realm der linearen Speicheradressen.

Wir kopieren einen "mysteriösen Kernel in C" in den Speicher, aber bevor wir ihn verwenden können, müssen wir offensichtlich die CPU, ihre Segmente im 32-Bit-Modus und außerdem den stack weiter konfigurieren.

pHUN pHAKT: Den stack nennen wir auf Criollo "La Pila".

Erklärung: „Pila“ hat im Spanischen zwei Bedeutungen. In der Informatik wird stack als pila übersetzt. Gleichzeitig bedeutet pila aber auch Batterie bzw. Batterie-Zelle – auf Deutsch also Batterie oder, je nach Kontext, Akku.

„En criollo le decimos ‘La Pila’“ macht also einen zusätzlichen Witz daraus: Der technische stack wird scherzhaft zu einer ganz normalen Batterie. Das funktioniert besonders gut, weil pila im argentinischen Spanisch ein ganz alltägliches Wort für eine Batterie ist.

; ---------------------------------------------------------------------
; 32 bit code! now we are able to use 32 bit instructions at this point
; ---------------------------------------------------------------------
bits 32         
init_protected_mode:
    ; and we need to setup the segments again
    ; with our previously configured GDT configuration addresses
    ; find gdt.asm and other %includes further in this listing
    mov ax, DATA_SEGMENT        ; defined in gdt.asm
    mov ds, ax                  ; data segment
    mov es, ax                  ; extended segment
    mov fs, ax                  ; fuck you segment
    mov ss, ax                  ; stack segment
    ;mov ax, USER_SEGMENT       ; in the future, maybe...
    mov gs, ax                  ; gorgeous segment <3
    ;mov ax, CODE_SEGMENT
    ;mov cs, ax                 ; TODO: why i didn't setup the code segment? 
                                ;       while reviewing the code for the article
                                ;       I found this commented like years ago
                                ;       and I got startled. Maybe for another
                                ;       time...

    ;ret ; !!!!!
    ; set up the stack
    ;mov ebp, 0xf90000        ; 16MB RAM machine! this breaks old 86box / PCem Machines!
    mov ebp, STACK_ADDRESS    ; equ 0x80000 --> stack ~ 576KiB
    mov esp, ebp

Eine der großen Debugging-Sessions, aus denen spawnten Teile von ISABugger-Related-Code hervorgegangen sind, bestand darin herauszufinden, warum unser Kernel abkackte, und es lag tatsächlich daran, dass der stack korrekt eingerichtet werden musste. Wie man im asm sehen kann, verwendete ich die Adresse 0xf90000 als extended base pointer und extended stack pointer, um festzulegen, wo unser Stack liegen sollte, und die VMs, auf denen ich testete, hatten nicht SO viel Speicher, wodurch ich über die Grenzen hinausging und die CPU eine nicht wiederherstellbare Exception auslöste. Nur eine einzige Instruktion und ziemlich viel Debugging-Zeit mit bochs waren nötig, damit ich den Fehler bemerkte.

Der stack liegt jetzt also ungefähr bei 576KiB.

    ; [ a lot of debugging stuff ]

    ; PIC Remapping ! (now handled by kernel at pic.c)

    ; mov al, 0x11
    ; out 0x20, al        ; restart pic1
    ; out 0xa0, al        ; restart pic2

    ; mov al, 0x20
    ; out 0x21, al        ; pic1 now starts at 32
    ; mov al, 0x28
    ; out 0xa1, al        ; pic2 now starts at 40

    ; mov al, 0x04
    ; out 0x21, al        ; setup cascading
    ; mov al, 0x02
    ; out 0xa1, al

    ; mov al, 0x01
    ; out 0x21, al
    ; out 0xa1, al        ; listo!


    ; ; IRQ1 = keyboard
    ; mov al, 0xFD
    ; out 0x21, al

    ; ; mask slave PIC
    ; mov al, 0xFF
    ; out 0xA1, al
    
    ; the kernel is the responsible for enabling
    ; the interrupts!
    ; the kernel now initializes IDT --> THEN STI()
    ;sti ; enable interrupts!

    jmp  KERNEL_OFFSET ; execute kernel code
  

Und der letzte jmp. Alles andere wurde auskommentiert, weil ich es in den in C geschriebenen kernel verschoben habe, zu dem wir im nächsten Artikel springen werden.

Zusammenfassend haben wir im Grunde den Kernel kopiert, den wir eigentlich noch gar nicht haben, sind in den [Protected Mode] gesprungen, haben dort den 32-Bit-stack eingerichtet und sind schließlich zum C Kernel Entrypoint gesprungen.

Referenzen