boot/uefi: a working UEFI loader, with one known limitation

Loads the kernel from the ESP, places its ELF segments, records the
framebuffer, translates the UEFI memory map into E820 entries, exits boot
services, builds the page tables the kernel expects and jumps. Verified
under OVMF: the kernel starts, brings up PCI, VGA, e1000 and the RTC, and
reaches "cpu0: starting".

UEFI structures are written from the specification rather than importing
gnu-efi or EDK II -- a large dependency for two hundred lines of use, and
a licensing question the project does not need to answer. clang emits
PE/COFF directly, so this needs no new toolchain.

The kernel gained a second entry point. The multiboot entry is 32-bit
code that climbs into long mode itself, and firmware that already put us
there would have to drop back down only to climb out again -- a mode
switch whose failure mode is a triple fault with no console. So entry.S
publishes its 64-bit entry the same way it publishes the multiboot
header: a magic to scan for, SCH64BOO, with include/schboot.h owning the
constants so the two cannot disagree. That header goes *after* the
multiboot header, never before: header_addr says the multiboot header
lives at mboot_load_addr, and the BIOS loader computes the kernel's file
offset from it.

The kernel loads its own GDT at that entry rather than the loader doing
it. Arriving in long mode means arriving on the firmware's descriptors,
and the first thing depending on the kernel's own selectors faulted --
"unexpected trap 13 ... cs=38", a long way from the cause.

KNOWN LIMITATION, diagnosed and not yet fixed: e820_init takes the usable
run *containing the kernel*, which under OVMF ends at 8 MB -- the
firmware places an ACPI NVS region at 0x800000, so the run is genuinely
fragmented. The map is truthful and untruncated (21 entries); the
assumption of one contiguous run is BIOS-shaped. The fix is for the
kernel to free each usable region rather than one range, which touches
kalloc and deserves its own change rather than the tail of this one.

BIOS boot is unaffected: 27 checks, no failures.
This commit is contained in:
Nexoniarz 2026-08-09 17:13:09 +02:00
commit 56139bd0e6
7 changed files with 874 additions and 1 deletions

View file

@ -341,6 +341,63 @@ schos.img: $(BUILD)/boot/bootblock $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel $
kernel: $(BUILD)/kernel/kernel
# -----------------------------------------------------------------------
# UEFI loader
#
# A PE/COFF executable, not ELF, so it needs a different target and a
# different linker driver -- but the same clang install. That is the whole
# reason the toolchain is clang: one compiler reaches every target here.
#
# -mno-red-zone is not optional. UEFI firmware may take an interrupt at any
# point, and the red zone is memory below RSP that a handler is entitled to
# clobber; leaving it enabled corrupts locals at random.
# -fshort-wchar makes L"..." UTF-16, which is the only string type UEFI has.
# -----------------------------------------------------------------------
UEFI_CFLAGS = --target=x86_64-unknown-windows -ffreestanding -fshort-wchar -mno-red-zone -fno-stack-protector -Wall -Wextra -O1 -I./include
$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/schboot.h
@mkdir -p $(BUILD)/uefi
clang $(UEFI_CFLAGS) -c boot/uefi/main.c -o $(BUILD)/uefi/main.obj
lld-link -subsystem:efi_application -entry:EfiMain -nodefaultlib $(BUILD)/uefi/main.obj -out:$@
uefi: $(BUILD)/uefi/BOOTX64.EFI
# The EFI System Partition, as a directory. QEMU serves a directory as FAT
# with `-drive file=fat:rw:`, which is enough to boot and test without any
# image-building tools -- mtools is not present on every supported host.
# A real ESP image for `make iso` is still to come.
$(BUILD)/esp/EFI/BOOT/BOOTX64.EFI: $(BUILD)/uefi/BOOTX64.EFI $(BUILD)/kernel/kernel
@mkdir -p $(BUILD)/esp/EFI/BOOT $(BUILD)/esp/schos
@cp $(BUILD)/uefi/BOOTX64.EFI $(BUILD)/esp/EFI/BOOT/BOOTX64.EFI
@cp $(BUILD)/kernel/kernel $(BUILD)/esp/schos/kernel
@cp boot/uefi/startup.nsh $(BUILD)/esp/startup.nsh
@echo "ESP staged in $(BUILD)/esp"
esp: $(BUILD)/esp/EFI/BOOT/BOOTX64.EFI
# Boot the UEFI path under OVMF. The firmware ships with QEMU; the varstore
# is copied because it must be writable.
#
# i440fx, not q35: the kernel's IDE driver speaks to the legacy ports at
# 0x1f0, and q35 has no legacy IDE at all -- only AHCI. The root volume would
# simply not be there.
OVMF_CODE = $(BUILD)/OVMF_CODE.fd
OVMF_VARS = $(BUILD)/OVMF_VARS.fd
QEMU_SHARE = $(dir $(shell command -v $(QEMU)))share
$(OVMF_CODE):
@mkdir -p $(BUILD)
cp "$(QEMU_SHARE)/edk2-x86_64-code.fd" $@
chmod u+w $@
$(OVMF_VARS):
@mkdir -p $(BUILD)
cp "$(QEMU_SHARE)/edk2-i386-vars.fd" $@
chmod u+w $@
qemu-uefi: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -machine pc-i440fx-9.2 -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk -drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk -m 1G -smp 1 -cpu qemu64,+rdtscp -serial mon:stdio $(QEMU_DISPLAY)
# Build the ramdisk-root variant. This is a target rather than just
# "make RAMROOT=1" because toggling RAMROOT changes CFLAGS (-DRAMDISK_ROOT),
# and make has no way to notice that: objects compiled for one setting would be