; MEMORY LAYOUT ; R = reserved, U = usable ; -------------------------------------------------------------------- ; R | 0x000000 - 0x000400: real-mode interrupt vector table ; R | 0x000400 - 0x000500: bios data area ; U | 0x000500 - 0x007c00: stack ; U | 0x007c00 - 0x007e00: boot sector ; U | 0x007e00 - 0x080000: conventional usable memory ; R | 0x080000 - 0x0a0000: extended bios data area (maximum possible size) ; R | 0x0a0000 - 0x0c0000: video memory ; R | 0x0c0000 - 0x0c8000: video bios ; R | 0x0c8000 - 0x0f0000: bios expansiosn ; R | 0x0f0000 - 0x100000: motherboard bios ; BASE STACK FRAME ; -------------------------------------------------------------------- ; bp - 0x02: drive number ; bp - 0x04: sectors per track ; bp - 0x06: number of heads ; bp - 0x08: partition array starting LBA ; bp - 0x0a: number of GPT partitions (saturated to 16-bits) ; bp - 0x0c: sector stride when loading GPT entries ; bp - 0x0e: byte stride when loading GPT entries ; bp - 0x10: entries per sector when loding GPT entries ; bp - 0x12: current entry index ; bp - 0x14: current entry index within the current sector ; bp - 0x16: number of sectors loaded ; bp - 0x18: current LBA ; BIOS puts our boot sector at 0000:7c00 org 0x7c00 ; We're (probably) in real mode bits 16 ; Disable interrupts cli xor ax, ax mov ds, ax mov es, ax ; Put the stack base at 0x7c00. ; Stack grows high->low, so we'll grow away from our program text at 0x7c00. mov ss, ax mov bp, 0x7c00 mov sp, bp ; Store boot drive number xor dh, dh push dx ; Get drive geometry mov di, 0x00 mov ah, 0x08 int 0x13 jc .panic ; Load sectors per track into cx & spill and cl, 0x3f xor ch, ch push cx ; Load number of heads into bx & spill movzx bx, dh inc bx push bx ; Load LBA 1. cmp cl, 1 ; Division of 1 by nonzero can be done with cmp sete al ; temp = LBA / (sectors per track) setne cl ; sector - 1 = LBA % (sectors per track) inc cl xor ah, ah ; Zero-extend temp xor dx, dx ; div by 16-bit register divides dx:ax, so we zero dx div bx ; dx = mod (head), ax = div (cylinder) mov dh, dl ; Head mov ch, al ; Cylinder ; We already have sector in cl mov ah, 2 ; Read disk mov al, 1 ; Load one sector mov bx, 0x7e00 mov dl, [bp - 0x2] ; Drive number int 0x13 jc .panic ; head = temp % number of heads ; cylinder = temp / number of heads mov cx, 8 mov si, .gpt_magic mov di, 0x7e00 repe cmpsb jne .panic ; Ensure the 8-byte GPT starting LBA fits in 16 bits mov di, 0x7e00 ; The rep increments di so we need to reset it mov eax, [di + 0x4c] mov bx, [di + 0x4a] or ax, bx or eax, eax jnz .panic ; Load and spill the GPT starting LBA mov ax, [di + 0x48] push ax ; Load number of partitions mov ax, [di + 0x50] mov bx, [di + 0x52] or bx, bx jz .gpt_n_partitions_loaded ; Number of partitions overflows 16 bits, so we just concern ourselves with the first 65535. ; That's an awful lot of partitions anyway. mov ax, 0xffff .gpt_n_partitions_loaded: push ax ; Load GPT entry size mov eax, [di + 0x54] ; Operand size override otherwise this is going to be painful mov ebx, eax ; Assert that the entry size is 128 * 2^n for some integer n>=0. This is required for a valid GPT ; and has the nice properties that: ; - If each entry is larger than a sector (512 bytes), they'll be sector-aligned. ; - If each entry is smaller than a sector, an integer number of them will fit into a sector. or eax, eax ; Test size != 0 because 128 * 2^n != 0 jz .panic and eax, 127 ; Test size is a multiple of 128 jnz .panic mov eax, ebx shr eax, 7 ; Divide by 128 mov ecx, eax dec eax and eax, ecx ; Test size/128 is a power of 2. popcnt gives an exception for some reason... jnz .panic mov eax, ebx ; Find the "sector stride", which is the number of sectors we increment by each time we want to ; load a new entry. shr eax, 9 ; Divide by sector size to get sectors per entry cmp eax, 0xffff ; Make sure sectors per entry fits in 16 bits ja .panic or ax, ax jnz .gpt_sector_stride_loaded ; Sector stride must be at least one or we'll load the same sector each time! inc ax .gpt_sector_stride_loaded: push ax ; Find the "byte stride", which is the number of bytes we increment by each time we want to load ; the next entry in the same sector. cmp ebx, 512 jb .gpt_find_entries_per_sector push word 0 ; Arbitrary byte stride since there's only one entry per sector push word 1 ; 1 entry per sector, since an entry is larger than a sector jmp .gpt_found_entries_per_sector .gpt_find_entries_per_sector: push bx ; Store byte stride = entry length in this case xor dx, dx mov ax, 512 div bx ; Find entries per sector push ax .gpt_found_entries_per_sector: ; Set up stack variables for our second stage search loop. xor ax, ax push ax ; Current entry push ax ; Current entry within the current sector push ax ; First LBA loaded? mov ax, [bp - 0x08] ; Starting LBA push ax ; Current LBA ; Search for the partition storing our second stage. .loop_find_stage2: mov dx, [bp - 0x12] cmp [bp - 0x0a], dx ; Panic if we've run out of partitions and haven't found the second stage yet. jbe .panic ; If we haven't loaded any sectors yet, load the first one. cmp word [bp - 0x16], 0 ; Load number of sectors loaded je .load_first_lba ; If there's still more entries in the current sector, skip loading a new sector mov ax, [bp - 0x18] ; Load current entry index within the current sector cmp [bp - 0x10], ax ; Compare to entries per sector ja .process_current_entry mov ax, [bp - 0x0c] ; Load sector stride add word [bp - 0x18], ax ; Increment current LBA by sector stride mov word [bp - 0x14], 0 ; Reset the current entry index within the current sector .load_first_lba: mov ax, [bp - 0x18] ; Load current LBA xor dx, dx div word [bp - 0x04] ; Divide by sectors per track. dx = mod (sector - 1), ax = div (temp) mov cx, dx inc cx ; Put sector into cx (bios will read sector from cl) xor dx, dx div word [bp - 0x06] ; Divide by number of heads. dx = mod (head), ax = div (cylinder) mov dh, dl mov ch, al mov dl, byte [bp - 0x02] ; Load drive number mov ah, 0x02 mov al, 1 mov bx, 0x8000 int 0x13 ; Read sector jc .panic inc word [bp - 0x16] ; Increment number of sectors loaded .process_current_entry: jmp .debug ; TODO ; TODO: remember to read offset by current entry index within the current sector inc word [bp - 0x12] ; Increment current entry index inc word [bp - 0x14] ; Increment current entry index within the current sector jmp .loop_find_stage2 .debug: ; Segment for VGA (0xb800 * 16 = 0xb8000) mov ax, 0xb800 mov fs, ax ; Set VGA mode ; https://mendelson.org/wpdos/videomodes.txt mov ax, 0x0003 int 0x10 mov al, [0x7e00] mov byte fs:[0x0000], al mov al, [0x8000] mov byte fs:[0x0002], al ; mov word fs:[0x0000], 0xc048 ; mov word fs:[0x0002], 0xc069 hlt .panic: mov ax, 0x0003 int 0x10 mov ax, 0xb800 mov ds, ax mov word [0x0000], 0x4f21 hlt ; TODO: enable A20 ; TODO: load a second stage ; TODO: grab all the info we can from bios interrupts and deliver it to ths OS nicely ; e.g. in a fixed memory location ; TODO: ; - Generate GPT in justfile ; - Parse global parition table ; - Load second stage from GPT partition with a particular UUID / name like GRUB does ; (it's Hah!IdontNeedEFI in GRUB) ; - https://en.wikipedia.org/wiki/BIOS_boot_partition ; - Future work: ; - Boot from UEFI ; - Boot on non-GPT partitioned disk .gpt_magic: db "EFI PART" .guid_linux: db 0xaf, 0x3d, 0xc6, 0x0f, 0x83, 0x84, 0x72, 0x47, \ 0x8e, 0x79, 0x3d, 0x69, 0xd8, 0x47, 0x7d, 0xe4 ; MBR bootstrap field is 440 bytes long %if ($ - $$) > 440 %error "exceeded mbr bootstrap field size" %endif