From e08b93e3bb729788366c15dd39cb19ccff8cc4e5 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 3 Mar 2025 18:50:46 -0800 Subject: [PATCH] Comments --- Makefile | 46 +++-------- README.md | 3 - bios/main.asm | 59 +++++++------- bios/print.asm | 29 +++---- bios/protected_mode.asm | 46 ----------- bios/stage1.asm | 121 +++++++++++++++-------------- bios/stage2.asm | 50 ++++++++++-- tetros/linkers/x86-unknown-none.ld | 5 +- 8 files changed, 165 insertions(+), 194 deletions(-) delete mode 100644 bios/protected_mode.asm diff --git a/Makefile b/Makefile index 45f5b6c..e6b4901 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ BUILD=./build # Default rule .PHONY: default -default: all +default: $(BUILD)/disk.img # Remove all build files .PHONY: clean @@ -10,16 +10,12 @@ clean: rm -drf $(BUILD) cd tetros; cargo clean -# Make everything -# (but don't run qemu) -.PHONY: all -all: img - # -# MARK: boot +# MARK: disk # -# Compile tetros as library +# Compile tetros as a library +# (so that we can link it with a custom linker script) LIB_SRC = ./tetros/Cargo.toml ./tetros/Cargo.lock $(shell find ./tetros/src -type f) $(BUILD)/tetros.lib: $(LIB_SRC) @mkdir -p $(BUILD) @@ -35,7 +31,7 @@ $(BUILD)/tetros.lib: $(LIB_SRC) -- \ --emit link="$(CURDIR)/$@" -# Link tetros +# Link tetros using custom linker script BIOS_LD = ./tetros/linkers/x86-unknown-none.ld $(BUILD)/tetros.elf: $(BUILD)/tetros.lib $(BIOS_LD) ld \ @@ -49,13 +45,13 @@ $(BUILD)/tetros.elf: $(BUILD)/tetros.lib $(BIOS_LD) objcopy --only-keep-debug "$@" "$@.sym" objcopy --strip-debug "$@" -# Wrap tetros in three-stage BIOS loader +# Wrap tetros in BIOS loader # Parameters: # - BIOS_SRC: source directory of bios assembly # - STAGE2_SECTOR: the index of the first sector of the stage 2 binary on the disk BIOS_SRC = ./bios STAGE2_SECTOR = 1 -$(BUILD)/bios.bin: $(wildcard $(BIOS_SRC)/*.asm) $(BUILD)/tetros.elf +$(BUILD)/disk.img: $(wildcard $(BIOS_SRC)/*.asm) $(BUILD)/tetros.elf @mkdir -p "$(BUILD)" nasm \ -f bin \ @@ -66,30 +62,12 @@ $(BUILD)/bios.bin: $(wildcard $(BIOS_SRC)/*.asm) $(BUILD)/tetros.elf -i "$(BIOS_SRC)" \ "$(BIOS_SRC)/main.asm" -# Extract full mbr (first 512 bytes) -$(BUILD)/mbr.bin: $(BUILD)/bios.bin - @mkdir -p "$(BUILD)" - @echo "" - dd if="$<" bs=512 count=1 of="$@" - -# Extract stage 2 (rest of file) -$(BUILD)/stage2.bin: $(BUILD)/bios.bin - @mkdir -p "$(BUILD)" - @echo "" - dd if="$<" bs=512 skip=1 of="$@" - # -# MARK: bundle +# MARK: qemu +# +# Do not use `-enable-kvm` or `-cpu host`, +# this confuses gdb. # - -# Make full disk image -.PHONY: img -img: $(BUILD)/disk.img -$(BUILD)/disk.img: $(BUILD)/mbr.bin $(BUILD)/stage2.bin - @mkdir -p $(BUILD) - @echo "" - dd if="$(BUILD)/mbr.bin" of=$@ conv=notrunc bs=512 - dd if="$(BUILD)/stage2.bin" of=$@ conv=notrunc seek=$(STAGE2_SECTOR) bs=512 .PHONY: qemu qemu: $(BUILD)/disk.img @@ -129,5 +107,3 @@ qemu-gdb: $(BUILD)/disk.img -gdb tcp::26000 \ -S -# Do not use `-enable-kvm` or `-cpu host`, -# this confuses gdb. \ No newline at end of file diff --git a/README.md b/README.md index 0b0b32b..f33206e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # TetrOS: bare-metal tetris -## TODO: -- Fix stage 1 loader - ## Features - Compiles to a standalone disk image - Written from scratch using only Nasm and Rust diff --git a/bios/main.asm b/bios/main.asm index 50c62ba..3193bb7 100644 --- a/bios/main.asm +++ b/bios/main.asm @@ -1,30 +1,25 @@ sectalign off -; This program expects two external macros: +; The following code expects two external macros: ; STAGE3, a path to the stage3 binary ; STAGE2_SECTOR, the location of stage 2 ; on the disk, in 512-byte sectors. -; On a gpt disk, this is probably 34. - -; Stage 1 is MBR code, and should fit in LBA 0 -; (512 bytes). Layout is as follows: -; (Format is `offset, length: purpose`) -; 0, 424: x86 boot code -; 440, 4: Unique disk signature -; 444, 2: unknown -; 446, 16*4: Array of four legacy MBR records -; 510, 2: signature 0x55 0xAA -; 512 to end of logical block: reserved -; -; See https://uefi.org/specs/UEFI/2.10/05_GUID_Partition_Table_Format.html +; +; Both of these are set in the makefile. +; BIOS loads stage 1 at 0x7C00 ORG 0x7C00 SECTION .text -; stage 1 is sector 0, loaded into memory at 0x7C00 +; Stage 1 is MBR code, and should fit in LBA 0 +; (i.e, in the first 512 bytes). %include "stage1.asm" ; Stage 1 is at most 440 bytes +; This limit is set by the GPT spec. +; See https://uefi.org/specs/UEFI/2.10/05_GUID_Partition_Table_Format.html +; +; This `times` will throw an error if the subtraction is negative. times 440-($-$$) db 0 db 0xee @@ -32,30 +27,38 @@ db 0xee times 510-($-$$) db 0 ; MBR signature. -; This isn't loaded into memory, it's -; only here for debugging. +; This tells the BIOS that this disk is bootable. db 0x55 db 0xaa + +; Include stage 2. This is loaded into memory by stage 1. +; (stage 1 loads both stage 2 and stage 3) +; +; Stage 2 sets up protected mode, sets up the GDT, +; and initializes a minimal environment for stage 3. +; +; On a "real" boot disk, this data will not immediately follow stage 1. +; It would be stored in a special disk partition. +; +; We don't need this kind of complexity here, though, so we store +; stage 2 right after stage 1. (This is why STAGE2_SECTOR is 1.) +; +; This is nice, because the layout of the code on our boot disk +; matches the layout of the code in memory. THIS IS NOT USUALLY THE CASE. stage2: %include "stage2.asm" align 512, db 0 stage2.end: -; The maximum size of stage2 is 4 KiB, -; This fill will throw an error if the subtraction is negative. -times (4*1024)-($-stage2) db 0 - -; Pad to 0x9000. -; This needs to match the value configured in the stage3 linker script -times (0x9000 - 0x7c00)-($-$$) db 0 +; Pad to 0x3000. +; This makes sure that state3 is loaded at the address +; the linker expects. Must match the value in `tetros/linkers/x86-unknown-none.ld`. +times (0x8000 - 0x7c00)-($-$$) db 0 +; Include stage 3, the binary compiled from Rust sources. stage3: %defstr STAGE3_STR %[STAGE3] incbin STAGE3_STR align 512, db 0 .end: - -; TODO: why? Of the disk, or of memory? -; the maximum size of the boot loader portion is 384 KiB -times (384*1024)-($-$$) db 0 diff --git a/bios/print.asm b/bios/print.asm index 5e958a3..ac8e111 100644 --- a/bios/print.asm +++ b/bios/print.asm @@ -1,22 +1,21 @@ SECTION .text USE16 -; provide function for printing in x86 real mode - -; print a string and a newline -; CLOBBER -; ax +; Print a string and a newline +; +; Clobbers ax print_line: mov al, 13 call print_char mov al, 10 jmp print_char -; print a string -; IN +; Print a string +; +; Input: ; si: points at zero-terminated String -; CLOBBER -; si, ax +; +; Clobbers si, ax print: pushf cld @@ -30,8 +29,9 @@ print: popf ret -; print a character -; IN +; Print a character +; +; Input: ; al: character to print print_char: pusha @@ -42,10 +42,11 @@ print_char: ret ; print a number in hex -; IN +; +; Input: ; bx: the number -; CLOBBER -; al, cx +; +; Clobbers al, cx print_hex: mov cx, 4 .lp: diff --git a/bios/protected_mode.asm b/bios/protected_mode.asm deleted file mode 100644 index 1df111f..0000000 --- a/bios/protected_mode.asm +++ /dev/null @@ -1,46 +0,0 @@ -SECTION .text -USE16 - -protected_mode: - -.func: dd 0 - -.entry: - ; disable interrupts - cli - - ; load protected mode GDT - lgdt [gdtr] - - ; set protected mode bit of cr0 - mov eax, cr0 - or eax, 1 - mov cr0, eax - - ; far jump to load CS with 32 bit segment - ; (we are in 32-bit mode, but instruction pipeline - ; has 16-bit instructions. - jmp gdt.pm32_code:.inner - - ; gdt.pm32_code is a multiple of 8, so it always ends with three zero bits. - ; The GDT spec abuses this fact, and uses these last three bits to store other - ; data (table type and privilege). In this case, 000 is what we need anyway. - ; - ; Also note that CS isn't an address in protected mode---it's a GDT descriptor. - - - -USE32 - -.inner: - ; load all the other segments with 32 bit data segments - mov eax, gdt.pm32_data - mov ds, eax - mov es, eax - mov fs, eax - mov gs, eax - mov ss, eax - - ; jump to specified function - mov eax, [.func] - jmp eax diff --git a/bios/stage1.asm b/bios/stage1.asm index 5452f8e..a266f5a 100644 --- a/bios/stage1.asm +++ b/bios/stage1.asm @@ -1,37 +1,39 @@ USE16 -stage1: ; dl comes with disk - ; initialize segment registers - xor ax, ax +stage1: + ; Initialize segment registers + xor ax, ax ; Set ax to 0 mov ds, ax mov es, ax mov ss, ax - ; initialize stack + ; Initialize stack pointer + ; (stack grows up) mov sp, 0x7C00 - ; initialize CS - ; far jump sets both CS and IP to a known-good state, - ; we don't know where the BIOS put us at startup. - ; (could be 0x00:0x7C00, could be 0x7C00:0x00. - ; Not everybody follows spec.) - push ax + ; Initialize CS + ; + ; `retf` sets both CS and IP to a known-good state. + ; This is necessary because we don't know where the BIOS put us at startup. + ; (could be 0x00:0x7C00, could be 0x7C00:0x00. Not everybody follows spec.) + push ax ; `ax` is still 0 push word .set_cs retf .set_cs: - ; save disk number + ; Save disk number. + ; BIOS sets `dl` to the number of + ; the disk we're booting from. mov [disk], dl + ; Print "Stage 1" mov si, stage_msg call print mov al, '1' call print_char call print_line - - - ; read CHS gemotry + ; read CHS gemotry, save into [chs] ; CL (bits 0-5) = maximum sector number ; CL (bits 6-7) = high bits of max cylinder number ; CH = low bits of maximum cylinder number @@ -51,11 +53,10 @@ stage1: ; dl comes with disk and cl, 0x3f mov [chs.s], cl - ; disk address of stage 2 - ; (start sector) + ; First sector of stage 2 mov eax, STAGE2_SECTOR - ; where to load stage 2 + ; Where to load stage 2 mov bx, stage2 ; length of stage2 + stage3 @@ -63,36 +64,40 @@ stage1: ; dl comes with disk mov cx, (stage3.end - stage2) / 512 mov dx, 0 + ; Consume eax, bx, cx, dx + ; and load code from disk. call load jmp stage2.entry -; load some sectors from disk to a buffer in memory -; buffer has to be below 1MiB -; IN +; Load sectors from disk to memory. +; Cannot load more than 1MiB. +; +; Input: ; ax: start sector ; bx: offset of buffer ; cx: number of sectors (512 Bytes each) ; dx: segment of buffer -; CLOBBER -; ax, bx, cx, dx, si -; TODO rewrite to (eventually) move larger parts at once -; if that is done increase buffer_size_sectors in startup-common to that (max 0x80000 - startup_end) +; +; Clobbers ax, bx, cx, dx, si load: - ; replaced 127 with 1. - ; see https://stackoverflow.com/questions/58564895/problem-with-bios-int-13h-read-sectors-from-drive - ; TODO: fix later + ; Every "replace 1" comment means that the `1` + ; on that line could be bigger. + ; + ; See https://stackoverflow.com/questions/58564895/problem-with-bios-int-13h-read-sectors-from-drive + ; We have to load one sector at a time to avoid the 1K boundary error. + ; Would be nice to read more sectors at a time, though, that's faster. - cmp cx, 1 ;127 + cmp cx, 1 ; replace 1 jbe .good_size pusha - mov cx, 1; 127 + mov cx, 1 ; replace 1 call load popa - add eax, 1; 127 - add dx, 1 * 512 / 16 ; 127 - sub cx, 1;127 + add eax, 1 ; replace 1 + add dx, 1 * 512 / 16 ; replace 1 + sub cx, 1 ; replace 1 jmp load .good_size: @@ -101,44 +106,37 @@ load: mov [DAPACK.count], cx mov [DAPACK.seg], dx -; This should be a subroutine, -; but we don't call/ret to save a few bytes. -; (we only use this once) -; -;call print_dapack -;print_dapack: - mov bx, [DAPACK.addr + 2] + ; Print the data we're reading + ; Prints AAAAAAAA#BBBB CCCC:DDDD, where: + ; - A..A is the lba we're reading (printed in two parts) + ; - BBBB is the number of sectors we're reading + ; - CCCC is the index we're writing to + ; - DDDD is the buffer we're writing to + mov bx, [DAPACK.addr + 2] ; last two bytes call print_hex - - mov bx, [DAPACK.addr] + mov bx, [DAPACK.addr] ; first two bytes call print_hex - mov al, '#' call print_char - mov bx, [DAPACK.count] call print_hex - mov al, ' ' call print_char - mov bx, [DAPACK.seg] call print_hex - mov al, ':' call print_char - mov bx, [DAPACK.buf] call print_hex - call print_line - ;ret - ; End of print_dapack + ; Read from disk. + ; int13h, ah=0x42 does not work on some disks. + ; use int13h, ah=0x02 in thes case. cmp byte [chs.s], 0 jne .chs - ;INT 0x13 extended read does not work on CDROM! + mov dl, [disk] mov si, DAPACK mov ah, 0x42 @@ -188,6 +186,10 @@ load: jc error ; carry flag set on error ret +; +; MARK: errors +; + error_chs: mov ah, 0 @@ -200,13 +202,18 @@ error: mov si, stage1_error_msg call print - call print_line + call print_line +; halt after printing error details .halt: cli hlt jmp .halt +; +; MARK: data +; + %include "print.asm" stage_msg: db "Stage ",0 @@ -215,9 +222,9 @@ stage1_error_msg: db " ERROR",0 disk: db 0 chs: -.c: dd 0 -.h: dd 0 -.s: dd 0 + .c: dd 0 + .h: dd 0 + .s: dd 0 DAPACK: db 0x10 @@ -225,6 +232,4 @@ DAPACK: .count: dw 0 ; int 13 resets this to # of blocks actually read/written .buf: dw 0 ; memory buffer destination address (0:7c00) .seg: dw 0 ; in memory page zero -.addr: dq 0 ; put the lba to read in this spot - -db 0xff \ No newline at end of file +.addr: dq 0 ; put the lba to read in this spot \ No newline at end of file diff --git a/bios/stage2.asm b/bios/stage2.asm index f964884..2675ab5 100644 --- a/bios/stage2.asm +++ b/bios/stage2.asm @@ -1,6 +1,9 @@ SECTION .text USE16 +%include "gdt.asm" +%include "thunk.asm" + stage2.entry: mov si, stage_msg call print @@ -13,26 +16,57 @@ stage2.entry: or al, 2 out 0x92, al - mov dword [protected_mode.func], stage3.entry - jmp protected_mode.entry +protected_mode: + ; disable interrupts + cli -%include "gdt.asm" -%include "protected_mode.asm" -%include "thunk.asm" + ; load protected mode GDT + lgdt [gdtr] + ; set protected mode bit of cr0 + mov eax, cr0 + or eax, 1 + mov cr0, eax + + ; far jump to load CS with 32 bit segment + ; We need to do this because we are entering 32-bit mode, + ; but the instruction pipeline still has 16-bit instructions. + ; + ; gdt.pm32_code is a multiple of 8, so it always ends with three zero bits. + ; The GDT spec abuses this fact, and uses these last three bits to store other + ; data (table type and privilege). In this case, 000 is what we need anyway. + ; + ; Also note that CS isn't an address in protected mode---it's a GDT descriptor. + jmp gdt.pm32_code:protected_mode_inner + +; We can now use 32-bit instructions! USE32 -stage3.entry: - ; stage3 stack at 448 KiB (512KiB minus 64KiB disk buffer) +protected_mode_inner: + ; load all the other segments with 32 bit data segments + mov eax, gdt.pm32_data + mov ds, eax + mov es, eax + mov fs, eax + mov gs, eax + mov ss, eax + + ; Place stage 3 stack at 448 KiB + ; (512KiB minus 64KiB disk buffer) mov esp, 0x70000 ; push arguments to `start()` mov eax, thunk.int10 push eax + + ; Call `start()`. + ; 0x18 skips ELF headers. mov eax, [stage3 + 0x18] call eax - + .halt: + ; Halt if `start()` ever returns (it shouldn't, but just in case) + ; Without this, we'll try to execute whatever comes next in memory. cli hlt jmp .halt diff --git a/tetros/linkers/x86-unknown-none.ld b/tetros/linkers/x86-unknown-none.ld index dc7e852..f3eaeb1 100644 --- a/tetros/linkers/x86-unknown-none.ld +++ b/tetros/linkers/x86-unknown-none.ld @@ -1,9 +1,10 @@ +/* This is the name of the Rust function we start in */ ENTRY(start) OUTPUT_FORMAT(elf32-i386) SECTIONS { - /* The start address must match bootloader.asm */ - . = 0x9000; + /* The start address must match main.asm */ + . = 0x8000; . += SIZEOF_HEADERS; . = ALIGN(4096);