boot: the BIOS loader is gone

1449 lines of boot/bios/, tools/sign.pl and tools/padstage2.pl, plus the
build rules, the disk image and the ISO entry that existed to carry them.

Everything since roughly 2012 boots UEFI, and CSM -- the firmware-side
BIOS emulation that lets a legacy loader run on such a machine at all --
has been going away since Intel dropped support for it in 2020. So this
is not a path being retired early; it is one whose remaining hardware is
already the exception.

The cost was never the code, it was the tax on everything near it. Stage
2 had a 65-sector ceiling it overflowed silently -- 25440 bytes against
24576 -- and because the kernel is written at sector 1 + STAGE2_SECTORS,
an overflowing loader was overwritten by the very kernel it was loading.
The resolution list could not be shared with UEFI either: VBE mode
setting is INT 10h, so matching it needed unreal mode, which is the
riskiest code in the tree for a feature that already worked elsewhere.

What replaces schos.img is the staged ESP, handed to QEMU as a virtual
FAT volume. That is a development arrangement and not an installable
disk; a real GPT image with a real FAT partition is the installer's job,
and until it exists nothing here writes a disk a machine boots unaided.
Secure Boot is untouched too -- the loader is unsigned.

The ISO keeps one El Torito entry, the FAT image holding loader and
kernel. It never booted from a USB stick via BIOS anyway: the firmware
presents the stick as a hard disk while stage2main.c read the kernel back
over ATAPI.

The 32-bit multiboot entry in entry.S now has no caller and is left in
place. Deleting kernel entry code is its own change with its own way of
failing silently, and it should not ride along with this one.

Ported with it: run.ps1, the six screenshot tools, and schcom_test.sh,
each with its own OVMF varstore copy so no run inherits another's UEFI
variables.

Two things this shook out. OVMF now belongs in `all`: nothing boots
without firmware, `make clean` wipes build/ including the copy fetched
from the host, and a clean followed by a build left every run script
throwing. And it cannot be listed as a prerequisite at the top of the
file -- make expands prerequisites when it reads the line, and
$(OVMF_CODE) is defined further down, so naming it there appends nothing
and says nothing.

Verified from a clean tree: 110 [ok] and no failures on the full suite,
the ISO boots with no hard disk attached, schfs-check passes, schcom's
end-to-end pixel test passes, and fbshot captures a real 1280x800 frame.
This commit is contained in:
Nexoniarz 2026-08-11 10:20:28 +02:00
commit 651899220c
23 changed files with 317 additions and 1826 deletions

155
Makefile
View file

