| git.druid.rocks | index | druid520 | kaboom | src/ | boot/ | dynamite.s |
src/boot/dynamite.s
/*
* dynamite: kaboom's real bios bootloader. bios loads this exact
* 512-byte sector at 0x7c00 in 16-bit real mode and jumps to it with
* dl = the boot drive number -- that's the entire contract a real
* bios gives us. everything past that is this file's own job: enable
* a20, read the kernel off disk in bios-safe chunks, get into 32-bit
* protected mode, relocate the kernel up to its real load address,
* and jump to it. matches boot.s's own note on why this class of code
* is the single highest-risk in the whole project -- one wrong byte
* here triple-faults silently, no error message, real hardware and
* qemu alike.
*
* KERNEL_BLOB_DWORDS, KERNEL_LOAD_ADDR and KERNEL_ENTRY_ADDR are never
* hand-computed or hardcoded here -- mk/bl.sh passes them in as --defsym
* values, read straight off the actual built out/kaboom.elf at build time (its
* real entry address, and the raw blob's real size rounded up to a
* dword), so this file never depends on a guessed offset that could
* silently drift out of sync with a real build.
*
* two real, known, out-of-scope-to-fix limitations, noted here rather
* than coded around -- neither is meaningfully testable without real
* hardware to try it against:
* 1. there is no mbr partition table -- lba 0 is dynamite's own
* 512-byte sector in full, not a partitioned disk. fine for a
* dedicated boot device (a usb flash drive imaged with dd), but
* this image should never be presented as one partition among
* several to a bios/os expecting a partition table.
* 2. some older bioses' usb-floppy-emulation mode (distinct from the
* far more common usb-hdd/"superfloppy" emulation mode most
* modern bioses use) patch bytes 0x03-0x3d of the boot sector
* (the classic fat12/16 bpb region) before executing it -- since
* dynamite's real code occupies that exact byte range (code
* starts immediately at offset 0, no bpb reserved), such a bios
* would corrupt dynamite's own instructions. a known, real, but
* narrow limitation of the current design.
*/
.code16
.global _dynamite_start
_dynamite_start:
cli
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
movw $0x7c00, %sp
sti
movb %dl, boot_drive
/* fast a20 gate (port 0x92) -- every cpu capable of the long mode
* kaboom itself needs already supports this, no need for the
* older, slower keyboard-controller a20 dance ancient bootloaders
* needed. */
inb $0x92, %al
orb $0x02, %al
outb %al, $0x92
/* int 13h ah=41h "extensions present" check, run ONCE here before
* the chunked ah=42h read loop begins -- a standard, cheap bios
* capability check. some very old bioses don't support the
* extended (lba) disk services ah=42h relies on; without this,
* such a bios would just get called with ah=42h blindly and
* return an undefined/garbage result instead of a clean, visible
* failure. reuses the existing disk_error path (the same crude
* "DE" red-screen vga signal a failed ah=42h read already uses) --
* dl gets reloaded from boot_drive again below for the real int
* 13h calls inside read_loop, so nothing here needs to preserve it
* across the two calls. */
movb $0x41, %ah
movw $0x55aa, %bx
movb boot_drive, %dl
int $0x13
jc disk_error
cmpw $0xaa55, %bx
jne disk_error
/* bit 0 of cx: "extended disk access functions (ah=42h-44h,47h,
* 48h) supported" -- the specific subset read_loop's own ah=42h
* calls below actually rely on, distinct from the coarser
* carry/bx check above (which only confirms extensions of SOME
* kind are present). */
testb $0x01, %cl
jz disk_error
/* read the whole reserved kernel region (lba 1..1024) in 64-
* sector (32kib) chunks, not one single 1024-sector call -- real
* bios int 13h ah=42h implementations commonly cap a single
* transfer well under 1024 sectors (some as low as 127), and
* keeping each chunk's transfer buffer within one real-mode
* segment side-steps the classic "transfer crosses a 64kib
* segment boundary" bios bug class entirely, for free. 16 chunks
* of 64 sectors = 1024 sectors total. */
movw $16, chunks_left /* 16*64 = 1024 sectors -- must stay in sync with
* KFS_LBA_BASE (currently 1025 = 1 dynamite
* sector + 1024 kernel-blob sectors) in
* src/fs/kfs.nsc and mk/disk.pl, the same way
* those two already reference each other. */
movw $1, dap_lba_lo /* first chunk starts at lba 1, right after us --
* see the KFS_LBA_BASE note just above. */
movw $0x1000, dap_seg /* destination segment for chunk 0: 0x1000:0000 = 0x10000 */
read_loop:
movw $dap, %si
movb $0x42, %ah
movb boot_drive, %dl
int $0x13
jc disk_error
/* advance to the next chunk: +64 sectors on the disk side, +32kib
* (0x800 paragraphs) on the memory side. */
movw dap_lba_lo, %ax
addw $64, %ax
movw %ax, dap_lba_lo
movw dap_seg, %ax
addw $0x0800, %ax
movw %ax, dap_seg
decw chunks_left
jnz read_loop
/* real bios e820 memory map -- same (addr, size, type) 24-byte
* entry shape vmm_e820_add (vmm.nsc) already expects, written
* directly into a fixed low-memory scratch buffer kmain reads
* after the mode transition (same "compute it once in real mode,
* hand it forward" pattern this file already uses for
* KERNEL_ENTRY_ADDR/KERNEL_LOAD_ADDR). es is still 0 from the very
* top of this file, so es:di addresses e820_buf directly as long
* as its linked address stays below 0x10000 -- true here since
* e820_buf lives in .bss, placed by the linker right after this
* file's own 512-byte .text, nowhere near the 0x10000 kernel-blob
* scratch segment read_loop just filled. */
xorl %ebx, %ebx
movl $e820_buf, %edi
movl $0, e820_count
e820_loop:
movl $0x0534d4150, %edx /* 'SMAP' */
movl $0xe820, %eax
movl $24, %ecx
int $0x15
jc e820_done /* carry set: error or end of list */
cmpl $0x0534d4150, %eax
jne e820_done /* bios didn't return 'SMAP': stop */
incl e820_count
addl $24, %edi
cmpl $256, e820_count
jae e820_done /* hit the same 256-entry ceiling
* vmm_e820_add enforces -- stop
* asking rather than overflow
* e820_buf */
testl %ebx, %ebx
jnz e820_loop /* ebx==0 means that was the last entry */
e820_done:
/* interrupts off before the mode switch (intel sdm vol 3, the
* protected-mode switch procedure's own step 1). the sti above was
* only for the int 13h reads. an irq taken after cr0.pe is set
* would be dispatched through idtr, which still points at the
* real-mode ivt, read as protected-mode gates: triple fault. the
* window runs through the whole rep movsl below and the jmp into
* _start, which does its own cli. */
cli
lgdt gdt_ptr
movl %cr0, %eax
orl $0x1, %eax
movl %eax, %cr0
ljmpl $0x08, $protected_mode_entry
disk_error:
/* a crude but real visible failure signal (prints "DE" in white-
* on-red directly into vga text memory) rather than a silent
* hang indistinguishable from a triple fault -- there's no string-
* printing code this early to do anything fancier. */
movw $0xb800, %ax
movw %ax, %es
movw $0x4c44, %es:0x00
movw $0x4c45, %es:0x02
cli
hang:
hlt
jmp hang
.align 4
gdt:
.quad 0 /* null descriptor */
.quad 0x00cf9a000000ffff /* 0x08: 32-bit code, ring0, flat, 4kib granularity */
.quad 0x00cf92000000ffff /* 0x10: 32-bit data, ring0, flat, 4kib granularity */
gdt_end:
gdt_ptr:
.word gdt_end - gdt - 1
.long gdt
.align 4
dap: /* bios int 13h ah=42h disk address packet, 16 bytes */
.byte 0x10 /* packet size */
.byte 0 /* reserved */
dap_count:
.word 64 /* sectors per chunk */
.word 0x0000 /* transfer buffer offset, always 0 -- only the segment advances */
dap_seg:
.word 0 /* transfer buffer segment, patched per chunk above */
dap_lba_lo:
.word 0 /* starting lba (low 16 bits), patched per chunk above */
.word 0
.long 0 /* starting lba (high 32 bits) -- always 0, this disk is nowhere near 4 billion sectors */
boot_drive:
.byte 0
chunks_left:
.word 0
/* e820 scratch buffer -- deliberately .bss, not the default (.text)
* section everything above lives in: mk/bl.sh's objcopy -j .text pulls
* ONLY .text into the real 512-byte sector, so a .bss reservation here
* costs the sector nothing (no bytes emitted) while still getting a
* real, stable linked address from the same `ld -Ttext=0x7c00` command
* that lays out everything else in dynamite.elf -- i.e. this is a
* READ address computed once, by the linker, never a hand-guessed
* magic number the way this file's own header comment warns against
* for KERNEL_LOAD_ADDR/KERNEL_ENTRY_ADDR. 256 entries * 24 bytes =
* 6144, the same ceiling and entry size vmm_e820_add (vmm.nsc)
* already enforces. */
.section .bss
.align 4
e820_buf:
.skip 6144
e820_count:
.long 0
.section .text
.code32
protected_mode_entry:
movw $0x10, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
movw %ax, %fs
movw %ax, %gs
movl $0x9000, %esp /* well clear of the 0x10000-0x90000 scratch region the kernel blob was just read into */
/* relocate the whole kernel blob from the low scratch buffer
* (0x10000) up to its real home, KERNEL_LOAD_ADDR -- the lowest
* load address in out/kaboom.elf, which is where objcopy -O binary
* starts the blob (mk/bl.sh reads it off the elf, same as the
* entry point). 32-bit addressing has no real-mode-style reach
* limit, so this is a plain, ordinary copy. cld: neither a bios
* handoff nor anything else guarantees df=0, and a backwards copy
* would run down through low memory instead. */
movl $0x10000, %esi
movl $KERNEL_LOAD_ADDR, %edi
movl $KERNEL_BLOB_DWORDS, %ecx
cld
rep movsl
/* hand the e820 buffer forward to boot.s's _start the same way
* pvh's own entry hands hvm_start_info forward via %ebx: esi/edx
* are both free again here (rep movsl is done with them) and
* neither is touched by _start before it saves them off, same as
* ebx -- see boot.s's own comment on that. %ebp carries an explicit
* "this is the real-bios path" sentinel rather than leaving _start
* to guess the boot path from whether esi/edx happen to be zero:
* the pvh entry ABI only defines %ebx, so nothing guarantees any
* other register is reliably zero there, now or in some future
* qemu -- an explicit magic beats an inferred one. */
movl $0xb105b00b, %ebp /* BIOS BOOT marker, picked to be
* essentially impossible to collide
* with whatever pvh's own loader
* happens to leave in %ebp */
movl $e820_buf, %esi
movl e820_count, %edx
jmp KERNEL_ENTRY_ADDR
/* boot signature -- bios refuses to treat this sector as bootable at
* all without these exact two bytes at offset 510/511. */
.org 510
.word 0xaa55