Three things, and the first one is the freeze.
**The loader jumped out from under itself.** A machine with a lot of RAM
stopped at "Starting the kernel" with the splash still up: no panic, no reset,
nothing. The firmware's own exception handler had the answer the whole time,
once a machine was found that printed it:
X64 Exception Type - 0E(#PF - Page-Fault)
ExceptionData - 0x10 I:1 P:0
RIP - 0000000140004DEE <- the loader's own code, at 5.4 GB
RAX - 0000000000001000 <- the page tables it had just installed
RCX - 00000000001000F0 <- the kernel it was about to jump to
I:1 P:0 is an instruction fetch from a page that is not present. The loader
built page tables covering four gigabytes, loaded cr3 -- and the firmware had
placed the loader image *above* four gigabytes, so the next instruction fetch
was unmapped. It reproduces exactly on `-m 8G` and never on `-m 1G`, which is
why every test here passed.
The PDPT is one page whether four entries are filled or all 512, so all 512
are: 512 GB of identity map, replaced by kvmalloc() a few hundred instructions
later.
**And 3 GB was never the limit, it was a circle.** PHYSTOP_MAX was 0xC0000000
because kvmalloc() mapped exactly four 1 GB pages, and kvmalloc mapped four
because that was the cap -- each holding the other up, throwing away thirteen
gigabytes of a sixteen gigabyte machine. kvmalloc now maps as many gigabytes as
the machine has RAM and tells ioremap where the direct map stopped, instead of
ioremap assuming it stopped at four.
Freeing memory becomes two passes, and the ordering is forced rather than
chosen: kinit1() runs *before* kvmalloc because kvmalloc calls kalloc for its
own page tables, and freeing a page means writing into it -- so kinit1 can only
touch what the loader mapped. kinit_high() adds the rest afterwards.
`phystop` and the new `lowtop` are now two different numbers: the DMA arena
stays below 4 GB because a device is not obliged to reach the top of memory,
and an RTL8139's buffer address register is thirty-two bits wide.
**And the 0 MB.** The System Information screen reported "0 MB usable" on a
machine with sixteen gigabytes in it, because memory_mb() had its own 16 KB
guess at how big a memory map gets, and returned zero when GetMemoryMap said
EFI_BUFFER_TOO_SMALL. That is the same bug as the previous commit's, in a
second copy, and this one failed silently. There is one buffer and one
map_read() now.
Verified at 1 GB, 8 GB and 16 GB: all three boot, no traps, no panics, socktest
and memfdtest green in each. 8 GB and 16 GB did not boot at all before -- and
16 GB now reports 17408 MB with "mem: 13312 MB above 4 GB added to the
allocator", where the old kernel would have used three. Both display
configurations still clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EFI status 0x5 is EFI_BUFFER_TOO_SMALL, and that is the whole bug: the loader
asked the firmware for its memory map with a 32 KB buffer, which is about 680
descriptors -- generous next to QEMU's map and not generous at all next to a
machine whose firmware has carved out every reserved range it has ever needed.
The descriptor count is a property of the vendor, not of the RAM, which is why
this was invisible in every test here and fatal on two real machines.
The fix is not a bigger guess. A call with a **zero size** fails on purpose and
fills in the size actually needed, and it allocates nothing -- so the map key
from the real call immediately after is still valid, which is the constraint
this whole sequence is written around and the reason the buffer has to be
static. 256 KB now, which is 5461 descriptors, and a machine that somehow
exceeds even that is told in numbers rather than in a status code:
schos: the firmware's memory map needs 5664 bytes and this loader has 512
schos: memory map larger than the loader's buffer (status 0x...0005)
That line is real: the failure path was provoked by shrinking the buffer to 512
and booting, which is also how the 5664 was measured -- QEMU needs 118
descriptors, so hardware needs more.
**A second limit is nearby, and it is only a warning.** SCHBOOT_E820_MAX is 40
entries, fixed by the hand-off block's layout; build_e820() merges adjacent
regions of the same type, so 40 survives a long map. If it ever does truncate,
what is dropped is the high end -- the map arrives sorted and phystop is capped
below 4 GB regardless -- so it stays a warning. But it now appears on screen as
well as on the serial line, because after the splash print() goes to a port a
laptop does not have, and "some RAM was quietly unused" is not a thing to
discover months later from a total that never looked right.
Both supported configurations: one boot each, no traps, no panics, socktest and
memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
schdbgd ships Disabled, and its manifest explains why: a debugger that can read
the process table should not run on a machine whose owner never asked for one.
That default is right and has one bad consequence -- **the machines that most
need debugging are the ones you cannot get a shell on to enable it.** A laptop
with no serial port that stops before it can draw has nowhere to say why, and
`schinitd enable` needs a prompt that does not exist.
So `make DEBUG=1` adds a sixth entry to the F8 menu. Picking it toggles;
booting carries the choice through SCHBOOT_DEBUG_FLAG; the kernel publishes
/proc/bootdebug; schinitd starts the one job that flag may un-disable, with
the manifest's Args now naming 0.0.0.0 -- a debugger reachable only from the
machine being debugged is no use when that machine is the one that will not
talk to you.
Four decisions, since this is a network-reachable debugger:
compiled out, not hidden A release build cannot set the flag because the
code that would set it is not in the binary. A
menu item that is present and refuses is an
invitation to make it not refuse.
consent at the machine Somebody standing at the keyboard during boot is
the strongest consent this system can ask for and
the weakest one that is any use on hardware that
will not boot.
one job, named in schinitd Not a manifest key like EnableIfDebug: the general
version of "the boot menu starts things that ship
off" is worse than the specific one, and this way
what the flag can start is one readable line.
the kernel only publishes Nothing in the kernel behaves differently, so a
debug kernel and a release kernel are the same
kernel and a bug cannot be the flag's fault.
/proc/bootdebug is its own file rather than a second line in /proc/bootmode,
because bootmode's whole promise is that reading it is a string compare and
not a parser.
**Also: `tone` stopped lying.** On a machine with no sound card it reported
"the device will not take 16-bit samples", which sends you looking at the
format code instead of at the empty PCI slot -- /dev/dsp exists whether or not
hardware is behind it, and the ioctl is where the absence shows up. ENODEV is
now told apart from a format refusal.
Verified end to end: F8 -> item 6 -> boot -> schinitd says it is starting
schdbgd -> a client outside the VM connected to the forwarded port and ran
`dma`, getting the real arena dump back. The release build has no such entry
in the binary at all, and boots clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Leaving the firmware, and the bar is full" -- so on the laptop
ExitBootServices was reached, and everything after it was silent. die() draws
on screen, so a failed ExitBootServices would have said so; it did not, which
means the firmware call succeeded and the machine stopped somewhere in the
handful of instructions between there and the kernel's first.
That window had no diagnostics because of a claim in a comment: "nothing below
can report anything". Half true. ConOut is gone, but the *framebuffer* is not
-- it is memory, the firmware has stopped using it, and ui_progress_label() is
a write to it and nothing more. So the window is now split: "Starting the
kernel" is drawn after the page tables are built and immediately before the
jump. A machine that stops on "Leaving the firmware" stopped in
ExitBootServices or the page-table build; one that stops on "Starting the
kernel" stopped in the jump or in the kernel itself, before it clears the
screen.
**And one speculative change, reverted.** On the theory that hardcoded low
memory is a poor assumption under UEFI -- the firmware is entitled to have
anything at 0x1000 -- the page tables were moved to pages asked of
AllocatePages. It cost two triple faults: cr3 still loaded the constant, and
then the PML4 entries still pointed at the constant PDPT. Both were fixed, and
it still reboot-looped, which is when the actual objection turned up:
kernel/arch/x86_64/entry.S:85 # bootstrap page tables (0x1000 - 0x3000)
kernel/arch/x86_64/entry.S:111 mov $0x1000, %eax
mov %eax, %cr3
Those addresses are a *contract between the loader and the kernel's assembly*,
not this file's private choice. Moving them in one of the two places was never
going to work, and the theory that started it had no evidence behind it -- no
report says 0x1000 is the laptop's problem. So it is reverted, with the
contract written down where the constants are, and if it ever does move it
moves in both files at once.
Both supported configurations: one boot each, no traps, no panics, socktest
and memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two reports from an Asus TUF FA607NU, which now gets as far as the splash:
the progress bar does not move, and after a resolution change the screen keeps
the background colour with no mark and no bar. Both point at the same code,
and it is code this loader did not have a week ago.
**Modes were being matched on every frame.** mirrors_match() ran from inside
mirrors_show(), so every menu redraw and every segment of the kernel load
asked another display adapter to change mode. On QEMU that is instant and
idempotent. On real hardware a mode set can take the best part of a second, can
blank a panel while it happens, and -- on a laptop -- is quite often being
asked of the *same* panel reached through a second GOP. A loader that does that
in the middle of drawing a progress bar is a loader that shows a background
colour and then waits on a firmware call that is not coming back.
It now happens exactly once, at discovery, before anything is drawn. The
second head is centred and cropped afterwards rather than re-moded, and the
booted system matches resolutions anyway because the kernel driver and vgpud
both follow the console.
**And mirroring no longer runs per segment of the kernel load.** Copying a
megabyte-and-a-half framebuffer to every other adapter, three times, to animate
a bar on a screen nobody is looking at.
**One refusal now turns it off for the rest of the boot**, and
`\schos\nomirror` -- an empty file whose only job is to exist -- turns it off
before it starts. There is no console to ask a question on at that point and no
menu to reach if the menu is what is broken, so the answer has to already be on
the volume. That also makes it a one-file experiment on hardware we cannot
instrument.
**The loader names its stages on screen.** After the splash, print() goes to
the serial port and nowhere else, so on a machine with no serial port a stall
leaves a progress bar and no information at all. Every stage after the kernel
load now labels itself under the bar -- Reading the console font, Reading the
memory map, Leaving the firmware -- so the last thing on the screen names the
call that did not return. Past ExitBootServices there is nothing left to report
with, which is itself worth knowing: a machine stopped on the last label
stopped in the jump.
Opening the boot volume moved ahead of the display, because what is on the
volume now decides some of what the display does.
Both supported configurations: one boot each, no traps, no panics, socktest and
memfdtest green. Mirroring still verified end to end -- F8, a resolution
change, boot, and both heads byte-identical at the new mode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first piece of real-hardware work, and it is three bugs that QEMU cannot
show you. The report was an ASUS laptop -- Ryzen 7000-series APU, Radeon
integrated, RTX discrete -- booting to a white underscore on a black screen,
and unable to be offered 1920x1080.
**The framebuffer address was truncated.** The loader handed over
`(UINT32)gop->Mode->FrameBufferBase` and the kernel 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. A machine that puts its
aperture above 4 GB, which is ordinary on a laptop with a discrete GPU, got a
plausible-looking address pointing at nothing. The splash went into it, the
kernel was handed it, and the firmware's own cursor kept blinking on an
otherwise black screen. Every step in that sequence succeeded on a number that
was wrong, which is why there is no error message anywhere in it.
SCHBOOT_FB_BASE is the loader's own display block: 64-bit address, 64-bit
size, 32-bit width, height and stride, and the pixel format. The VBE block is
still written for the BIOS loader, but anything that does not fit is left at
zero rather than wrapped -- a truncated address is this bug, and a zero is at
least honest.
The kernel then has a problem the loader does not: its direct map covers the
low 4 GB, so a framebuffer above it needs ioremap(), which needs kpdpt, which
kvmalloc() has not allocated when the early console starts. A low framebuffer
is attached exactly as before; a high one is attached immediately after
kvmalloc(), still thousands of lines before anything else.
**There was more than one adapter, and the first one was taken.**
LocateProtocol answers with *a* GOP and the specification says nothing about
which. QEMU with a stdvga and a virtio-gpu has **three** handles and two of
them are Blt-only with a framebuffer address of zero -- so this is not a
hardware-only hazard, it was a one-in-three chance of drawing into address 0
that happened to keep coming up heads. Every handle is scored now: no
aperture, no candidacy; a readable current mode beats one that needs changing;
size breaks the tie.
**mode_usable() rejected modes real machines have.** A padded scanline (every
GPU that aligns its rows -- and very likely the reason 1920x1080 could not be
offered), a width not divisible by eight (1366x768, which is half the laptops
in the world), and anything below 800x600 (the only mode some firmware gives
over a KVM). All three passed on QEMU, which is why they survived. The mode
list now offers 1366x768 and 640x480 where it did not.
The loader also prints what it found, to the firmware's text console, which is
cleared before the splash: invisible on a boot that works, and the only thing
on the screen on a boot that dies before the splash.
One process note, recorded because it cost a wrong test: `make UEFI_KEEP=1`
ships the loader that is already built. Its own comment says "a build that
quietly ships a stale bootloader is how you spend an evening debugging code
that is not running", and that is exactly what happened -- a regression run
that looked clean had tested the old loader. Loader work gets a full build.
Both supported configurations verified after a clean build: one boot each, no
traps, no panics, socktest and memfdtest green, and the adapter report shows
the right one being chosen out of three.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gate 0's last-but-one open item. Drop a TrueType file at \schos\console.ttf on
the ESP and the console is drawn with it; with no file, the face linked into
the kernel image, which is what an early panic needs and is not a degraded
mode.
**The loader reads it, and that is the decision worth writing down.** The
kernel opening the file once the root filesystem is mounted is the obvious
design. It is also wrong: the console has been drawing since the kernel's
first line, so a font arriving later changes the cell size, which changes the
column count, which re-lays out every terminal -- and blanks them. The kernel
log would vanish off the screen at the moment the root filesystem mounted,
which is exactly the log a machine being brought up for the first time is
being read for. Since the next thing on the list is real hardware, losing that
was not a trade worth making.
So the choice happens before anything is drawn. Three details make it cheap:
- **EfiReservedMemoryType**, not EfiLoaderData. The E820 handoff turns
loader data into usable memory, on the argument that the loader's own
pages are the kernel's the moment it runs -- true of the loader, not true
of a buffer it wants the kernel to keep reading from.
- **Below 4 GB**, because the kernel reaches this through its direct map and
that is how far the direct map goes. A font above it would need an ioremap
the console cannot do before there is a console.
- **Used in place.** No copy, which matters because the kernel has no
contiguous allocator to copy 112 KB into -- the only one is the DMA arena,
and that belongs to devices.
Sized without EFI_FILE_INFO and its GUID: seek past the end and ask where that
landed, which the specification requires to clamp.
Every failure falls back and keeps going -- no file, implausible size, no
memory, short read, unparseable file. The last says so in the log, because a
font that silently did not load looks exactly like a font that did.
Verified by booting with JetBrainsMono-ExtraBold.ttf on the ESP and
screendumping the console: visibly the bold face, against the Regular of a
boot with no file. Three boots each way, no traps and no panics, socktest and
memfdtest green in all six.
Nothing ships a console.ttf: the file would be the file already in the kernel,
and two copies of a font are two things that can disagree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things.
**The F8 resolution now applies to the other displays as well.** It already
reached them once vgpud started -- that was the last commit but one -- but not
before: choosing 1024x768 changed the screen it was chosen on and left the
other at 1280x800, so the menu and then the splash appeared on it as a small
rectangle in the middle of a larger black one. Which is a fair reading of "the
resolution change is not changing resolution".
The resolution offered in that menu is a statement about the machine's display,
not about one adapter of it, so both stages before vgpud now follow it:
the loader mirrors_match() puts every mirror GOP into the primary's mode,
called from mirrors_show() so it cannot be forgotten at a new
call site. Two integer comparisons in the common case.
the kernel setup_scanout() takes gfx's dimensions over GET_DISPLAY_INFO's.
Same argument as vgpud's bring_up(): the host window's size is
a fact about QEMU, and the console's mode is the one somebody
chose.
**And the greeting is no longer a race.** "Welcome to SchOS!" was printed after
every job had been started, including the shell on the console -- so which of
the two reached the screen first was up to the scheduler, and it went both ways
about evenly. A boot that sometimes greets you above the prompt and sometimes
below it does not read as two orderings of one boot; it reads as a line printed
by mistake.
schinitd now holds back the job that owns the console -- `own_tty`, which is
exactly the job whose output lands there -- until after the greeting is
written. That is enough to settle it: this process writes the line, and only
then asks PID 1 to spawn the shell, so the fork has not happened yet. Nothing
waits on it that was not already waiting.
Verified: F8 -> Resolution -> 1024x768 -> boot, with the virtio head
screendumped over QMP at three points during startup and once at the prompt --
1024x768 throughout, and byte-identical to the stdvga head. Three consecutive
boots put the greeting at the same offset, ahead of the prompt every time.
Both supported configurations: one boot each, no traps, no panics, socktest and
memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things, both asked for.
**The crash report no longer says which monitors it reached.** "The report is
on the virtio-gpu display too" was a line about the crash screen printed on the
crash screen. The report says what happened and where to read it again; that is
not part of it.
**And the second head now shows the F8 menu and the boot.** It used to be black
until vgpud started several seconds into userspace -- through the splash,
through the menu, through every boot message -- which from the outside is
indistinguishable from a display that does not work. Three stages now cover the
whole boot and hand over to each other without coordinating:
*The loader.* LocateProtocol() answers with the first GOP and offers no way to
ask for the rest, so the rest are found by handle and fed the primary's
framebuffer as a Blt source at the points where a screen is finished: the menu,
the resolution chooser, system information, the splash, each segment of the
kernel load. Blt rather than a second framebuffer to draw into, because a
virtio GOP does not have one -- the same fact that made a virtio-only machine
reboot-loop two commits ago.
*The kernel.* gfx remembers which rows were drawn, as one span rather than a
list of rectangles, because console output is lines and a span is what lines
make. display_mirror_console() copies that span across: a line of boot text is
one cell-row transfer, not a screen. Primed once after pci_init(), because a
display that has just appeared has a black scanout and the console it should be
showing was drawn by the loader into a framebuffer it knows nothing about.
*Then vgpud*, from the same console framebuffer, unchanged.
**Two bugs found building it, both worth the retelling.**
The mirror ran per character from inside vt_feed -- which is inside the console
lock -- and vga_disp_acquire() announces its back buffer with a cprintf. A CPU
taking a lock it already holds, with a call chain that said exactly that. It
runs at the end of consolewrite() and cprintf() now, outside the lock, which is
also the better batching: one transfer per write instead of one per character.
And "is this the console's own screen" was asked of the pointer acquire()
returned. vga_fb hands back a back buffer it copies onto the console's
framebuffer afterwards, so on a one-screen machine the answer was "a different
screen" -- and the console was mirrored onto itself through two megabytes of
intermediate, allocated for the purpose. display_ops grew scanout_phys, which
is a fact about the hardware rather than a policy a driver can get subtly
wrong, and it is asked first: it has no side effects and acquire() has two.
Verified with two heads under QEMU, screendumping the virtio head every 400 ms
from power-on: splash, splash, splash, then the console, and it stays. The F8
menu appears on it; choosing 1024x768 there brings both heads up at 1024x768
byte-identical. A panic still reaches both. Both supported configurations: one
boot each, no traps, no panics, socktest and memfdtest green.
One gap remains and is documented rather than hidden: while the kernel's driver
resets the device during probe, QEMU shows "Display output is not active" for a
fraction of a second. A device being taken over has no scanout until the new
driver sets one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three F8 screens painted into the framebuffer and nowhere else. That is
right on a machine with a screen and useless on one driven over a serial cable
-- which is how this loader is usually driven, and which f8_window() already
went out of its way to support: it accepts 'o' as well as F8 because, in its
own words, "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 then blocked for ever on a key they had no way to choose. The whole visible
behaviour was:
Press F8 for boot options...
<nothing, for ever>
Each screen echoes itself to COM1 now -- the boot menu, the resolution list and
System Information. The whole thing once on entry, then one line per *move*;
not on every keypress, because a serial terminal has no cursor to paint over
and an unrecognised key would scroll the selection past again for nothing.
Straight to COM1 rather than through print(): 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 found by actually driving it, neither 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.** A lone ESC byte on a serial line 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 as F8, and the same answer.
Driven end to end over serial: open the menu, walk it with W/S, open System
Information and come back, open Resolution and come back with Q, then boot.
The graphical menu is unchanged apart from the hint now naming Q.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A boot printed about thirty lines before reaching a prompt. Every one was
useful once, and printed on every boot for ever they train the person in front
of the machine to stop reading their own console -- which is expensive on the
day one of them changes.
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 / »
**Nothing was deleted, it was routed.** consputc() already wrote to the klog
ring before anywhere else, so a line can be kept in full while being held back
from the two places a person looks. kinfo() is cprintf() with that routing, and
`dmesg` still has the whole boot with timestamps afterwards. The rule for which
is which is not how interesting a line is -- everything was interesting to
somebody once -- but whether you would want to see it 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.
The verbose boot the F8 menu already offered now means something much bigger,
which is the other half of this: it is the only way to see any of it live.
Userspace follows through /proc/bootmode, one word. schinit, schinitd and vgpud
hold back their progress lines and never their failures -- a supervisor that
goes quiet when a job dies is worse than one that chatters.
Two things found by testing rather than by reading:
- schinit read /proc/bootmode *before* bootstrap() mounted /proc, so it got
"quiet" every time and the verbose boot was quiet in the one process that
runs first. It reads after the mount now.
- resolvconf announced "already exists; -f to replace it" on every boot after
the first, which is a line about having done nothing. It is quiet when the
file already says what it would have written, and speaks only when the two
disagree -- which means the nameserver changed and the machine is using the
old one.
Verified both ways: quiet boot is six lines, a forced-verbose build prints all
thirty plus the `verbose boot:` steps, and /proc/kmsg holds every line in both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
Boot died straight after "exiting boot services" with a KVM internal error,
RIP=0x801a5dc5 and RBX=0x80000000 -- the framebuffer base. The bytes at RIP were
0x0C0E13 and 0xD1D5DC: SCH_BG and a grey from the splash's metal gradient. The
machine was executing the picture of itself starting up.
The hand-off to the kernel writes rbx and did not say so:
"mov %1, %%rbx\n\t" <- writes rbx
"jmp *%2\n\t"
: : "r"(PML4_ADDR), "r"(fb), "r"(entry)
: "memory"); <- rbx not declared clobbered
With rbx absent from the clobber list the compiler may allocate an input there,
and clang 21 allocated `entry`:
movq %rax, %cr3
movq %rcx, %rbx <- the framebuffer, into rbx
jmpq *%rbx <- %2 was rbx, and now holds the framebuffer
Declaring "rbx" forbids the allocation. Verified in the generated assembly
rather than by reasoning: jmpq *%rbx became jmpq *%r14, and the machine now
boots through to schinit: ready.
This had been latent since the loader was written. It worked only because every
compiler that had built it happened to choose a different register, and it
surfaced the first time the binary was relinked with a different clang -- which
only happened because the previous commit fixed the Makefile dependency that had
been leaving the loader stale. A stale artifact hides the bug the fresh one
reveals.
It is also the third time this project has been broken by a register the code
depends on but never asks for: docs/history/kernel.md section 4 (the BIOS
loader passing the framebuffer in EAX by luck) and section 8 (%fs:0 used as
scratch when the ABI reserves it). Written up as section 7b, with the one-line
recipe that settles all three -- read the generated assembly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
Reported from a real laptop booting through Ventoy: the F8 menu appears,
selecting Boot SchOS gives a black screen and a white dash in the corner.
Both halves of that are bugs.
The disc could not be chainloaded. Booting it directly, firmware runs the
El Torito FAT image and the loader opens \schos\kernel on that volume,
which is where mkiso.sh put it. Ventoy does not use El Torito at all: it
mounts the ISO 9660 tree and runs \EFI\BOOT\BOOTX64.EFI from there. The
loader then looks for the kernel on the volume it was itself loaded from
-- the ISO tree -- and the kernel was only ever inside the FAT image. It
is now in both. The comment in mkiso.sh had even described this failure,
as the reason the kernel is in the FAT image, without noticing the ISO
tree had the same requirement.
The error was invisible. print() redirects to the serial port once the
splash is up, which is right for a development machine and useless on a
laptop: the loader died with a perfectly good explanation and sent it to
a wire nobody was holding. What is left on screen is the firmware's own
cursor -- the white dash. die() now draws the message on the framebuffer
too, over a cleared band so it does not land on the logo.
Verified by reproducing the exact failure: the ISO tree extracted and
booted as a plain volume, as a chainloader presents it. With the kernel
removed it dies visibly with "no kernel at \schos\kernel or \kernel, EFI
status 0x000E" -- EFI_NOT_FOUND, which is what the laptop was hitting
silently. With it present, that same volume boots through to a shell.
110 [ok] on the ESP path, the ISO check passes, no failures.
Two bugs on the ExitBootServices path, both invisible under OVMF and both
aimed squarely at real firmware.
The retry did not re-translate. If ExitBootServices returns an error the
loader re-reads the memory map and tries again -- correct, and the
specification expects it -- but the e820 table handed to the kernel was
still the one built from the *previous* map. The retry exists precisely
because the map changed, so the kernel was being given a description of
memory the firmware had already rearranged. QEMU never takes this path,
which is why it survived. Translation is now a function called on both.
Truncation was silent. phystop comes from the largest usable region, so
dropping the tail of the map can leave the kernel believing a large
machine has a fraction of its RAM -- and nothing said so, because the
loop simply stopped at the cap. It now warns while there is still a
console to warn on; after ExitBootServices there is nowhere to report
anything.
SCHBOOT_E820_MAX goes from 32 to 40. The map sits at BASE+0x208 and the
next field is at BASE+0x600, so 1016 bytes are reserved and 42 entries of
24 bytes fit. Real firmware emits far more descriptors than QEMU's; the
headroom was already allocated and unused.
Coalescing adjacent same-type runs is what keeps any of this in budget,
and it works because boot services memory becomes usable at this point --
that is most of the fragmentation in a UEFI map.
110 [ok], no failures, and the map still reads 7 regions under OVMF.
ui_panel was written for this screen during the loader restyle and then
never called -- dead code sitting in boot/common/ui.c waiting for the row
that would use it. This is that row: F8 -> System Information, showing
firmware vendor and revision, UEFI revision, CPU brand, usable memory,
current mode and whether ACPI was found.
Everything is read when the screen is drawn rather than captured at
startup, because the resolution row above it can have changed the mode,
and a panel still reporting what the machine booted with is worse than no
panel at all.
Two facts are deliberately read through the same code that feeds the
kernel -- efi_type_to_e820() for what counts as usable memory, and
find_rsdp() for ACPI. A panel with its own idea of either would be a
second implementation to keep in step, and the ACPI line exists precisely
because a missing RSDP means no MADT, no IOAPIC and no SMP, which has
already been debugged once from the far end.
The screenshot caught a bug in the screen it was taken to verify: UEFI
revision read "21587.2286". EFI_TABLE_HEADER begins with a UINT64
Signature, so Revision is at offset 8 and I had read offset 4 -- the
upper half of "IBI SYST", formatted as a version number. It now reads 2.7
under OVMF, which is correct.
Verified by driving the menu over the QEMU monitor and reading the
pixels: five rows with the hint updated to 1-5, the panel centred, and
every value cross-checkable -- EDK II, firmware 1.0, UEFI 2.7, "QEMU
Virtual CPU version 2.5+" against -cpu qemu64, 1018 MB of the 1 GB given,
1280x800 matching what fbshot captures, ACPI present. The highlight sits
9 px inside both panel borders, symmetric.
F8 has a 1.5 s window, so the capture presses it repeatedly through boot
rather than guessing when OVMF finishes -- and only F8, since any other
key breaks the wait and boots.
Also: every ESP drive now says format=raw. QEMU warned about probing the
format on each of the fifteen or so places that attach it, which is new
noise on what is now the only boot path. 110 [ok], no failures, no
warnings.
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.
The screenshot was mode 16. OVMF advertises it as 1366 wide; asking for
it produces a framebuffer QEMU scans out at 1360, so every row written
lands six pixels further along than the one above and the whole screen
shears. The firmware never says it rounded, and nothing in the mode
information hints at it -- PixelsPerScanLine agrees with the width it
claims.
So a mode is only usable if its width is a multiple of 8, which is the
VGA scanout granularity. Checked rather than assumed: 1400x1050 and
1280x720 render perfectly centred, 1366x768 does not, and it is the only
advertised width that is not a multiple of 8.
The single cycling row is now a list. Every usable mode the firmware
offers, deduplicated -- it advertises the same geometry several times with
different internals, and a menu with 1280x1024 in it three times is one
nobody trusts -- plus a row for typing a size. A typed size is matched
against the same filtered list, so a mode that would shear cannot be
reached by typing it either, and a size that does not exist says so
rather than silently keeping the old one.
Also fixes the panel layout you spotted: the title band was exactly as
tall as the heading, so the rule was drawn through the lettering and the
first row sat on its baseline; and the selected row's highlight had a
fixed 3px offset against a 24px row, so it sat on the text rather than
around it.
Verified by applying four resolutions from the list -- 1280x800,
1024x768, 1280x960, 1600x1200 -- and checking the panel is centred in
each. bootcheck 108 [ok].
BIOS still asks in a text prompt before the framebuffer exists, and that
is the remaining half of "the same for both". See the next commit message
or the note in stage2.S: VBE is INT 10h, so it needs real mode, and a
graphical chooser needs either unreal mode before the switch or a return
to real mode after it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
UEFI had no boot menu at all, and asked about resolution in a prompt of
its own before anything else. Two questions at two moments through two
mechanisms is what made the firmwares feel like different programs.
There is one window now: press F8 (or 'o' -- a serial console sends no
scan codes, and that is how this is usually driven) and the same panel
the BIOS loader shows appears, with resolution as a row in it. Selecting
it steps to the next 32bpp mode at or above 800x600 and redraws; the
framebuffer is re-read every pass, because changing mode moves it and
drawing where it used to be is a black screen with no explanation.
Two layout bugs, both visible and both arithmetic:
- TITLE_H was 34 while the heading alone occupies PANEL_PAD + GLYPH_H
= 34, so the rule at TITLE_H - 6 was drawn *through* the lettering
and the first row started on the heading's baseline. TITLE_H is
derived from its parts now and cannot drift from them again.
- the selected row's highlight was offset by a fixed 3px against a
24px row, so it sat on the text rather than around it. It is centred
on the glyph box: (ROW_H - GLYPH_H) / 2 above and below.
Verified from the host: the panel is centred at 640 on a 1280 screen, the
highlight is exactly ROW_H tall and clear of the rule, and Enter boots
through to cpu0. bootcheck 108 [ok], iso builds.
The BIOS resolution chooser is still a separate text prompt, and that is
not an oversight to fix later in the same way. VBE mode setting is an
INT 10h call, so it can only happen in real mode -- before there is any
framebuffer to draw a menu in. Offering it from the graphical menu means
either returning to real mode from long mode, or entering unreal mode to
draw the menu before the switch. Both are real mode-switching work in the
riskiest code in the tree, and neither is a tidy-up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The BIOS loader drew its own splash and the UEFI loader drew a different
one, which is how two implementations of a single screen end up looking
nothing alike: only one of them ever got a change. stage2main.c's
fourteen drawing functions are deleted and it calls boot/common/ui.c,
which is what the UEFI loader already used. There is one splash now.
What changed in the look, in order of how much it mattered:
- No product name under the mark. The name is *in* the mark, and
setting it again underneath read as a placeholder nobody removed.
- The mark is scaled to the bar's width and sits directly above it, so
the two are one object rather than a large logo with an unrelated
line beneath. Box-filtered down from the source mask, never up:
point-sampling a coverage mask drops half the edge pixels and puts
holes in the thin strokes.
- Metal rather than flat grey. A vertical gradient with a knee near the
top where a highlight falls, applied to the mark, the bar fill and
panel headings. A single colour on a dark ground looks printed; the
knee is what makes it look lit. No texture -- at this size noise
reads as dirt.
- The boot manager is a panel with a lit selected row, replacing a
screen laid out in literals with a magic y of 340 that only ever
looked right at 800x600.
Three defects found by reading pixels rather than code:
- the UEFI loader kept calling print() after the splash, and ConOut on
a graphical firmware renders into the same framebuffer -- a
screenshot showed firmware text across our mark from the left edge.
Diagnostics go to COM1 directly now, which is where they were being
read from anyway, and the console is cleared before drawing so the
firmware has nothing to put back.
- SPLASH_UP was declared and read but never assigned, so that redirect
was dead code and the "serial works" reading was OVMF's own console.
- stage2 overflowed its allocation -- 25440 bytes against 24576 -- and
the build did not stop. The kernel is written at sector
1 + STAGE2_SECTORS, so a stage2 past its allocation is overwritten by
the kernel it is loading. STAGE2_SECTORS is 64 now, and the padding
tool's check does fire; it had simply not been re-run.
Verified from the host on both: BIOS 800x600 and UEFI 1280x800 both draw
a centred group with zero stray pixels, UEFI's spanning x 576-703 which
is exactly the mark width centred. bootcheck 108 [ok], iso builds.
Still text-mode: the resolution menu. On BIOS it necessarily is -- it
runs in real mode before any framebuffer exists, which is the whole
reason it can set one. On UEFI it could be graphical and is not yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first piece of the two loaders that is genuinely common code rather
than a contract they both honour. It can be, because the difference
between the firmwares is entirely in how a framebuffer is *obtained* --
VBE in real mode, or GOP -- and not in what you do with one afterwards.
boot/common/ui.c takes a base, a size and a stride and draws; it knows
nothing about disks, memory maps or mode setting, which is what lets it
compile into a 16/32/64-bit BIOS loader and a PE/COFF UEFI application
without either leaking into it.
The look is deliberately quiet: dark ground, the mark centred, one thin
bar beneath it, nothing on screen that is not load-bearing. The whole
group is laid out from the centre outwards rather than from the top down,
so it sits identically at 800x600 and at 1600x1200 instead of drifting up
the screen as the mode grows.
UEFI had no graphical splash at all -- it printed text to a console the
QEMU window is usually not even showing. It draws the same screen as the
BIOS path now, with the bar tracking bytes of kernel actually written
rather than a segment count, which says nothing about how long each takes.
Two ordering bugs found by looking at the pixels rather than the code:
setup_framebuffer ran *after* the kernel was read, so every progress tick
went to a framebuffer that did not exist yet and the splash then painted
over the result; and that also put the resolution prompt after the
slowest step instead of at the start. The display is set up first now.
Verified from the host with screendump: background is exactly SCH_BG, the
bar is 4px tall and width/4 wide at y=495 on a 1280x800 screen, which is
where centring the group puts it. UEFI still boots to cpu0; BIOS
bootcheck 27/27.
Not yet shared: the BIOS loader still draws with its own copy of this
code. Deleting that copy in favour of this one is the next step and the
point of the exercise -- until then this is one loader using shared code
and one duplicating it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Groundwork for the unified loader: before the two paths can share code,
they have to agree on what they are producing, and until now they only
agreed by coincidence.
Every field of the loader/kernel hand-off was #defined in whichever file
happened to need it -- kernel/core/main.c, kernel/dev/vga.c,
kernel/arch/x86_64/e820.c, boot/bios/cdboot.S, boot/bios/stage2main.c and
boot/uefi/main.c, six copies of the same addresses. schboot.h existed for
exactly this and held only the entry magic and the RSDP slot, with a
comment describing the rest of the block it did not define.
The whole block lives there now: the VBE info fields, the verbose and
media flags (with the real-mode segment:offset spelling cdboot.S needs),
the e820 count, map and cap, and the RSDP. Every one of those files
includes it instead of restating it.
This is not a tidy-up. It is the same hazard as defining the entry magic
twice, only quieter -- nothing breaks until one copy moves, and the two
loaders diverging is precisely what produced the EFER.SCE and RSDP bugs,
where a fix on one path silently did not exist on the other.
Verified on both firmwares: BIOS boots to schinit with the same e820,
ACPI and VGA lines as before; UEFI reaches cpu0 with 1018 MB in 7 regions
and its own 1280x800 mode. `make iso` still builds. bootcheck 108 [ok].
Next: the loaders become starters that gather firmware-specific facts and
hand to one common stage. That needs the BIOS side to climb to long mode
first, since common code can only be compiled for one CPU mode -- which
is the risky part, and is why the contract went first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moving the mouse repainted the whole screen. At 1280x800 that is a
million pixels of desktop fill, every surface re-blitted, and a 4 MB copy
to the framebuffer -- per event. Under emulation the pointer lagged the
mouse by whole seconds, and that was not the emulation's fault: moving a
10x10 cursor does not require touching four megabytes.
Two changes, and the second matters as much as the first.
Damage tracking: one rectangle, unioned from whatever actually changed,
and only those rows are redrawn and copied out. A pointer move damages
where it was and where it is; a commit damages that surface; a client
going away damages what it had. One rectangle rather than a list, because
in practice the changes are a cursor and the window under it, and a list
costs more bookkeeping than it saves.
Draining input: a single mouse movement produces a burst of EV_REL ending
in EV_SYN, and the loop handled one event per poll and repainted. So the
compositor rendered every intermediate position of a cursor that was
never seen at those positions. Both queues are now drained before
anything is painted. handle_input grew a distinct "queue empty" return
for it -- EV_SYN changes nothing but is not the end of the burst, and a
drain that stopped there would leave the rest queued.
Also cuts the loader's resolution prompt from four seconds to one and a
half, on both the BIOS and UEFI paths. Four was mine and it was too long:
it is paid on every unattended boot, and adding four seconds to every
start to offer a menu nobody usually wants makes the common case worse to
serve the rare one.
schcom-test passes, including two overlapping clients and stacking order.
bootcheck 108 [ok].
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GOP instead of VBE, and the same reasoning. There are no mode numbers
worth hardcoding here: the firmware publishes however many modes the
hardware can do and which index means what varies by machine, so the menu
is built by walking them. Press R within four seconds for a list, then
either a mode number or Enter to type a size. 800x600 stays the floor.
Fixes a header bug this is the first code to notice. The spec's order in
EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE is MaxMode then Mode; efi.h had them
swapped. Info, SizeOfInfo and FrameBufferBase sit at the same offsets
either way because both fields are UINT32, so the framebuffer worked
perfectly and nothing was wrong until something asked how many modes
existed -- and was told the current mode number instead. Zero, on a
machine reporting thirty.
QueryMode and SetMode were void* padding and now have real prototypes;
ConIn likewise, so the loader can read a key. Under OVMF the console
includes the serial line, which is where `make qemu-uefi` puts it and
where the person actually is -- the same mistake the BIOS menu made first
time round.
Verified over serial against OVMF: 30 modes enumerated; picking listed
mode 14 gives 1280x1024; typing 1024x768 gives 1024x768; no key keeps the
firmware's mode and boots. All reach schinit. bootcheck 108 [ok].
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
find_rsdp() scanned the EBDA and 0xE0000-0x100000. That is a BIOS
convention, and UEFI firmware is under no obligation to leave a copy
there -- OVMF does not. So the kernel found no RSDP, no MADT and no
IOAPIC, and nothing routed IRQ 4. Serial output is polled, so the machine
booted all the way to a shell prompt and then ignored every key, which
reads as a broken console rather than a missing table.
The UEFI loader reads the RSDP out of the EFI configuration table,
preferring ACPI 2.0 for its XSDT, and leaves it at SCHBOOT_RSDP_ADDR --
the same low-memory hand-off block the e820 map already uses, now
described in include/schboot.h so the two sides cannot disagree.
The kernel validates it rather than trusting it: the BIOS path never
writes that slot, so there it holds whatever was in memory, and following
a wild pointer faults before there is anything to report the fault with.
Signature and checksum do that. It deliberately does not require 16-byte
alignment -- that rule is about searching the BIOS area, and OVMF's RSDP
sits at 0x3f77e014, so demanding it threw away the one address this code
exists to use.
UEFI now reports "acpi: 1 cpu, lapic at ..., ioapic id 0" and the ISO
boots to an interactive shell: uname, ls /bin (241), writing and reading
a file, ps, /proc/schfs -- all driven over serial.
BIOS unchanged: bootcheck 108 [ok], same ACPI line as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
boot/bios/ now holds the MBR, the El Torito stub and the real-mode stage
2; boot/uefi/ is empty and documented.
Separate directories rather than #ifdefs, because the two share almost
nothing. A BIOS loader lives in 16-bit real mode, reaches the firmware
through software interrupts, and walks itself up into long mode. A UEFI
loader is a 64-bit PE that the firmware has already placed in long mode
with paging on, calling services through a table of function pointers.
What they do share is the hand-off contract, which boot/README.md now
writes down: ELF segments placed, long mode, a 24-byte-entry memory map
at E820_MAP_ADDR, framebuffer geometry at VBE_INFO_ADDR.
That contract is the point of documenting it now. UEFI's GetMemoryMap
returns EFI_MEMORY_DESCRIPTORs, and the right move is to translate them
into the entries kernel/arch/x86_64/e820.c already parses rather than
teach the kernel a second format.
Build output paths under $(BUILD)/boot are unchanged; only sources moved.