SchOS/.claude/memory/schos-uefi-loader.md
Nexoniarz c55e65de5e memory: the e820 work, and why the extended suite flaps
Records the measured effect of the region-based allocator (2.2x on a cold
ls, 6 s off the boot), the two pre-existing UEFI blockers found while
verifying it, and the three confounds that each produced a confident
wrong answer about guest speed in one session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:05:34 +02:00

5.3 KiB

name description metadata
schos-uefi-loader boot/uefi works under OVMF; the SCH64 entry, the GDT trap, and the one open bug (e820 fragmentation)
node_type originSessionId modified
memory d4821aa9-2e49-40da-91e7-90ef9ed9997e 2026-08-09T19:05:15.325Z

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.

The memory map — fixed 2026-08-09

e820_init used to take the usable run containing the kernel, which under OVMF ends at 8 MB because the firmware puts an ACPI NVS region at 0x800000. That failed the PHYSTOP_MIN check, so the kernel fell back to asserting 224 MB it had not measured and then freed straight through the reserved hole.

The kernel now keeps the usable regions as a list (mem_usable[] / nmem_usable in memlayout.h) as well as a top address, and kinit1 frees each one clipped to the window main() passes in. PHYSTOP still means exactly what it did, so no call site changed. phystop comes from the largest region, not the highest, because the DMA arena is carved off the top of it and must be contiguous; a smaller region above it is deliberately left unfreed rather than raising the bound past a hole.

Result: 1018 MB in 7 regions under OVMF, 1023 MB in 2 under BIOS.

It is also a large speedup, which was not the goal and is worth knowing. Measured from inside the guest (/proc/uptime, fresh image each boot, alternating rounds): boot to prompt 14.8 s vs 21.2 s, and a cold ls /bin | wc -l 23.6 s vs 51.2 s — 2.2x. Both rounds agreed within 0.3 s. So if the allocator ever looks slow again, this is the baseline to compare against.

What still stops a full UEFI boot

Two separate, pre-existing problems — neither is the memory map, and both predate the e820 work (the unfixed kernel triple faults in the same place):

  1. virtio-gpu's BAR lands above 4 GB. OVMF assigns the 64-bit BAR high; the first MMIO write faults with CR2 around 0xffff80c000000014, which is P2V of physical 768 GB, far outside the 4 GB direct map kvmalloc builds from four 1 GB pages. Then a double fault, then a triple fault and a reboot loop. SeaBIOS assigns it low, so the BIOS path never sees it. Drop -device virtio-gpu-pci to boot UEFI past this.
  2. Boot stalls after cpu0: starting, before schfs: mounted. Under UEFI the ESP is hd0 and schfs.img is hd1; the root never mounts. Not diagnosed.

cpu0: starting remains the UEFI high-water mark, now reached with a truthful memory map rather than a fabricated one.

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.