Um das Betriebssystem tOSh herunterzuladen, kannst du dies über den Bereich Resources dieser Website tun.
Teile
Diese Artikelserie wird aus folgenden Teilen bestehen:
- Teil I - Bootloader
- Teil II - Stage 1 <— Du bist hier
- Teil III - Sprung zu C
- Teil IV - Interrupts
- Teil V - Dateisystem und Diskettenlaufwerk-Treiber
- Teil VI - Booten auf verschiedenen Maschinentypen
Turbo 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.