| Filename | Latest commit message | Latest commit date |
|---|---|---|
A laptop has no serial port, and the machine we are trying to boot stops with the loader's splash still on screen. Three completely different things look exactly like that: the jump never landed, the kernel started and hung, or the kernel started and panicked with a full report drawn into a framebuffer nobody can see. They call for different next steps, so the kernel now says which without needing a console at all. **A bar, as the kernel's first act.** Eight lines of accent colour across the top of the framebuffer, written straight through the handoff block before a single subsystem exists -- no gfx, no scanout, no allocator, nothing that can itself fail. If it appears over the splash, the jump landed and the kernel is executing. If it does not, nothing below that point is worth looking at. Skipped when the framebuffer is above the direct map, because a fault there leaves the machine quieter still, and free on a boot that works: the console clears the screen a few lines later. **Blinking keyboard lights, on panic.** Scroll, Num and Caps together, about three times a second, for ever. It is the one output every PC has had since before any of this existed -- no driver, no interrupt, no memory -- and it works when the graphics do not. Linux does the same thing for the same reason. Every wait in it is bounded. It runs after `panicked` is set so there is nothing left to protect, but a machine that hangs inside its own death rattle tells you less than one that stops quietly. The delay is microdelay, which spins on the TSC: no timer, no interrupt, no calibration -- all three of which a kernel that died during boot may not have. That makes the three cases distinguishable from across the room: no bar and no blink is a jump that did not land; a bar and no blink is a kernel that hung; a blinking keyboard is a panic, and the report is in /proc/kmsg after a restart. **And the loader now describes the mode it is actually handing over**, not only the one the firmware started in. On a machine where changing the resolution loses the splash entirely, the difference between those two lines is the bug report; on a boot that works nobody sees either, because ClearScreen runs a few lines later. Verified: `echo c > /proc/sysrq`, then QMP polled for five and a half seconds afterwards -- the machine answers throughout and a screendump still shows the panic report intact, so the blink loop runs without faulting, resetting, or scribbling on what it is there to accompany. Both supported configurations boot clean with the early bar in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
| .. | ||
| common | ||
| uefi | ||
| README.md | ||
Booting SchOS
One firmware path:
boot/
uefi/ the loader: a 64-bit PE application
common/ the user interface, shared with nothing else now
There were two. boot/bios/ held an MBR, an El Torito CD stub and a real-mode
stage 2, and it was a separate directory rather than #ifdefs because the two
shared almost nothing: a BIOS loader lives in 16-bit real mode and talks to the
firmware through software interrupts, while UEFI hands a loader long mode with
paging already on and a table of function pointers to call.
That asymmetry is what removed it. Everything since roughly 2012 boots UEFI,
CSM — the firmware-side BIOS emulation that let legacy loaders run at all — has
been going away since Intel dropped support for it in 2020, and the cost of the
second path was not the code but the tax on everything that touched it: stage 2
had a 65-sector ceiling that it silently overflowed, and the resolution menu
could not be shared because VBE mode setting is INT 10h and needs real mode.
The contract below survives it. It was written to let a second loader exist, and it is worth keeping in that shape: it is what the kernel expects, stated once, rather than a description of whoever happens to be calling.
What the loader must hand the kernel
Whichever path runs, the kernel expects the same things. This is the contract; a second loader is correct exactly when it satisfies it.
| Where | Notes | |
|---|---|---|
| Kernel image | loaded per its ELF program headers | entry at entry64 |
| CPU state | see below -- the two paths differ here | |
| Memory map | E820_MAP_ADDR, 24-byte entries, count at E820_COUNT_ADDR |
kernel/arch/x86_64/e820.c reads it |
| Framebuffer | VBE_INFO_ADDR |
address, pitch, width, height, depth |
| Verbose flag | BOOT_VERBOSE_FLAG_ADDR |
set by the F8 menu |
The addresses are defined once, in include/memlayout.h and include/schboot.h.
The kernel does not care which firmware produced them.
The entry point
UEFI hands a loader long mode with paging already on. Dropping back to
protected mode only to climb out again is a mode switch whose failure mode is a
triple fault with no console, so the kernel publishes an entry that takes the
machine as it already is.
kernel/arch/x86_64/entry.S carries a sch64_header — magic SCH64BOO,
8-byte aligned within the first 16 KB, followed by a 64-bit physical address —
found by scanning. include/schboot.h holds the magic so loader and kernel
cannot disagree.
The 32-bit multiboot entry is still in entry.S and now has no caller: the
BIOS loader was the only thing that used it. It is left in place rather than
removed in the same change that removed the loader, because deleting kernel
entry code is its own piece of work with its own way of failing silently.
What that entry expects:
- long mode, interrupts off
- CR3 pointing at tables that identity-map the low 4 GB and map the same
4 GB at
0xffff800000000000.entry64low's first instruction is amovabsto a high address, so identity alone triple faults immediately. - RBX = the framebuffer physical address, and never 1 — that value marks an
application processor in
entry64high.
The kernel loads its own GDT at that entry (entry64uefi), rather than the
loader doing it. A loader arriving in long mode is still on the firmware's
descriptors, and the first thing the kernel does that depends on its own
selectors takes a #GP against a GDT the firmware no longer owns — it
surfaced as unexpected trap 13 ... cs=38, a long way from the cause.
e820.c reads a BIOS memory map. UEFI hands out a different structure
(EFI_MEMORY_DESCRIPTOR, from GetMemoryMap), so the UEFI loader has to
translate it into the same 24-byte entries rather than inventing a second
format for the kernel to learn.
What booting looks like now
make esp stages EFI/BOOT/BOOTX64.EFI and schos/kernel, and QEMU is
pointed at OVMF with that directory as a virtual FAT volume — make qemu.
make iso builds a disc with a single El Torito entry pointing at a FAT image
holding the same two files.
Neither is an installable disk. A real GPT image with a real FAT partition is the installer's job, and until it exists there is nothing here that writes to a disk a machine could boot from unaided.
Secure Boot is not addressed either: the loader is unsigned, so a machine enforcing it will refuse to run this.