nadir-kernel/kernel/arch/x86_64/boot.asm

301 lines
10 KiB
NASM

; Nadir stage-1 boot sector.
;
; Runs in 16-bit real mode at 0x7C00 (Ring 0, CPL 0, BIOS hands us full
; privilege; privileged instructions used: cli, lgdt, mov cr0).
; Fault behavior: none possible here beyond a failed disk read, which prints
; an error and halts. There is no IDT yet, so any CPU exception would
; triple-fault and reset the machine.
;
; Job: enable A20, load the kernel image (LBA 1..N) to 0x10000, install a
; flat GDT, enter 32-bit protected mode and jump to the stage-2 entry.
;
; Memory map used by the whole boot chain:
; 0x005000 E820 staging: count byte at 0x5000, 24-byte entries from
; 0x5020 (64 max = 1536 bytes, ends 0x5620). Free: below the
; 0x7C00 sector, clear of the 0x10000 kernel load, the 0x70000
; tables and the 0x90000 stack. Stage 2 copies it into kernel
; .bss (memmap_count/memmap_entries) after BSS zeroing.
; 0x007C00 this sector (512 bytes)
; 0x010000 kernel image (loaded here, linked here)
; 0x70000 PML4 | 0x71000 PDPT | 0x72000 PD (built by stage 2)
; 0x90000 stack top (real-mode stack grows down from 0x7C00 instead)
; 0xB8000 VGA text buffer
bits 16
org 0x7C00
%ifndef KERNEL_SECTORS
%error "KERNEL_SECTORS not defined - build with make"
%endif
KERNEL_LOAD_SEG equ 0x1000 ; ES for load target 0x1000:0x0000 = 0x10000
STACK_TOP equ 0x7C00 ; real-mode stack grows down from here
E820_COUNT equ 0x5000 ; staged E820 entry count (one byte)
E820_BUF equ 0x5020 ; staged E820 entries (24 bytes each)
E820_MAX equ 64 ; cap: 64 * 24 = 1536 bytes, ends 0x5620
E820_ENTRY_SIZE equ 24
start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, STACK_TOP
sti
mov [boot_drive], dl ; BIOS passes the boot drive in DL
mov si, msg_loading
call puts
call enable_a20
call load_kernel
call query_e820
mov si, msg_entering
call puts
cli
lgdt [gdt_ptr]
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 0x08:prot32
; ---------------------------------------------------------------------------
; puts: print NUL-terminated string at DS:SI via BIOS teletype.
; Clobbers AX, BX, SI.
puts:
push ax
push bx
mov ah, 0x0E
mov bh, 0
.loop:
lodsb
test al, al
jz .done
int 0x10
jmp .loop
.done:
pop bx
pop ax
ret
; ---------------------------------------------------------------------------
; enable_a20: try the BIOS and fast-A20 methods in turn. Each is harmless
; if A20 is already on.
; WHY only two methods: the keyboard-controller pulse was dropped to free
; sector bytes for the E820 query above — every 64-bit CPU postdates
; chipsets with the 0x92 fast gate (or a BIOS that honors 0x2401), so the
; legacy AT pulse buys nothing on this kernel's hardware floor.
enable_a20:
mov ax, 0x2401 ; BIOS: enable A20
int 0x15
in al, 0x92 ; fast A20 (skip if already set or absent)
test al, 0x02
jnz .done
or al, 0x02
out 0x92, al
.done:
ret
; ---------------------------------------------------------------------------
; Floppy geometry is fixed at 18 sectors/track, 2 heads (the image only
; ever boots via -fda; see [spt]/[heads] inits below). The old int 0x13
; AH=08 probe was dropped to free sector bytes for the E820 query above.
; HDD generality wants EDD packet reads (AH=42), planned, not CHS probing.
; ---------------------------------------------------------------------------
; lba_to_chs: convert LBA in AX to int 0x13 CHS in CH/CL/DH.
; Clobbers AX, BX, CX, DX.
lba_to_chs:
xor dx, dx
div word [spt] ; AX = LBA/spt, DX = sector offset
inc dx ; sectors are 1-based
push dx ; save sector number
xor dx, dx
div word [heads] ; AX = cylinder, DX = head
mov dh, dl
mov bx, ax ; BX = cylinder
pop ax ; AL = sector
mov cl, al
mov ch, bl ; CH = cylinder bits 0-7
shr bx, 2
and bl, 0xC0
or cl, bl ; CL[7:6] = cylinder bits 8-9
ret
; ---------------------------------------------------------------------------
; load_kernel: read KERNEL_SECTORS sectors starting at LBA 1 to 0x10000.
; Halts with an error message after 3 failed retries per sector.
;
; NOTE: no register value is trusted across int 0x13 — SeaBIOS/real BIOSes
; may clobber general registers (observed: BX zeroed) and disturb the
; stack, so all loop state lives in memory and ES:BX/DS/DL are reloaded
; from memory before every disk call.
load_kernel:
xor ax, ax
mov ds, ax ; re-establish DS (BIOS may clobber it)
mov ax, KERNEL_LOAD_SEG
mov [buf_seg], ax
mov word [buf_off], 0
mov word [lba], 1
mov ax, KERNEL_SECTORS
mov [sectors_left], ax
.next:
mov ax, [lba]
call lba_to_chs
mov byte [tries], 3
.attempt:
mov ax, [buf_seg] ; reload buffer: ES:BX must not survive
mov es, ax ; a BIOS call
mov bx, [buf_off]
mov dl, [boot_drive]
mov ah, 0x02 ; read 1 sector to ES:BX
mov al, 1
int 0x13
xor ax, ax
mov ds, ax ; DS may not have survived either
jnc .ok
mov ah, 0x00 ; reset drive, then retry
mov dl, [boot_drive]
int 0x13
xor ax, ax
mov ds, ax
dec byte [tries]
jnz .attempt
mov si, msg_disk_err
call puts
jmp halt
.ok:
add word [buf_off], 512 ; advance buffer past the sector
jnc .no_carry
add word [buf_seg], 0x1000 ; +64K on segment wrap
.no_carry:
inc word [lba]
dec word [sectors_left]
jnz .next
ret
; ---------------------------------------------------------------------------
; query_e820: ask BIOS for the physical memory map (int 0x15, eax=0xE820,
; edx='SMAP') and stage it at E820_BUF with the entry count byte at
; E820_COUNT. Must run BEFORE the protected-mode switch as BIOS calls are
; unavailable after. Runs in 16-bit real mode at Ring 0; clobbers AX,
; BX, CX, DX, DI, ES.
;
; NOTE: like load_kernel, no register survives int 0x15 — SeaBIOS/real
; BIOSes may clobber general registers across the call, so the
; continuation (EBX) lives in memory ([e820_next]) and ECX/EDX/ES:DI are
; reloaded from immediates before every call. DS is saved on the stack
; across the call (push/pop restores OUR value even if BIOS clobbers it);
; it must NOT be rebuilt with xor ax,ax + mov ds,ax after the call, as
; that zeroes EAX's low half and breaks the 'SMAP' check below. DI is recomputed from
; the in-memory count each pass (index < 64, so the 8-bit multiply
; cannot overflow) instead of keeping a second pointer.
;
; Fallback choice: if no entry was staged (first query failed via carry
; or EAX magic mismatch), synthesize a single usable region at 1MB,
; length 127MB (type 1) — enough for the kernel + PMM to boot on an
; unknown box — and continue silently (no message: the sector has no
; bytes to spare). A mid-list failure keeps whatever entries were
; already staged. Documented here so the PMM knows the map may be
; synthetic on old hardware. Kept golfed on purpose: this sector is
; exactly 510 bytes, every instruction here displaced a boot message.
query_e820:
xor ax, ax
mov ds, ax ; re-establish DS
mov [E820_COUNT], al ; count byte = 0
.loop:
xor ax, ax
mov es, ax ; ES:DI rebuilt per call
mov al, [E820_COUNT]
mov cl, E820_ENTRY_SIZE
mul cl ; AX = index * 24
mov di, E820_BUF
add di, ax
mov ecx, E820_ENTRY_SIZE ; reload every call: BIOS owns nothing
mov edx, 0x534D4150
mov eax, 0xE820
mov ebx, [e820_next] ; continuation lives in memory
push ds ; DS survives on the stack, EAX intact
int 0x15
pop ds ; our DS back, whatever BIOS did
jc .done ; carry = end of list or call failed
cmp eax, 0x534D4150 ; BIOS must echo 'SMAP' in EAX
jne .done
cmp byte [E820_COUNT], E820_MAX ; buffer full: stop, keep what we have
jae .done
inc byte [E820_COUNT] ; entry already written by BIOS at ES:DI
mov [e820_next], ebx ; save continuation across the next call
test ebx, ebx ; EBX=0 ends the list
jnz .loop
.done:
cmp byte [E820_COUNT], 0
jne .ok
xor ax, ax ; fallback: ES:DI may not have survived
mov es, ax
mov di, E820_BUF
mov eax, 0x00100000
stosd ; base = 1MB
xor eax, eax
stosd ; base high = 0
mov eax, 0x07F00000
stosd ; length = 127MB
xor eax, eax
stosd ; length high = 0
inc eax
stosd ; type = 1 (usable)
inc byte [E820_COUNT] ; count was 0, now 1
.ok:
ret
halt:
cli
.hang:
hlt
jmp .hang
; ---------------------------------------------------------------------------
bits 32
prot32:
mov ax, 0x10 ; flat data segment
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x90000
mov eax, 0x10000 ; stage-2 entry at start of kernel image
jmp eax
; ---------------------------------------------------------------------------
bits 16
; Flat GDT: null, 32-bit code, 32-bit data. (64-bit descriptors live in the
; stage-2 image, which brings its own GDT.)
gdt:
dq 0x0000000000000000
dq 0x00CF9A000000FFFF ; code32: base 0, 4GB, exec/read
dq 0x00CF92000000FFFF ; data32: base 0, 4GB, read/write
gdt_end:
gdt_ptr:
dw gdt_end - gdt - 1
dd gdt
msg_loading db "Nadir: load", 0x0D, 0x0A, 0
msg_entering db "Nadir: pmode", 0x0D, 0x0A, 0
msg_disk_err db "Nadir: disk err", 0x0D, 0x0A, 0
boot_drive db 0
spt dw 18
heads dw 2
lba dw 0
buf_seg dw 0 ; load buffer segment (starts 0x1000)
buf_off dw 0 ; load buffer offset within segment
sectors_left dw 0 ; sectors still to load
tries db 0 ; retries left for current sector
e820_next dd 0 ; E820 continuation across int 0x15
; (image-zeroed; first call starts at 0)
times 510-($-$$) db 0
dw 0xAA55