From d41a0b16180b834ca427be500fdf85aa4b585ecd Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 9 Aug 2026 17:13:44 +0200 Subject: [PATCH] memory: the UEFI loader --- .claude/memory/MEMORY.md | 1 + .claude/memory/schos-uefi-loader.md | 74 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 .claude/memory/schos-uefi-loader.md diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 83dbe56..a73b732 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -5,6 +5,7 @@ - [Trusting bootcheck](schos-bootcheck-trust.md) - it silently stopped running; count the [ok] lines, never just grep FAIL. - [schinit](schos-schinit.md) - process 1 is launchd-shaped; job manifests, and why setsid() cost the shell its terminal. - [BIOS/UEFI split](schos-boot-split.md) - boot/bios and boot/uefi, and the hand-off contract a second loader must satisfy. +- [UEFI loader](schos-uefi-loader.md) - it boots under OVMF; the SCH64 entry, the GDT trap, and the open e820 fragmentation bug. - [Schunix build environment](schunix-build-environment.md) — builds natively on Windows with clang/lld + mingw + Git Bash; WSL is gone, and the five latent bugs the move exposed. - [Syscalls: one table](schunix-syscall-single-table.md) — declare in kernel/sys/table.inc only; linux_abi.c is generated, never hand-edited. - [Linux ABI decision](schunix-linux-abi-decision.md) — the native syscall ABI is being dropped; Linux numbering becomes the only ABI. diff --git a/.claude/memory/schos-uefi-loader.md b/.claude/memory/schos-uefi-loader.md new file mode 100644 index 0000000..63a1f5c --- /dev/null +++ b/.claude/memory/schos-uefi-loader.md @@ -0,0 +1,74 @@ +--- +name: schos-uefi-loader +description: boot/uefi works under OVMF; the SCH64 entry, the GDT trap, and the one open bug (e820 fragmentation) +metadata: + node_type: project +--- + +**`boot/uefi/` boots.** `make uefi`, `make esp`, `make qemu-uefi`. Verified +under OVMF 2026-08-09: kernel loaded, PCI/VGA/e1000/RTC up, reaches +`cpu0: starting`. + +Written from the UEFI specification, not gnu-efi or EDK II — a large dependency +for ~200 lines of use, and a licensing question ([[schunix-licensing]]) the +project does not need. clang emits PE/COFF directly +(`--target=x86_64-unknown-windows`, `lld-link -subsystem:efi_application`), so +**no new toolchain**. `-mno-red-zone` is mandatory: firmware can interrupt at +any point and would clobber the red zone. + +## The second kernel entry + +The multiboot entry is **32-bit** code that climbs into long mode itself. UEFI +hands over long mode already, and dropping back down to climb out again is a +mode switch whose failure mode is a triple fault with no console. So entry.S +publishes a 64-bit entry the same way it publishes the multiboot header: magic +`SCH64BOO`, 8-byte aligned in the first 16 KB, then a 64-bit physical address. +`include/schboot.h` owns the constants so loader and kernel cannot disagree — +the first attempt defined them twice and searched for `SCH6` while the +assembler had written `6HCS`. + +**Put that header *after* the multiboot header, never before.** `header_addr` +claims the multiboot header sits at `mboot_load_addr`, and the BIOS loader +computes the kernel's file offset from `(header_addr - load_addr)`. Bytes +inserted ahead of it load the kernel shifted, which fails as a triple fault. + +Entry expects: long mode, interrupts off; CR3 mapping the low 4 GB **and** the +same 4 GB at `0xffff800000000000` (entry64low's first instruction is a movabs +to a high address — identity alone triple faults); RBX = framebuffer physical +address, never 1 (that marks an AP). + +**The kernel loads its own GDT there (`entry64uefi`), not the loader.** A loader +arriving in long mode is still on the firmware's descriptors; the first thing +needing the kernel's own selectors faulted with `unexpected trap 13 ... cs=38`, +a long way from the cause. + +## Open bug: the memory map + +`e820_init` takes the usable run **containing the kernel**. Under OVMF that run +ends at 8 MB — the firmware places an ACPI NVS region at 0x800000, so it is +genuinely fragmented. The loader's map is truthful and untruncated (21 entries); +the one-contiguous-run assumption is BIOS-shaped. + +**Fix: have the kernel free each usable region rather than one range.** That +touches `kalloc`/`freerange` and `kinit1`, which is the most safety-critical +code in the tree — it deserves its own change with both boot paths verified, +not the tail of a session. + +## Testing + +QEMU ships the firmware: `share/edk2-x86_64-code.fd` plus +`edk2-i386-vars.fd` as the varstore, copied because it must be **writable** +(a copy on Windows inherits the read-only attribute — "Block node is +read-only"). + +- **i440fx, not q35.** The kernel's IDE driver speaks the legacy ports at + 0x1f0; q35 has only AHCI, so the root volume would not be there at all. +- `-drive file=fat:rw:build/esp` serves a directory as FAT, so no mtools is + needed. **Do not add `format=raw`** — it conflicts with the `fat:` driver and + fails with "Block node is read-only". +- OVMF drops to its shell rather than auto-booting, so the ESP carries + `startup.nsh`. It is a **file** (`boot/uefi/startup.nsh`), not generated by + `printf`: printf reads `\E` as ESC and silently corrupts the path. + +Still to do: a real FAT ESP image for `make iso`, so the ISO carries an El +Torito EFI entry beside the BIOS one.