From dad65146188909b9568ed39b3b3fbc5e88e158c3 Mon Sep 17 00:00:00 2001
From: =?utf8?q?Duje=20Mihanovi=C4=87?= <duje.mihanovic@skole.hr>
Date: Wed, 27 Apr 2022 17:47:44 +0200
Subject: [PATCH] WIP: Add new FAT32 bootloader

Loads a LOADER.BIN which currently prints a hello message.

TODO: Make it actually load the kernel.
---
 Makefile                       |  25 +++--
 boot/x86/.gitignore            |   4 +
 boot/x86/Makefile              |  25 +++++
 boot/x86/disk.dump             |   7 ++
 boot/x86/fat32-structs.s       |  56 ++++++++++
 boot/x86/fat32.s               | 124 ++++++++++++++++++++++
 boot/x86/loader.s              |  31 ++++++
 boot/x86/mbr.s                 | 185 +++++++++++++++++++++++++++++++++
 boot/x86/{ => old}/a20.s       |   0
 boot/x86/{ => old}/boot.s      |   0
 boot/x86/{ => old}/print.s     |   0
 boot/x86/{ => old}/protected.s |   0
 boot/x86/vbr-fat32.s           | 102 ++++++++++++++++++
 how-to-compile                 |   7 +-
 14 files changed, 552 insertions(+), 14 deletions(-)
 create mode 100644 boot/x86/.gitignore
 create mode 100644 boot/x86/Makefile
 create mode 100644 boot/x86/disk.dump
 create mode 100644 boot/x86/fat32-structs.s
 create mode 100644 boot/x86/fat32.s
 create mode 100644 boot/x86/loader.s
 create mode 100644 boot/x86/mbr.s
 rename boot/x86/{ => old}/a20.s (100%)
 rename boot/x86/{ => old}/boot.s (100%)
 rename boot/x86/{ => old}/print.s (100%)
 rename boot/x86/{ => old}/protected.s (100%)
 create mode 100644 boot/x86/vbr-fat32.s

diff --git a/Makefile b/Makefile
index 848c795..d3f2ee4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
-AS = yasm
-LD = i686-elf-ld
-CC = i686-elf-gcc
+export AS = yasm
+export LD = i686-elf-ld
+export CC = i686-elf-gcc
+QEMU = qemu-system-i386
 
 GIT_REV = $(shell git rev-parse --short HEAD)
 
@@ -8,14 +9,15 @@ CFLAGS = -std=gnu89 -g -Iinclude/arch/x86 -ffreestanding -DGIT_COMMIT=\"$(GIT_RE
 
 KERNEL_OBJ = kernel/entry.o kernel/arch/x86/tty/tty.o kernel/drivers/irq/i8259a.o kernel/arch/x86/irq/idt.o kernel/arch/x86/irq/sample_handler.o kernel/kernel.o
 
-all: boot.img kernel/kernel.elf
+default: kernel/kernel.elf
 
-boot.img: boot/x86/boot kernel/kernel.bin
-	cat boot/x86/boot kernel/kernel.bin > $@
-	truncate -s1440K $@
+all: default boot/x86/disk.img
 
-boot/x86/boot: boot/x86/boot.s boot/x86/a20.s boot/x86/protected.s boot/x86/print.s
-	$(AS) -f bin boot/x86/boot.s -o $@
+run: boot/x86/disk.img
+	$(QEMU) boot/x86/disk.img
+
+boot/x86/disk.img: boot/x86/mbr.s boot/x86/vbr-fat32.s boot/x86/loader.s boot/x86/disk.dump boot/x86/fat32.s boot/x86/fat32-structs.s
+	cd boot/x86 && $(MAKE) all
 
 kernel/kernel.bin: ${KERNEL_OBJ}
 	$(LD) -o $@ -T kernel/linker.ld ${KERNEL_OBJ}
