SchOS/.claude/memory/schos-uefi-loader.md
2026-08-09 17:13:44 +02:00

3.6 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
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.