SchOS/docs/architecture/boot.md
Nexoniarz 3c5212a7c7 panic: blink the keyboard lights, and mark the screen on the way in
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>
2026-09-24 15:46:38 +02:00

545 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Boot
Firmware to the first shell prompt. This is the shortest path through the whole
system, which makes it the best thing to read first.
The loader's side of the hand-off is specified in
[`boot/README.md`](../../boot/README.md) — that file is the contract, and this
one is the narrative.
---
## 1. Firmware
**UEFI only.** There was a BIOS path and it was removed: everything since
roughly 2012 boots UEFI, and CSM — the firmware-side BIOS emulation that let
legacy loaders run — has been going away since 2020. The cost of keeping it was
not the code but the tax on everything it touched, and the asymmetry is total. A
BIOS loader lives in 16-bit real mode and calls the firmware through software
interrupts; UEFI hands a loader long mode with paging already on and a table of
function pointers.
The loader is `boot/uefi/`, a 64-bit PE application built by the same clang
install as everything else (`--target=x86_64-unknown-windows`). It imports
neither gnu-efi nor EDK II — the handful of structures it uses were written from
the specification, which is both a smaller dependency and no licence question.
## 2. The loader
Six steps, in this order, and the order is load-bearing:
1. Find the kernel on the volume this image was loaded from — `\schos\kernel`,
or `\kernel`.
2. Load its ELF `PT_LOAD` segments to their physical addresses.
3. Record the framebuffer geometry from the Graphics Output Protocol.
4. Translate `GetMemoryMap()` into the 24-byte E820-shaped entries
`kernel/arch/x86_64/e820.c` already parses — not a second format for the
kernel to learn.
5. `ExitBootServices`.
6. Build the page tables the kernel's 64-bit entry expects, and jump.
**Nothing allocates after step 5 and nothing calls a firmware service after it,
because there are none.** Everything the jump needs is computed before.
### The hand-off block
One page of low memory at `0x30000`, defined once in
[`include/schboot.h`](../../include/schboot.h) and included by both the loader
and `entry.S` so the two cannot disagree.
| Offset | What |
|---|---|
| `+0x000` | VBE-shaped mode info: LFB address, stride, width, height, depth |
| `+0x100` | Verbose-boot flag |
| `+0x101` | Boot medium (`1` = CD; `0` = not stated) |
| `+0x200` | E820 entry count, then up to 40 × 24-byte entries |
| `+0x600` | ACPI RSDP physical address, or 0 |
Two of these exist because of specific failures worth knowing about.
**The RSDP is passed explicitly** because the kernel's own search scans the EBDA
and `0xE0000`–`0x100000`, which is a BIOS convention. UEFI publishes the RSDP in
its configuration table and is under no obligation to leave a copy anywhere;
OVMF does not. Without it the kernel finds no MADT, so no IOAPIC, so no
interrupt routing — and the machine boots to a prompt on polled serial output
and then ignores every keystroke, which looks like a broken console rather than
a missing table.
**The E820 cap is 40 entries, not 32.** Real UEFI firmware produces far more
descriptors than QEMU's, and the space was already there.
### Choosing a display, on a machine that has more than one
`LocateProtocol` answers with *a* GOP, and the specification says nothing about
which. On QEMU with a stdvga and a virtio-gpu there are **three** handles, and
two of them report `PixelBltOnly` with a framebuffer address of zero:
```
schos: 3 display adapter(s)
gop: 1280x800 BGRX scanline 1280 fb 0x0000000080000000 size 0x3e8000
gop: 1280x800 Blt-only scanline 1280 fb 0x0000000000000000 size 0x0
gop: 1280x800 Blt-only scanline 1280 fb 0x0000000000000000 size 0x0
schos: display 1280x800 BGRX scanline 1280 fb 0x0000000080000000 size 0x3e8000
```
A laptop with switchable graphics is the same shape: an integrated adapter, a
discrete one, and often a console splitter over both. Taking the first handle
means a one-in-three chance of drawing into an adapter with no aperture — a
black screen with the firmware's own cursor still blinking on it, which looks
exactly like a loader that crashed.
So every handle is examined and scored: an adapter with no framebuffer address
is not a candidate at all, one whose current mode is readable beats one that
would need changing, and size breaks the tie. The report above is printed to
the firmware's text console, which is cleared a few lines later before the
splash — so on a boot that works nobody sees it, and on a boot that dies before
the splash it is the only thing on the screen.
### A framebuffer address is sixty-four bits
The loader used to hand over `(UINT32)gop->Mode->FrameBufferBase`, and the
kernel used to read a 32-bit field out of a *VBE mode-info block* — a BIOS
structure the UEFI path had been filling in because the kernel already knew how
to read one.
Any machine that puts its aperture above 4 GB — which is ordinary on a laptop
with a discrete GPU — got a truncated address that pointed at nothing. The
splash was drawn into it, the kernel was handed it, and the screen kept the
firmware's cursor. There is no error anywhere in that sequence; every step
succeeded on a number that was wrong.
`SCHBOOT_FB_BASE` is the fix: the loader's own display block, with a 64-bit
address, a 64-bit size, 32-bit width, height and stride, and the pixel format.
The VBE block is still written for the BIOS loader and for anything that has
not been taught to look at the new one, but anything that does not fit is left
at **zero** rather than wrapped — a truncated address is the bug above, and a
zero is at least honest about it.
The kernel then has a second problem the loader does not: it runs on its own
page tables, and its direct map covers the low 4 GB. A framebuffer above that
needs `ioremap()`, which needs `kpdpt`, which `kvmalloc()` has not allocated
when the early console is set up. So a low framebuffer is attached exactly as
before, and a high one is attached a few dozen instructions later, immediately
after `kvmalloc()` — still thousands of lines before anything else happens.
### Two signals for a machine that cannot speak
A laptop has no serial port. When it stops with the loader's splash still on
screen, three very different things look identical: 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. Each calls for a different next
step, so the kernel now produces two signals that need no 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. If it appears over
the loader's splash, the jump landed and the kernel is executing; if it does
not, nothing below that point is worth looking at. It is skipped when the
framebuffer is above the direct map, because a fault there would leave the
machine quieter still, and it costs nothing on a boot that works: the console
clears the screen a few lines later.
**Blinking keyboard lights, on panic.** `panic_blink()` toggles 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, so no loop in there can be entered
that cannot be left. The delay is `microdelay`, which spins on the TSC and
needs no timer, no interrupt and no calibration — all three of which a kernel
that died during boot may not have.
So the three cases separate:
| What you see | What happened |
| --- | --- |
| No bar, no blink | The jump did not land. The loader's last label says where. |
| Bar, no blink | The kernel is executing and hung. Not a panic. |
| Blinking lights | The kernel panicked. The report is in `/proc/kmsg` after a restart. |
### The loader says which stage it is in
After the splash, `print()` goes to the serial port and nowhere else — writing
text into the framebuffer would scribble it across the splash. On a laptop with
no serial port that means the last thing on screen is a progress bar, and a
loader that stops has no way to say where.
So every stage after the kernel load names itself under the bar:
*Reading the console font*, *Reading the memory map*, *Leaving the firmware*.
On a machine that works these are invisible — they take microseconds — and on
one that stops, the label is the last thing on the screen and names the call
that did not come back. It costs three strings.
Past `ExitBootServices` there is nothing left to report with, so the final
label stands for the whole of the jump into the kernel.
### Mirroring is a nicety, and booting is not
`mirrors_show()` copies the primary's framebuffer onto every other display
adapter with `Blt`. Three rules keep a cosmetic feature from being able to stop
a machine:
**Modes are matched once, at discovery, before anything is drawn.** It used to
be done inside `mirrors_show()` — which is to say on every menu redraw and
every segment of the kernel load. Setting a mode is not a cheap call on real
hardware: it can take the best part of a second, it can blank a panel while it
happens, and on a laptop the "other" adapter is quite often the *same* panel
reached through a second GOP. Doing that repeatedly, in the middle of drawing a
progress bar, produces a machine showing a background colour and nothing else
while the loader waits on a firmware call.
**One refusal turns it off for the boot.** A `Blt` that fails once will fail
every time, and retrying it on every screen is how a nicety becomes a hang.
**`\schos
omirror` disables it entirely.** An empty file whose only job is to
exist. There is no console to ask a question on and no way to reach a menu if
the menu is what is broken, so the answer has to already be on the volume.
### Modes a real machine actually has
`mode_usable()` rejected three things it should not have:
| Was rejected | Why it was wrong |
| --- | --- |
| `PixelsPerScanLine != HorizontalResolution` | A GPU is entitled to pad each row, and 1920×1080 arriving with a 1984- or 2048-pixel scanline is ordinary. The stride is carried through to the kernel, so this excluded nothing but every machine that pads. |
| `HorizontalResolution % 8` | Excludes 1366×768, the panel on a very large number of laptops. |
| resolutions below 800×600 | 640×480 is a legitimate fallback and the only mode some firmware offers over a KVM. |
All three passed on QEMU, which is why they survived. None of them protected
anything: the drawing code steps by the stride and has done for as long as
there has been a stride to step by.
### The entry point
The kernel publishes a `sch64_header` — magic `SCH64BOO`, 8-byte aligned within
the first 16 KB, followed by a 64-bit physical entry address — found by
scanning, exactly as a multiboot loader scans for its own magic.
It exists because the firmware has *already* put the machine in long mode.
Dropping back to protected mode to re-enter through the 32-bit multiboot path
is a mode switch whose failure mode is a triple fault with no console. The
32-bit entry is still present in `entry.S` and now has no caller.
### The menu is on the serial line too
The three F8 screens paint into the framebuffer, which is the right place for
them on a machine with a screen and nowhere at all on one driven over a serial
cable — which is how this loader is usually driven. `f8_window()` already went
out of its way to support that, accepting `o` as well as F8 because *"a serial
console sends no scan codes at all"*. So the prompt invited a serial user in,
drew them a menu they could not see, and blocked for ever on a key they had no
way to choose.
Each screen now echoes itself to COM1: the whole menu once, then one line per
*move*. Not on every keypress — a serial terminal has no cursor to paint over,
so an unrecognised key would scroll the selection past again for nothing.
Straight to COM1 rather than through `print()`, because `print()` goes to
`ConOut` until the splash is up, and `ConOut` on a machine with graphics draws
its text across the menu that was just painted there.
Two details that are not cosmetic:
- **The item number is shown only where it is a key you can press.** The boot
menu takes 1–5. The resolution list takes only W/S and Enter and is longer
than nine entries, so numbering it printed `:`, `;`, `<` past the ninth — a
character nobody can type, offering to select something.
- **Q goes back as well as Esc.** On a serial line a lone `ESC` byte is
indistinguishable from the start of an escape sequence, so the terminal
driver holds it back waiting for the rest and it never arrives as a key. Esc
is the obvious thing to press on a screen and unreachable on a cable — the
same shape of problem, and the same answer, as `o` for F8.
## 3. `kmain()`
`kernel/core/main.c`. The ordering here is not arbitrary and several steps are
where they are because of a bug:
```
e820_init() memory map first — PHYSTOP is a variable, not a constant
kinit1() the allocator, given everything below the DMA arena
dma_init() the arena, carved off the TOP of physical memory
kvmalloc() the kernel page tables
acpi_init() CPU enumeration from the MADT; mpinit() only as fallback
lapicinit() tvinit() seginit() ioapicinit()
irq_init() before any driver that registers a handler
inputinit() before the keyboard and mouse start producing events
consoleinit() uartinit() mouseinit()
platform_register_devices(fb) the fixed furniture of a PC, plus the framebuffer
pci_init() enumerate, and bind drivers
pinit() binit() fileinit() sockinit()
time_init() after the timer runs, before userspace
ideinit() ramdiskinit()
startothers() the other processors
kinit2() sinit() the rest of memory, then PID 1
netinit() after sinit — it starts a kernel thread
mpmain() enter the scheduler; never returns
```
**The DMA arena is taken before `kalloc` gets the rest.** A device needs
physically contiguous memory and `kalloc`'s free list makes no such promise —
two pages from it are adjacent only by chance. Taking it first is the only way
to be sure the region exists.
**ACPI is asked before the legacy MP table**, and getting this backwards is
invisible under QEMU. Modern firmware publishes a complete MADT *and* an MP
table containing one processor, so asking the older interface first means never
seeing more than one CPU. VMware published the real count, which is why the
resulting triple fault only ever appeared there.
**`main()` names no display driver.** Whichever driver owns the framebuffer the
loader left running publishes it with `gfx_attach_scanout()`, and the console
and crash screen draw on that. See [graphics.md](graphics.md).
### Memory sizing
`PHYSTOP` is a variable read from the firmware's map, not a constant. It used to
be `0xE000000` — 224 MB — inherited from xv6, and it was not a limit on what the
kernel used but an **assertion that exactly that much RAM existed**. Less and the
allocator walked off the end of memory; more and it was invisible.
Three details survive that fix:
- It takes the end of the usable region **containing the kernel**, not the
highest usable address anywhere. A memory map has reserved holes, and freeing
a single range from the kernel's end to a global maximum puts firmware tables
on the free list — failing later, elsewhere, looking like anything but a
memory map problem.
- The usable regions are *also* kept as a list, and `kinit1()` frees each one.
One range ending at a top address is what a BIOS map looks like and what UEFI
never does: OVMF drops an ACPI NVS region at `0x800000`, so the run containing
the kernel ends at 8 MB and everything above the hole was invisible.
- The ceiling is 3 GB, because `kvmalloc()` maps 0–4 GB as four 1 GB pages and
marks the top one uncached for PCI BARs. The floor is 24 MB, and below it the
kernel prints why and stops rather than triple faulting in silence.
## 4. SMP
### A processor announces itself after it can run, not before
`mpmain()` used to open with `kinfo("cpu%d: starting")` and *then* call
`enable_sse()` and `idtinit()`. On a verbose boot with more than one CPU that
reset the machine — silently, with no panic and no last line, because the
failure is a triple fault and a triple fault prints nothing.
Two of those three lines have to come first, for different reasons:
- **`enable_sse()` clears `CR0.EM`.** Until it runs, *any* SSE instruction on
this CPU faults, and the kernel is not built with `-mno-sse`, so the compiler
emits them freely for struct copies and inline `memset`.
- **`idtinit()` loads this CPU's IDT.** Until it runs there is nowhere for a
fault to go — so the `#UD` above is not a panic, it is a double fault with no
handler, then a triple fault, then the firmware comes back.
**Why only on a verbose boot**, which is what made it look like an F8 bug
rather than an SMP one: `kinfo()` is log-only when the boot is quiet, so the
work is a few bytes into a ring and no XMM is involved. A verbose boot renders
the same line through the TrueType rasteriser, and that is where the SSE lands.
The symptom was that choosing "Boot SchOS (verbose)" appeared to do nothing:
the machine really did reboot, and the second boot was quiet, so what you ended
up looking at was an ordinary boot.
`startothers()` wakes each application processor through the local APIC, pointing
it at a trampoline copied to `0x7000`. Each AP runs `mpenter()` → `mpmain()`,
reports `cpu%d: starting`, and enters the scheduler.
This was broken from the project's beginning and is fixed. Five distinct bugs,
each hidden behind the one before it — the details are in
[../history/kernel.md](../history/kernel.md), and the one worth carrying
forward is in [kernel.md](kernel.md#per-cpu-state): **`__thread` is wrong for
per-CPU kernel data.**
APs are counted on the boot processor *after* each one reports in, so the count
cannot include a processor that started and died.
## 5. The first process
`sinit()` builds PID 1 by hand: a page directory, `initcode` mapped at 0, 32
pages of stack, a trapframe pointing at user mode, and session and process group
both set to 1.
It does **not** mount the root filesystem, and cannot: mounting reads blocks,
`bread()` sleeps, and sleeping needs a process context. So the mount happens in
`forkret()`, the first time any process is scheduled — guarded by a `static int
first`.
### Which device the root is on is decided there, not at build time
`ROOTDEV` used to be `#define ROOTDEV 1` — IDE disk 1 — so a kernel could only
ever root on the disk it was *built* expecting. Booting the same image from a
USB stick was impossible by construction, and that was the one thing standing
between this kernel and its own acceptance test, which begins "boot on real
hardware from a USB stick".
`rootdev_mount()` probes instead. `schfs_kmount()` was already written to be
used this way — *"the uberblock scan doubles as the probe: no valid uberblock
means this is simply not a SchFS volume, which is not an error worth reporting
loudly"* — and a failed probe leaves the slot unmounted and reusable, which is
what makes a loop over candidates legal at all.
| order | device | |
|---|---|---|
| 1 | `RAMDISK_DEV` | only in a `RAMDISK_ROOT` build, where it is an explicit choice |
| 2 | `USBDISK_DEV` | a stick that is plugged in was plugged in on purpose |
| 3 | IDE 1 | what the old constant named |
| 4 | IDE 0 | usually the ESP, so usually not SchFS |
`iderw()` *panics* on an absent drive rather than failing, so each candidate is
checked with `ide_disk_present()` / `usb_storage_present()` first. That is the
whole reason those predicates exist.
When nothing matches, the panic names what it looked at, because "no root
filesystem" with no list is the least useful thing a kernel can say at the one
moment somebody needs to know which disk it expected:
```
root: no SchFS volume on any candidate device. Looked at:
device 3, USB disk: not present
device 1, IDE disk 1: not present
device 0, IDE disk 0: present, but no SchFS on it
```
The chosen device is reported in `dmesg` as `root: USB disk (device 3)`.
That is also where PID 1's working directory is re-pointed at the real root,
and the reason is worth keeping:
> `sinit()` sets `cwd = namei("/")` before any filesystem is mounted. The old
> v1 root survived that because `iget(ROOTDEV, ROOTINO)` is two compile-time
> constants and touches no disk. SchFS has no constant to offer — the root
> object id comes off the uberblock — so that early `namei("/")` builds an inode
> from an unmounted volume. Nothing complains, because absolute paths never
> consult cwd: an absolute `exec` works fine. It is the first *relative* lookup
> that fails, and in init that is `open("console")`. No console means no fd
> 0/1/2, so every shell it spawns exits immediately and init respawns it
> forever. The symptom is a silent boot that reads like a hang and is not one.
`initcode` then execs **`/bin/schinit`**, and [init.md](init.md) takes over.
There is no fallback path and deliberately no `/bin/init` any more: the older
hardcoded fork-a-shell PID 1 was unreachable from the day that string changed,
and an unreachable fallback rots rather than saves.
## 6. What the screen shows
**The kernel log is on screen from the first line that can draw.** `kmain`
attaches the loader's framebuffer and clears `cons_serial_only` before it does
anything else, so `pci: 7 devices` and `schfs: mounted` are visible on the
machine itself.
This is the reverse of what the flag was added for, and the reversal was earned.
The original argument was that a boot which prints a developer's log to the
screen looks like a hobby project, and it is a good argument — right up until a
Chromebox with **no serial port** froze at boot showing nothing at all. The whole
diagnosis had to be made from a photograph of a screen that had been told to stay
quiet. A tidy boot that cannot be debugged on the hardware it fails on is not a
trade worth making.
`cons_serial_only` still exists for the case it is now the right answer to: no
framebuffer from the loader. Then everything goes to the UART and
`console_takeover()` clears the flag when userspace starts.
---
## 7. What the boot says
A working boot prints one line:
```
SchOS UEFI loader
Press F8 for boot options... booting
Welcome to SchOS! (F8 at boot for the full log, or dmesg once you are in)
root@schos / »
```
It used to print about thirty — the memory map, the local APIC's calibration,
every PCI device, each driver announcing itself, each job starting. All of that
is genuinely useful, and all of it is useful *once*. Printed on every boot it
trains the person in front of the machine to stop reading their own console,
which is expensive on the day one of those lines changes.
**Nothing was deleted; it was routed.** `consputc()` writes to the klog ring
before it writes anywhere else, so a line can be kept in full while being held
back from the two places a person is looking. `kinfo()` is `cprintf()` with
that routing, and the whole boot is still there afterwards:
```
# dmesg | head -4
[ 0.000000] e820: 1017 MB usable in 8 regions, top 1002 MB (21 entries)
[ 0.000000] klog: 128 KB at ffff800000737000, nothing recoverable was there
[ 0.000000] acpi: 4 cpus, lapic at ffff8000fee00000, ioapic id 0
[ 0.000000] console: JetBrains Mono, 10x21 cells, 128 by 38
```
**`dmesg` printed nothing at all until 2026-09-22**, which made that promise in
the banner a lie for as long as it had been there. Two separate things were
wrong, and each alone was enough:
- `sys_syslog()` was `return 0`. `dmesg` does not read `/proc/kmsg`; it calls
`klogctl(10)` for the buffer size and `klogctl(3)` to read, and 0 to the
first one means "the log is zero bytes". A stub that answers 0 is
indistinguishable from a machine that logged nothing.
- With that implemented, `dmesg` was *still* empty. It parses each line with
`sscanf(msg, "<%u>[%llu.%llu] %n", ...)` and **silently drops** anything that
does not match. The ring's `[ 0.00] text` already matches everything
after the priority — scanf skips the leading spaces — so the only missing
thing was the `<6>`.
`klog_render_syslog()` adds it on the way out rather than storing it, because
the ring is also what a person reads with `cat /proc/kmsg` and what PSOGM
points at after a crash. An ABI detail belongs at the boundary that has the
ABI.
The rule for `kinfo` against `cprintf` is not how interesting a line is —
everything was interesting to somebody once. It is: **would you want to see
this on a machine that is working?** `pci: 7 devices` is yes the first time and
no every time after. `e1000: no DMA memory for the rings` is yes for ever, and
stays a `cprintf`.
Three levels, then, and the middle one is new:
| | |
|---|---|
| quiet (default) | the greeting, plus anything that went wrong |
| verbose (F8) | every line above, on the console and the serial line |
| the klog | all of it, always, whichever of the two you booted |
Userspace follows the same rule through `/proc/bootmode`, which holds one word.
`schinit`, `schinitd` and `vgpud` read it and hold back their progress lines —
but never their failures, because a supervisor that goes quiet when a job dies
is worse than one that chatters.
**`schinit` reads it after `bootstrap()` and not before**, which is the sort of
detail that only shows up when tested: `/proc` is what `bootstrap()` mounts, so
reading it earlier returned "quiet" every time and made the verbose boot quiet
in the one process that runs first.
## What it does not do
- **No BIOS path.** A machine without UEFI does not boot SchOS.
- **The quiet boot hides a slow one.** With nothing printing between the
loader and the greeting, a boot that stalls for ten seconds looks the same as
one that does not. F8 is the answer and it has to be pressed *before* the
stall, which is the wrong order for finding out.
- **`/proc/bootmode` is read once, at start.** A program that wants to be
quieter or louder later has nowhere to say so, and nothing re-reads it.
- **The root device is compiled in.** Booting from a USB disk needs the root
device to be selectable, which it is not; the USB mass-storage driver works
and a superblock reads back through `bread()`, but nothing can mount it.
- **`make ramroot` is the only way to boot with no data disk.** It links a
filesystem image into the kernel binary, which costs boot time and carries a
licensing constraint — see [../licensing.md](../licensing.md) §4.
- **The 32-bit multiboot entry is dead code.** It is left in place deliberately:
deleting kernel entry code is its own piece of work with its own way of
failing silently.
- **`make qemu-uefi` under OVMF reports less memory than the machine has** in
some configurations. The loader's map is correct; the fix went in with the
region list above, and any remaining case is a fragmentation pattern not yet
seen.