SchOS/.claude/memory/schos-uefi-loader.md
Nexoniarz a1c965da1e include: one folder per subsystem, and includes that name it
Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".

Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 15:05:12 +02:00

6.4 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-09T20:44:02.893Z

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/arch/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.

A "2.2x speedup" was claimed for this change on 2026-08-09. It was wrong, and the retraction matters more than the claim did. The measurement sent a command, slept a fixed 50 s on the host, and then read /proc/uptime — so it timed the sleep, not the command, through a guest clock that drifts under load. Correctly measured (see schos-bootcheck-trust), ls /bin | wc -l is 2.9 s under TCG, 1.1 s under KVM, and the region-based allocator has no measurable effect on it either way. The change is still right; it simply is not a performance change.

UEFI reaches userspace — 2026-08-09

make iso produces a disc that boots under OVMF all the way to an interactive shell, verified by driving it over serial: uname -a, 241 binaries in /bin, write-then-read a file, ps, /proc/schfs. The old cpu0: starting ceiling is gone.

Four bugs stood between here and there. Three were pre-existing and invisible purely because this path had never got far enough to expose them.

  1. virtio-gpu's BAR lands above 4 GB. OVMF puts the 64-bit BAR at about 768 GB; P2V of that has no translation, so the first register write page faulted, then double, then triple, then a reboot loop. Every driver did P2V(bar.base). Fixed by a real ioremap() — see schunix-ioremap-and-bars.
  2. EFER.SCE was never set on the UEFI entry. The 32-bit entry sets it while climbing into long mode; entry64uefi skips that bootstrap because firmware already switched modes, and UEFI has no reason to set SCE. The first sysretq raised #UD in pid 1 at zero ticks, rip in syscall_trapret.
  3. The RSDP is not where a BIOS leaves it. find_rsdp() scanned the EBDA and 0xE0000; OVMF publishes it in the EFI configuration table only. No RSDP means no MADT, no IOAPIC, no IRQ routing — and since serial output is polled, the machine reached a prompt and then ignored every keystroke, which looks like a broken console. The loader now passes it at SCHBOOT_RSDP_ADDR. Do not require 16-byte alignment when validating it: that rule is about scanning the BIOS area, and OVMF's sits at 0x3f77e014.
  4. The ramdisk was a v1 image the kernel could not mount — see schunix-iso-boot.

Still true: the loader prints could not reserve warnings for kernel segments the firmware already owns, and writes there anyway. It has not bitten yet.

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.