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
122 lines
6.4 KiB
Markdown
122 lines
6.4 KiB
Markdown
---
|
|
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: memory
|
|
originSessionId: d4821aa9-2e49-40da-91e7-90ef9ed9997e
|
|
modified: 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.
|