@ -186,7 +186,6 @@ ifdef USE_CLANG
# object is silently the wrong format and the first thing to complain is the
# linker.
CC = clang --target=x86_64-elf
CC32 = clang --target=i386-elf
LD = ld.lld
OBJCOPY = llvm-objcopy
OBJDUMP = llvm-objdump
@ -221,7 +220,6 @@ else
TOOLPREFIX ?=
endif
CC = $(TOOLPREFIX)gcc
CC32 = $(CC) -m32
LD = $(TOOLPREFIX)ld
OBJCOPY = $(TOOLPREFIX)objcopy
OBJDUMP = $(TOOLPREFIX)objdump
@ -363,16 +361,24 @@ $(shell mkdir -p $(BUILD); \
# usbstick.img is built here rather than on demand because bootcheck -Usb
# cannot start QEMU without it, and a missing test image looks exactly like a
# broken USB driver: QEMU refuses the -drive and the whole run produces nothing.
all: schos.img $(BUILD)/usbstick.img
# OVMF is in this list because it is no longer optional: with the BIOS loader
# gone, nothing in this tree boots without firmware, and `make clean` wipes
# build/ including the copy fetched from the host. Leaving it out meant a clean
# followed by a build produced a tree where every run script threw.
all: esp $(BUILD)/schfs.img $(BUILD)/usbstick.img
# ...and the firmware, added to `all` further down. It cannot be listed here:
# make expands a rule's prerequisites when it reads the line, and $(OVMF_CODE)
# is defined below this point, so naming it here appends nothing at all and
# does so silently.
schos.img: $(BUILD)/boot/bootblock $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel $(BUILD)/schfs.img
dd if=/dev/zero of=schos.img count=10000
dd if=$(BUILD)/boot/bootblock of=schos.img conv=notrunc
dd if=$(BUILD)/boot/stage2 of=schos.img seek=1 conv=notrunc
# seek is 1 + STAGE2_SECTORS, not a literal. It was 25 against a 24-sector
# stage2; raising STAGE2_SECTORS for the boot logo without touching it wrote
# the kernel straight over stage2's own tail.
dd if=$(BUILD)/kernel/kernel of=schos.img seek=$$(( 1 + $(STAGE2_SECTORS) )) conv=notrunc
# schos.img is gone with the BIOS loader that gave it its shape. It was a raw
# disk with the boot sector at LBA 0, stage2 next and the kernel at a fixed
# offset -- a layout only a loader that seeks by sector number can use. UEFI
# firmware wants a filesystem, so what boots now is the staged ESP, offered to
# QEMU through its virtual FAT.
#
# That is a development arrangement, not an installable disk. A real GPT image
# with a real FAT partition is the installer's job.
kernel: $(BUILD)/kernel/kernel
@ -451,6 +457,11 @@ $(OVMF_VARS):
cp "$(OVMF_VARS_SRC)" $@
chmod u+w $@
ovmf: $(OVMF_CODE) $(OVMF_VARS)
# The other half of `all`, now that OVMF_CODE and OVMF_VARS have values.
all: $(OVMF_CODE) $(OVMF_VARS)
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) $(QEMU_INPUT)
@ -506,12 +517,10 @@ iso:
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
$(MAKE) RAMROOT=1 schos.iso
# The UEFI loader is a dependency now: the disc carries a second El Torito
# entry pointing at a FAT image that holds it, so one disc boots both firmwares.
# One El Torito entry, pointing at a FAT image holding the loader and kernel.
# sysroot-install because the disc also carries the tree as a real hierarchy.
schos.iso: $(BUILD)/boot/cdboot $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel \
$(BUILD)/uefi/BOOTX64.EFI sysroot-install
STAGE2_SECTORS=$(STAGE2_SECTORS) SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@
schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI sysroot-install
SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@
# Boot the ISO the way a machine booting off a disc would: no hard disk at
# all, so a kernel that still secretly depends on one fails here rather than
@ -521,71 +530,6 @@ qemu-iso: schos.iso
-cdrom schos.iso -boot d \
-m 1G -smp 2 -machine pc-i440fx-9.2
# -----------------------------------------------------------------------
# Bootloader — two stages. Stage 1 (bootblock) is the actual 512-byte MBR
# sector: BIOS-imposed hard limit, so it only loads stage 2 off disk and
# jumps to it. Stage 2 does VBE/A20/protected-mode setup, draws the boot
# splash, and loads the kernel — not size-constrained the same way, just
# padded out to a fixed STAGE2_SECTORS*512 bytes so the kernel's starting
# sector on disk is predictable (kept in sync with STAGE2_SECTORS in
# boot/bios/bootasm.S and boot/bios/stage2main.c).
# -----------------------------------------------------------------------
# STAGE2_SECTORS used to be written out three times -- here, in boot/bios/bootasm.S
# and in boot/bios/stage2main.c -- and docs/layout.md carried a warning about keeping
# them in step. They are now all fed from this one variable via -D, so the
# warning describes a problem that can no longer happen. Both C/asm files keep
# an #ifndef fallback purely so they still read sensibly on their own.
#
# The ceiling is memory, not disk: stage2 is loaded at 0x7E00 and the multiboot
# kernel-search buffer begins at 0x10000, so 65 sectors is the hard maximum.
# stage2main.c static-asserts this. padstage2.pl fails the build if the compiled
# binary overflows the budget, rather than truncating it -- a short stage2 would
# shift the kernel's start sector and fail in a way that looks nothing like its
# cause.
# 64, not 48. stage2 carries the shared UI now -- which means the 8x16 font and
# the logo mask -- and it is 64-bit code since the long-mode climb moved here.
# At 48 it came to 25440 bytes against a 24576 limit and the build did not stop,
# which is worse than the overflow: the kernel is written at sector
# 1 + STAGE2_SECTORS, so a stage2 that runs past its allocation is overwritten
# by the very kernel it is loading.
STAGE2_SECTORS = 64
STAGE2_DEF = -DSTAGE2_SECTORS=$(STAGE2_SECTORS)
# Byte budget is computed in the recipe with shell arithmetic, not $(shell expr
# ... \* 512): make hands the backslash through untouched, sh then treats the
# `*` as a glob, and expr gets a directory listing. It "worked" to the extent
# that make carried on and padstage2.pl received nonsense.
STAGE2_BYTES = $$(( $(STAGE2_SECTORS) * 512 ))
$(BUILD)/boot/bootblock: boot/bios/bootasm.S
@mkdir -p $(BUILD)/boot
$(CC32) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/bootasm.S -o $(BUILD)/boot/bootasm.o
$(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e start -Ttext 0x7C00 \
-o $(BUILD)/boot/bootblocktmp.o $(BUILD)/boot/bootasm.o
$(OBJCOPY) -S -O binary -j .text $(BUILD)/boot/bootblocktmp.o $(BUILD)/boot/bootblock
./tools/sign.pl $(BUILD)/boot/bootblock
# Stage 1 for optical media. Same job as bootblock, far less of it: El Torito
# has already loaded stage2 alongside this sector by the time it runs, so it
# only records that we came from a CD and jumps. See boot/bios/cdboot.S.
$(BUILD)/boot/cdboot: boot/bios/cdboot.S
@mkdir -p $(BUILD)/boot
$(CC32) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/cdboot.S -o $(BUILD)/boot/cdbootasm.o
$(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e start -Ttext 0x7C00 \
-o $(BUILD)/boot/cdboottmp.o $(BUILD)/boot/cdbootasm.o
$(OBJCOPY) -S -O binary -j .text $(BUILD)/boot/cdboottmp.o $@
./tools/sign.pl $@
$(BUILD)/boot/stage2: boot/bios/stage2.S boot/bios/stage2main.c include/logo.h \
boot/common/ui.c boot/common/ui.h
@mkdir -p $(BUILD)/boot
$(CC) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/stage2.S -o $(BUILD)/boot/stage2asm.o
$(CC) $(STAGE2_DEF) -fno-builtin -fno-pic -O -nostdinc -mno-red-zone -mno-sse -mcmodel=small -I./include -c boot/bios/stage2main.c -o $(BUILD)/boot/stage2main.o
$(CC) -fno-builtin -fno-pic -O -nostdinc -mno-red-zone -mno-sse -mcmodel=small -I./include -c boot/common/ui.c -o $(BUILD)/boot/ui.o
$(LD) -m elf_x86_64 -nostdlib $(LD_LOWBASE) -n -N -e stage2start -Ttext 0x7E00 \
-o $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2asm.o $(BUILD)/boot/stage2main.o $(BUILD)/boot/ui.o
$(OBJCOPY) -S -O binary -j .text -j .rodata -j .data $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2
./tools/padstage2.pl $(BUILD)/boot/stage2 $(STAGE2_BYTES)
# -----------------------------------------------------------------------
# Kernel binary
# -----------------------------------------------------------------------
@ -912,9 +856,10 @@ $(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp
@echo "Creating SchOS File System Image..."
@# A running QEMU holds this file open, and on Windows that makes rm fail
@# with "Device or resource busy" -- which stops the build *after* the
@# kernel has already relinked, leaving schos.img stale. The next test run
@# then boots the previous kernel and the change appears to have done
@# nothing, which has cost several debugging cycles. Say so plainly.
@# kernel has already relinked, leaving the staged ESP holding a kernel
@# from the previous build. The next test run then boots that one and the
@# change appears to have done nothing, which has cost several debugging
@# cycles. Say so plainly.
@rm -f $@ || { \
echo ""; \
echo " $@ is in use -- a QEMU is still holding it."; \
@ -943,18 +888,26 @@ $(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp
# framebuffer and asserts the pixels on the host. That last part is the whole
# value: the compositor saying "I composited it" proves nothing, and a
# screenshot nobody inspects proves less. Needs `make projects` first.
schcom-test: schos.img $(BUILD)/schfs.img
schcom-test: esp $(BUILD)/schfs.img
sh tools/posix/schcom_test.sh
qemu-kvm: schos.img $(BUILD)/schfs.img
$(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu host \
-hda schos.img -hdb $(BUILD)/schfs.img \
-m 1G -smp 2 -machine pc-i440fx-9.2
# The firmware and the disks, in one place. Every run target below boots the
# same way, because there is only one way to boot now: OVMF, the staged ESP as
# the first disk, the SchFS volume as the second. Keeping the argument list in
# a variable is not tidiness -- four copies of a pflash pair is how one of them
# ends up with a writable firmware image and nobody notices.
OVMF_DRIVES = -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
qemu: schos.img $(BUILD)/schfs.img
qemu-kvm: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu host \
$(OVMF_DRIVES) -m 1G -smp 2 -machine pc-i440fx-9.2
qemu: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu qemu64,+rdtscp \
-hda schos.img -hdb $(BUILD)/schfs.img \
-m 1G -smp 2 -machine pc-i440fx-9.2
$(OVMF_DRIVES) -m 1G -smp 2 -machine pc-i440fx-9.2
# The root filesystem. Built from the staged sysroot; the kernel mounts it as
# / in forkret().
@ -972,18 +925,18 @@ $(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
# the live v1 root and the SchFS probe correctly finds nothing there.
#
# Once up: cat /proc/schfs
qemu-schfs: schos.img $(BUILD)/schfs.img
qemu-schfs: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) -cpu qemu64,+rdtscp \
-hda schos.img -hdb $(BUILD)/schfs.img \
-m 1G -smp 2 -machine pc-i440fx-9.2
$(OVMF_DRIVES) -m 1G -smp 2 -machine pc-i440fx-9.2
# -----------------------------------------------------------------------
# Misc
# -----------------------------------------------------------------------
# schos.img is deliberately NOT here. It is a real file with real
# prerequisites; listing it as phony made make skip the timestamp check and
# re-run the four dd commands -- including zeroing five megabytes -- on every
# single invocation, including ones where nothing had changed.
# Nothing that is a real file with real prerequisites belongs here. schos.img
# used to be listed, and being phony made make skip the timestamp check and
# re-run its four dd commands -- including zeroing five megabytes -- on every
# single invocation, including ones where nothing had changed. The file is gone
# with the BIOS loader, but the rule it taught applies to whatever replaces it.
# include/logo.h is generated but committed, exactly as include/font.h is: it
# derives from art that changes about never, and making it a build dependency
# would put Python on the critical path of every clean build for no benefit.
@ -1053,13 +1006,13 @@ qrcheck: $(QRTEST)
.PHONY: all kernel ramroot cmd ports projects sysroot-install qemu qemu-kvm schcom-test \
schfs-test schfs-objtest schfs-image-test schfs-check mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
logo qrtest qrcheck crashqr usb-ids
logo qrtest qrcheck crashqr usb-ids ovmf esp uefi
.PRECIOUS: $(BUILD)/%.o
# 'clean' removes generated output only. dist/ (upstream tarballs) survives,
# so a rebuild never needs the network.
clean:
rm -rf $(BUILD) schos.img schos.iso kernel/arch/x86_64/vectors.S
rm -rf $(BUILD) schos.iso kernel/arch/x86_64/vectors.S
distclean: clean
rm -rf dist