@@ -38,6 +40,7 @@ kernel/kernel.elf: kernel/kernel.bin
 	$(LD) -o $@ -T kernel/linker.ld ${KERNEL_OBJ} --oformat=elf32-i386
 
 clean:
-	rm boot/x86/boot kernel/kernel.bin kernel/kernel.elf ${KERNEL_OBJ} boot.img
+	-rm kernel/kernel.bin kernel/kernel.elf ${KERNEL_OBJ}
+	cd boot/x86 && $(MAKE) clean
 
-.PHONY: all clean
+.PHONY: default all clean run
diff --git a/boot/x86/.gitignore b/boot/x86/.gitignore
new file mode 100644
index 0000000..ef679a9
--- /dev/null
+++ b/boot/x86/.gitignore
@@ -0,0 +1,4 @@
+mbr
+vbr-fat32
+LOADER.BIN
+disk.img
diff --git a/boot/x86/Makefile b/boot/x86/Makefile
new file mode 100644
index 0000000..0349d2a
--- /dev/null
+++ b/boot/x86/Makefile
@@ -0,0 +1,25 @@
+default: mbr vbr-fat32 LOADER.BIN
+
+all: mbr vbr-fat32 LOADER.BIN disk.img
+
+mbr: mbr.s
+	$(AS) $(ASFLAGS) -w-zeroing -o $@ $<
+
+vbr-fat32: vbr-fat32.s fat32.s fat32-structs.s
+	$(AS) $(ASFLAGS) -o $@ $<
+
+LOADER.BIN: loader.s
+	$(AS) $(ASFLAGS) -o $@ $<
+
+disk.img: mbr vbr-fat32 LOADER.BIN disk.dump
+	truncate -s100M disk.img
+	sfdisk disk.img < disk.dump
+	mkfs.fat -F 32 --offset 2048 disk.img
+	dd if=mbr of=disk.img bs=440 count=1 conv=notrunc
+	dd if=vbr-fat32 of=disk.img bs=1 skip=90 seek=1048666 conv=notrunc
+	mcopy -i disk.img@@1M LOADER.BIN ::.
+
+clean:
+	-rm mbr vbr-fat32 LOADER.BIN disk.img
+
+.PHONY : default all clean
diff --git a/boot/x86/disk.dump b/boot/x86/disk.dump
new file mode 100644
index 0000000..2210fd2
--- /dev/null
+++ b/boot/x86/disk.dump
@@ -0,0 +1,7 @@
+label: dos
+label-id: 0xc0c03566
+device: disk.img
+unit: sectors
+sector-size: 512
+
+disk.img1 : start=        2048, size=      202752, type=b, bootable
diff --git a/boot/x86/fat32-structs.s b/boot/x86/fat32-structs.s
new file mode 100644
index 0000000..23980ba
--- /dev/null
+++ b/boot/x86/fat32-structs.s
@@ -0,0 +1,56 @@
+; FAT32 data structures
+; Because they have to be defined before use
+
+struc part_entry
+	.attrib: resb 1
+	.chs_start: resb 3
+	.type: resb 1
+	.chs_end: resb 3
+	.lba_start: resd 1
+	.lba_end: resd 1
+endstruc
+
+struc dir_entry
+	.name: resb 11
+	.attr: resb 1
+	.ntres: resb 1
+	.crttimetenth: resb 1
+	.crttime: resw 1
+	.crtdate: resw 1
+	.lstaccdate: resw 1
+	.firstclushi: resw 1
+	.wrttime: resw 1
+	.wrtdate: resw 1
+	.firstcluslo: resw 1
+	.filesize: resd 1
+endstruc
+
+; BPB definition, we use offsets to bp to save space
+	%define BS_jmpBoot [bp]
+	%define BS_OemName [bp+0x3]
+	%define BPB_BytsPerSec [bp+0xb]
+	%define BPB_SecPerClus [bp+0xd]
+	%define BPB_RsvdSecCnt [bp+0xe]
+	%define BPB_NumFats [bp+0x10]
+	%define BPB_RootEntCnt [bp+0x11]
+	%define BPB_TotSec16 [bp+0x13]
+	%define BPB_Media [bp+0x15]
+	%define BPB_FatSz16 [bp+0x16] ; count of sectors
+	%define BPB_SecPerTrk [bp+0x18]
+	%define BPB_NumHeads [bp+0x1a]
+	%define BPB_HiddSec [bp+0x1c]
+	%define BPB_TotSec32 [bp+0x20]
+	%define BPB_FatSz32 [bp+0x24]
+	%define BPB_ExtFlags [bp+0x28]
+	%define BPB_FSVer [bp+0x2a]
+	%define BPB_RootClus [bp+0x2c]
+	%define BPB_FsInfo [bp+0x30]
+	%define BPB_BkBootSec [bp+0x32] ; number of sector
+	%define BPB_Reserved [bp+0x34]
+	%define BS_DrvNum [bp+0x40]
+	%define BS_Reserved1 [bp+0x41]
+	%define BS_BootSig [bp+0x42]
+	%define BS_VolId [bp+0x43]
+	%define BS_VolLab [bp+0x47]
+	%define BS_FilSysType [bp+0x52]
+
diff --git a/boot/x86/fat32.s b/boot/x86/fat32.s
new file mode 100644
index 0000000..31288cb
--- /dev/null
+++ b/boot/x86/fat32.s
@@ -0,0 +1,124 @@
+; FAT32 driver
+; BOOT_DRIVE, part_table_entry and bp must be set up by the code using this driver.
+; first_data_sec is to be set up by calling get_1st_data_sec.
+
+; eax - start cluster number
+; es:di - where to load the cluster chain
+; NOTE: Cluster chain might be the root directory or a file.
+read_cluster_chain:
+	pusha
+.loop:
+	; get the first sector of the cluster to read
+	push eax
+	sub eax, 2
+	xor ebx, ebx
+	mov bl, BPB_SecPerClus
+	mul ebx
+	add eax, [first_data_sec]
+	mov ebx, eax
+	pop eax
+
+	; read the cluster
+	movzx cx, BPB_SecPerClus
+	call read_sectors
+
+	; increment the load offset (and segment if needed)
+	push eax
+	xor eax, eax
+	movzx ax, BPB_SecPerClus
+	mul word BPB_BytsPerSec
+	add di, ax
+	jno .get_next_cluster
+	mov ax, es
+	add ax, 0x1000
+	mov es, ax
+
+.get_next_cluster:
+	pop eax
+	; get FAT sector number and offset containing the next cluster number
+	xor edx, edx
+	mov ebx, 4
+	mul dword ebx
+	movzx ebx, word BPB_BytsPerSec
+	div dword ebx
+	movzx ebx, word BPB_RsvdSecCnt
+	add eax, ebx
+	push bp
+	mov bp, [part_table_entry]
+	add eax, [bp+part_entry.lba_start]
+	pop bp
+	mov ebx, edx
+	; reminder for myself: EAX is FAT sector number, (E)BX is offset into FAT sector
+	
+	; load the FAT sector we're looking for
+
+	push ebx ; offset
+	push es ; we want to read at 0:1000, not STAGE3_SEGMENT:1000
+	push eax ; desired LBA
+	xor ax, ax
+	mov es, ax
+	pop ebx ; pop LBA into EBX
+	mov di, 0x1000
+	mov cx, 1
+	call read_sectors
+	
+	; find the cluster we're looking for
+	pop es ; restore STAGE3_SEGMENT
+	pop ebx ; pop FAT offset back into EBX for cmp
+	cmp dword [di+bx], 0xffffff7
+	jge .done ; if cluster number is greater than or equal to the defective cluster value, we're done
+		  ; TODO: should this perhaps be changed so that equal makes it error out?
+	; otherwise, load the next sector number in eax and read again
+	mov eax, [di+bx]
+	jmp .loop
+.done:
+	; cleanup and return
+	popa
+	ret
+
+; es:di - where to load the sector(s)
+; ebx - start LBA address
+; cx - how many sectors to read
+read_sectors:
+	pusha
+	mov ah, 0x42
+	push dword 0
+	push dword ebx
+	push es
+	push di
+	push cx
+	push word 0x10
+	mov si, sp
+	mov dl, [BOOT_DRIVE]
+	int 0x13
+	jc .error
+	add sp, 16
+	popa
+	ret
+	.error:
+		mov si, read_error
+		call print
+		hlt
+		jmp $-1
+
+; no arguments
+get_1st_data_sec:
+	push eax
+	push ebx
+	xor eax, eax
+	movzx ax, BPB_NumFats 
+	mul dword BPB_FatSz32 ; space occupied by all FATs
+	movzx ebx, word BPB_RsvdSecCnt
+	add eax, ebx ; space occupied by the reserved area
+	mov bx, [part_table_entry]
+	add eax, [bx+part_entry.lba_start] ; space before the partition
+	mov [first_data_sec], eax
+	pop ebx
+	pop eax
+	ret
+
+read_error: db "Read error", 0
+
+BOOT_DRIVE: db 0
+part_table_entry: dw 0
+first_data_sec: dd 0
diff --git a/boot/x86/loader.s b/boot/x86/loader.s
new file mode 100644
index 0000000..e18b87e
--- /dev/null
+++ b/boot/x86/loader.s
@@ -0,0 +1,31 @@
+bits 16
+cpu 686
+org 0x0
+
+%macro print 1
+
+	mov si, %1
+	call print_str
+	
+%endmacro
+
+_start:
+	print string
+	hlt
+	jmp $-1
+	
+print_str:
+	pusha
+	mov ah, 0xe
+	xor bh, bh
+.loop:
+	lodsb
+	cmp al, 0
+	je .done
+	int 0x10
+	jmp .loop
+.done:
+	popa
+	ret
+
+string: db "Hello from LOADER.BIN!", 0xd, 0xa, 0
diff --git a/boot/x86/mbr.s b/boot/x86/mbr.s
new file mode 100644
index 0000000..8089b2f
--- /dev/null
+++ b/boot/x86/mbr.s
@@ -0,0 +1,185 @@
+; x86 bootloader for nameless-os, MBR portion
+
+bits 16
+cpu 686
+org 0x600
+
+_start:
+	cli
+	; set up segment registers
+	xor ax, ax
+	mov ds, ax
+	mov es, ax
+	mov ss, ax
+	mov sp, 0x7c00 ; just under the soon-to-be-loaded VBR, should be more than sufficient space
+
+	; perform self-relocation
+	cld
+	mov si, 0x7c00
+	mov di, 0x600
+	mov cx, 0x100 ; 256 words = 512 bytes
+	rep movsw
+	; some BIOSes may set CS to 0x7c0, work around that
+	jmp 0:real_start
+real_start:
+	sti
+	; check for int 13h ext
+	call check_int13_ext
+	mov byte [BOOT_DRIVE], dl
+	; look for active partition
+	mov bx, part_1
+	mov cx, 4
+	.check_part_loop:
+		mov al, [bx]
+		test al, 0x80
+		jnz .part_found
+		add bx, 16
+		loop .check_part_loop
+		.error:
+			mov si, no_os
+			call print
+			cli
+			hlt
+			jmp short $-1
+	.part_found:
+		; look for any other active partitions, if they exist the partition table is invalid
+		cmp cx, 0
+		je .load_vbr
+		push bx
+	.look_other_loop:
+		add bx, 16
+		mov al, [bx]
+		test al, 0x80
+		jnz .invalid_mbr
+		loop .look_other_loop
+	.load_vbr:
+		; load active partition's VBR
+		pop bx
+		mov ax, 0x7c0
+		mov edx, [bx+0x8]
+		push bx ; save pointer to partition table entry
+		xor bx, bx
+		call read_sector
+		; check is the VBR bootable (ends with 0x55 0xaa), if not halt
+		cmp word [0x7dfe], 0xaa55
+		jne .not_bootable
+		mov dl, [BOOT_DRIVE]
+		pop si ; hand off partition table entry to VBR
+		jmp 0x7c00
+		.not_bootable:
+			mov si, no_os
+			call print
+			cli
+			hlt
+			jmp short $-1
+	.invalid_mbr:
+		mov si, invalid_mbr
+		call print
+		cli
+		hlt
+		jmp short $-1
+check_int13_ext:
+	pusha
+	mov ah, 0x41
+	mov bx, 0x55aa
+	mov dl, 0x80
+	int 0x13
+	jc .no_ext
+	test cx, 1
+	jz .no_ext
+	popa
+	ret
+	.no_ext:
+		mov si, no_int13_ext
+		call print
+		cli
+		hlt
+		jmp short $-1
+
+; ax = segment
+; bx = offset
+; edx = start LBA of where to read
+read_sector:
+	pusha
+	; set up temporary DAP
+	push dword 0
+	push edx
+	push ax
+	push bx
+	push word 1
+	push word 0x10
+	mov ah, 0x42
+	mov dl, byte [BOOT_DRIVE]
+	mov si, sp
+	int 0x13
+	jc .error
+	add sp, 16 ; dump our temporary DAP
+	popa
+	ret
+	.error:
+		mov si, read_fail
+		call print
+		cli
+		hlt
+		jmp short $-1
+
+; DS:SI = string
+print:
+	pusha
+	mov ah, 0xe
+	xor bh, bh
+.loop:
+	lodsb
+	cmp al, 0
+	je .done
+	int 10h
+	jmp .loop
+.done:
+	mov al, 0xd
+	int 10h
+	mov al, 0xa
+	int 10h
+	popa
+	ret
+
+BOOT_DRIVE: resb 1
+
+read_fail: db "Error reading stage 2", 0
+invalid_mbr: db "Invalid partition table", 0
+no_os: db "No OS found", 0
+no_int13_ext: db "INT 13h extensions not found", 0
+
+times 440-($-$$) db 0
+
+part_table:
+disk_id: dd 0
+reserved: dw 0
+part_1:
+	.attrib db 0
+	.chs_start times 3 db 0
+	.part_type db 0
+	.chs_end times 3 db 0
+	.lba_start dd 0
+	.sect_count dd 0
+part_2:
+	.attrib db 0
+	.chs_start times 3 db 0
+	.part_type db 0
+	.chs_end times 3 db 0
+	.lba_start dd 0
+	.sect_count dd 0
+part_3:
+	.attrib db 0
+	.chs_start times 3 db 0
+	.part_type db 0
+	.chs_end times 3 db 0
+	.lba_start dd 0
+	.sect_count dd 0
+part_4:
+	.attrib db 0
+	.chs_start times 3 db 0
+	.part_type db 0
+	.chs_end times 3 db 0
+	.lba_start dd 0
+	.sect_count dd 0
+signature: dw 0xaa55
diff --git a/boot/x86/a20.s b/boot/x86/old/a20.s
similarity index 100%
rename from boot/x86/a20.s
rename to boot/x86/old/a20.s
diff --git a/boot/x86/boot.s b/boot/x86/old/boot.s
similarity index 100%
rename from boot/x86/boot.s
rename to boot/x86/old/boot.s
diff --git a/boot/x86/print.s b/boot/x86/old/print.s
similarity index 100%
rename from boot/x86/print.s
rename to boot/x86/old/print.s
diff --git a/boot/x86/protected.s b/boot/x86/old/protected.s
similarity index 100%
rename from boot/x86/protected.s
rename to boot/x86/old/protected.s
diff --git a/boot/x86/vbr-fat32.s b/boot/x86/vbr-fat32.s
new file mode 100644
index 0000000..061ca52
--- /dev/null
+++ b/boot/x86/vbr-fat32.s
@@ -0,0 +1,102 @@
+; x86 bootloader for nameless-os, FAT32 VBR portion
+; This is what's going to be on most USB sticks and HDDs, for now
+
+bits 16
+org 0x7c00
+cpu 686
+
+STAGE3_ADDRESS equ 0x8000
+STAGE3_SEGMENT equ STAGE3_ADDRESS >> 4
+STAGE3_OFFSET equ STAGE3_ADDRESS & 0xf
+
+%include "fat32-structs.s"
+
+_start:
+	jmp short real_start
+	nop
+
+times 0x57 db 0 ; skip past BPB
+
+real_start:
+	sti
+	; no need to set up segments and stack again, because MBR did it for us
+	mov bp, 0x7c00
+
+	; we expect the boot drive to be in DL and our partition table entry in DS:SI
+	mov [BOOT_DRIVE], dl
+	mov [part_table_entry], si
+
+	; calculate the 1st sector of the data area
+	call get_1st_data_sec
+
+.load_root:
+	; load the root directory
+	mov ax, 0x1000
+	mov es, ax
+	xor di, di
+	mov eax, BPB_RootClus
+	call read_cluster_chain
+
+	mov cx, 11
+.find_stage_3:
+	mov si, STAGE3_NAME
+	cmp byte [es:di], 0 ; we have no more entries to look at
+	je .stage3_missing
+	cmp byte [es:di], 0xe5 ; the entry was only previously used and as such not worth looking at
+	je .increment
+	repe cmpsb
+	je .stage3_found
+.increment:
+	add di, 32
+	jno .find_stage_3
+	mov bx, es
+	add bx, 0x1000
+	mov es, bx
+.stage3_found:
+	sub di, 0xb ; "repe cmpsb" incremented this
+	; stage 3 has been found and ES:DI points to its directory entry
+	mov ax, [es:di+dir_entry.firstclushi] ; load high half of the 1st cluster
+	shl eax, 16
+	mov ax, [es:di+dir_entry.firstcluslo] ; load low half of the 1st cluster
+	mov bx, STAGE3_SEGMENT
+	mov es, bx
+	mov di, STAGE3_OFFSET
+	call read_cluster_chain ; read stage 3
+	mov ds, bx
+	call STAGE3_SEGMENT:STAGE3_OFFSET ; call stage 3
+	jmp .halt ; halt in case we return, which should never happen
+
+.stage3_missing:
+	mov si, stage3_missing
+	call print
+.halt:
+	cli
+	hlt
+	jmp short $-1
+
+; ds:si - string
+print:
+	pusha
+	mov ah, 0xe
+	xor bh, bh
+.loop:
+	lodsb
+	cmp al, 0
+	je .done
+	int 0x10
+	jmp .loop
+.done:
+	mov al, 0xd
+	int 0x10
+	mov al, 0xa
+	int 0x10
+	popa
+	ret
+
+%include "fat32.s"
+
+stage3_missing: db "LOADER.BIN is missing", 0
+STAGE3_NAME: db "LOADER  BIN"
+
+times 510-($-$$) db 0
+dw 0xaa55
diff --git a/how-to-compile b/how-to-compile
index bcd0b8b..c84edb4 100644
--- a/how-to-compile
+++ b/how-to-compile
@@ -1,10 +1,11 @@
 1. Compile an i686-elf GCC and binutils following the instructions on this page:
    https://wiki.osdev.org/GCC_Cross-Compiler
+   Or use some tool to automate it, like I use crossdev on my Gentoo machine.
 
 2. Install yasm, or modify the makefile to use nasm if you prefer it
 
 3. Run make in the source root
 
-4. Run boot.img with some emulator, I use qemu:
-   qemu-system-i386 boot.img
-   You can use the x86_64 qemu, Bochs, VirtualBox or something else if you prefer
+4. Run the OS:
+   make run
+   This will create a disk image (if needed) and run it. Assumes that sfdisk, mtools and dosfstools are installed.
-- 
2.39.5