SchOS/boot/uefi/main.c
Nexoniarz 1e52cb591b boot: the loader jumped into the framebuffer, because rbx was not clobbered
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
2026-09-12 18:44:24 +02:00

1193 lines
44 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// SchOS UEFI loader.
//
// The BIOS path and this one meet at a contract, not at shared code: see
// boot/README.md. This loader's whole job is to arrive at that contract from
// the other direction.
//
// 1. find the kernel on the volume this image was loaded from
// 2. load its ELF PT_LOAD segments to their physical addresses
// 3. record the framebuffer where kernel/dev/vga.c looks for it
// 4. translate the UEFI memory map into the E820 entries
// kernel/arch/x86_64/e820.c already parses
// 5. ExitBootServices
// 6. build the page tables the kernel's 64-bit entry expects, and jump
//
// Step 6 is why kernel/arch/x86_64/entry.S grew a `sch64_header`. The
// multiboot entry is 32-bit code that climbs into long mode itself; the
// firmware has already put us there, and dropping back down only to climb out
// again is a mode switch whose failure mode is a triple fault with no console.
// The kernel publishes its 64-bit entry instead, and this jumps straight to it.
//
// Nothing here allocates after ExitBootServices, and nothing calls a firmware
// service after it either -- there are none. Everything the jump needs is
// computed before.
#include "efi.h"
#include "../../include/schboot.h"
#include "../common/ui.h"
// Where the kernel expects to find things comes from schboot.h, included
// above: the hand-off is one contract and both loaders write the same one.
#define E820_MAXENTRIES SCHBOOT_E820_MAX
#define E820_USABLE 1
#define E820_RESERVED 2
#define E820_ACPI 3
#define E820_NVS 4
#define KERNEL_PATH L"\\schos\\kernel"
#define KERNEL_PATH_ALT L"\\kernel"
// Page tables are built here, exactly where mboot_entry builds them, so the
// kernel's entry sees the state it was written for.
#define PML4_ADDR 0x1000
#define PDPT_ADDR 0x2000
#define PTE_P 0x001
#define PTE_W 0x002
#define PTE_PS 0x080
struct e820entry {
UINT64 base;
UINT64 length;
UINT32 type;
UINT32 acpi_ext;
} __attribute__((packed));
// ELF64, only what is needed to place segments.
struct elf64_ehdr {
UINT8 e_ident[16];
UINT16 e_type, e_machine;
UINT32 e_version;
UINT64 e_entry, e_phoff, e_shoff;
UINT32 e_flags;
UINT16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx;
};
struct elf64_phdr {
UINT32 p_type, p_flags;
UINT64 p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align;
};
#define PT_LOAD 1
static EFI_SYSTEM_TABLE *ST;
static EFI_BOOT_SERVICES *BS;
// ---------------------------------------------------------------------------
// Console. Enough to say what went wrong, and no more.
// ---------------------------------------------------------------------------
/* Once the splash is up, nothing may write to the console.
*
* ConOut on a graphical firmware renders text into the same framebuffer the
* splash is drawn in, so every diagnostic after that point is scribbled across
* it -- which is exactly what a screenshot showed: our mark centred, and
* firmware text over the top of it from the left edge.
*
* The diagnostics are worth keeping, so they go to COM1 directly instead. That
* is legal before ExitBootServices, costs nothing, and is where they were being
* read from anyway.
*/
static int SPLASH_UP;
/* Set by the boot menu; handed to the kernel through the same flag byte the
* BIOS loader writes, so a verbose boot means the same thing either way. */
static int VERBOSE_BOOT;
static void
serial_putc(char c)
{
/* Bounded, for the same reason the BIOS loader's is: a machine with no
* serial port reads 0xFF from the status register forever. */
for (int i = 0; i < 100000; i++) {
UINT8 st;
/* Through DX, not an immediate: the immediate form of in/out only
* encodes ports 0-255, and COM1's registers are at 0x3F8. */
__asm__ volatile("inb %1, %0" : "=a"(st) : "d"((UINT16)0x3FD));
if (st & 0x20)
break;
}
__asm__ volatile("outb %0, %1" : : "a"((UINT8)c), "d"((UINT16)0x3F8));
}
static void
print(const CHAR16 *s)
{
if (!s)
return;
if (SPLASH_UP) {
for (int i = 0; s[i]; i++)
serial_putc((char)(s[i] & 0x7F));
return;
}
if (ST && ST->ConOut)
ST->ConOut->OutputString(ST->ConOut, (CHAR16 *)s);
}
static void
print_hex(UINT64 v)
{
CHAR16 buf[19];
const CHAR16 *digits = L"0123456789abcdef";
buf[0] = L'0'; buf[1] = L'x';
for (int i = 0; i < 16; i++)
buf[2 + i] = digits[(v >> ((15 - i) * 4)) & 0xf];
buf[18] = 0;
print(buf);
}
// A failure here cannot be recovered from and must not be silent: without this
// the firmware simply returns to its boot menu and the user is told nothing.
static EFI_STATUS
die(const CHAR16 *msg, EFI_STATUS st)
{
print(L"schos: "); print(msg);
print(L" (status "); print_hex(st); print(L")\r\n");
print(L"schos: halted.\r\n");
/* Say it on the screen too, if there is one.
*
* Once the splash is up, print() goes to the serial port and nowhere else.
* That is right for a development machine and useless on a laptop, which
* has no serial port -- a loader that failed after drawing the splash left
* a black screen and the firmware's own cursor, with the explanation going
* to a wire nobody was holding. A dead machine that says why is worth the
* dozen lines. */
if (ui_active()) {
char line[96];
int i = 0;
for (const char *q = "Cannot start SchOS: "; *q && i < (int)sizeof line - 1; q++)
line[i++] = *q;
for (int j = 0; msg[j] && i < (int)sizeof line - 1; j++)
line[i++] = (msg[j] >= 0x20 && msg[j] < 0x7F) ? (char)msg[j] : '?';
line[i] = 0;
char code[32];
i = 0;
for (const char *q = "EFI status 0x"; *q && i < (int)sizeof code - 1; q++)
code[i++] = *q;
/* Low nibbles are enough to identify the error and keep this short;
* EFI_NOT_FOUND is 0x0E with the high bit set. */
const char *hex = "0123456789ABCDEF";
for (int sh = 12; sh >= 0; sh -= 4)
if (i < (int)sizeof code - 1)
code[i++] = hex[(st >> sh) & 0xF];
code[i] = 0;
/* Clear a band first. Without it the message lands on top of the
* logo, which is exactly the moment legibility matters most. */
int mid = (int)ui_height() / 2;
ui_fill_rect(0, mid - 20, (int)ui_width(), 100, 0x0C0E13u);
ui_text_centered(mid, line, 0xE8EAEEu);
ui_text_centered(mid + 24, code, 0xB9BEC7u);
ui_text_centered(mid + 56, "Halted -- power off or reset", 0x8A9099u);
}
for (;;)
__asm__ volatile("hlt");
}
static void
zero(void *p, UINTN n)
{
UINT8 *b = (UINT8 *)p;
while (n--) *b++ = 0;
}
static void
copy(void *dst, const void *src, UINTN n)
{
UINT8 *d = (UINT8 *)dst;
const UINT8 *s = (const UINT8 *)src;
while (n--) *d++ = *s++;
}
// ---------------------------------------------------------------------------
// Reading the kernel off the ESP
// ---------------------------------------------------------------------------
static EFI_FILE_PROTOCOL *
open_kernel(EFI_HANDLE image)
{
EFI_GUID li_guid = EFI_LOADED_IMAGE_PROTOCOL_GUID;
EFI_GUID fs_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
EFI_LOADED_IMAGE_PROTOCOL *li = 0;
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = 0;
EFI_FILE_PROTOCOL *root = 0, *file = 0;
EFI_STATUS st;
// The volume this loader was itself loaded from, rather than a search of
// every filesystem: booting one installation's kernel from another's ESP
// is not a thing to do by accident.
st = BS->HandleProtocol(image, &li_guid, (void **)&li);
if (EFI_ERROR(st)) die(L"cannot get LOADED_IMAGE", st);
st = BS->HandleProtocol(li->DeviceHandle, &fs_guid, (void **)&fs);
if (EFI_ERROR(st)) die(L"boot volume has no filesystem", st);
st = fs->OpenVolume(fs, &root);
if (EFI_ERROR(st)) die(L"cannot open the boot volume", st);
st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH, EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st))
st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH_ALT,
EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st)) die(L"no kernel at \\schos\\kernel or \\kernel", st);
return file;
}
// Load PT_LOAD segments to p_paddr. Physical addresses, not virtual: the
// kernel is linked at 0xffff800000100000 but must be *placed* at 0x100000, and
// p_paddr is where the linker script says each segment belongs.
static UINT64
load_elf(EFI_FILE_PROTOCOL *file)
{
struct elf64_ehdr eh;
UINTN n = sizeof eh;
EFI_STATUS st;
st = file->SetPosition(file, 0);
if (EFI_ERROR(st)) die(L"cannot seek the kernel", st);
st = file->Read(file, &n, &eh);
if (EFI_ERROR(st) || n != sizeof eh) die(L"cannot read the ELF header", st);
if (eh.e_ident[0] != 0x7f || eh.e_ident[1] != 'E' ||
eh.e_ident[2] != 'L' || eh.e_ident[3] != 'F')
die(L"kernel is not an ELF file", 0);
if (eh.e_ident[4] != 2) // ELFCLASS64
die(L"kernel is not 64-bit", 0);
UINT64 lowest = ~0ULL, highest = 0;
// Two passes' worth of information from one: the loop below both loads and
// reports, so the bar tracks bytes actually written rather than a segment
// count that says nothing about how long each takes.
UINT64 total = 0, done = 0;
for (UINT16 i = 0; i < eh.e_phnum; i++) {
struct elf64_phdr ph;
UINTN m = sizeof ph;
if (EFI_ERROR(file->SetPosition(file, eh.e_phoff + (UINT64)i * eh.e_phentsize)))
break;
if (EFI_ERROR(file->Read(file, &m, &ph)) || m != sizeof ph)
break;
if (ph.p_type == PT_LOAD)
total += ph.p_memsz;
}
ui_progress_begin("Loading SchOS", (ui_u32)(total ? total : 1));
for (UINT16 i = 0; i < eh.e_phnum; i++) {
struct elf64_phdr ph;
n = sizeof ph;
st = file->SetPosition(file, eh.e_phoff + (UINT64)i * eh.e_phentsize);
if (EFI_ERROR(st)) die(L"cannot seek a program header", st);
st = file->Read(file, &n, &ph);
if (EFI_ERROR(st) || n != sizeof ph) die(L"cannot read a program header", st);
if (ph.p_type != PT_LOAD || ph.p_memsz == 0)
continue;
// Claim the pages before writing them. The firmware is still running
// and this is its memory until it says otherwise; writing to
// conventional memory it has not handed over is how a loader corrupts
// the very services it is about to call.
EFI_PHYSICAL_ADDRESS at = ph.p_paddr & ~0xfffULL;
UINTN pages = (UINTN)((ph.p_paddr + ph.p_memsz - at + 0xfff) / 0x1000);
st = BS->AllocatePages(AllocateAddress, EfiLoaderData, pages, &at);
if (EFI_ERROR(st)) {
// Already allocated by the firmware for something of its own, or
// simply not conventional memory. Not fatal on its own -- the
// range may still be usable -- but worth saying, because a kernel
// that boots and then corrupts itself starts here.
print(L"schos: warning: could not reserve "); print_hex(at);
print(L"\r\n");
}
if (ph.p_filesz) {
n = (UINTN)ph.p_filesz;
st = file->SetPosition(file, ph.p_offset);
if (EFI_ERROR(st)) die(L"cannot seek a segment", st);
st = file->Read(file, &n, (void *)(UINTN)ph.p_paddr);
if (EFI_ERROR(st) || n != ph.p_filesz)
die(L"short read loading a segment", st);
}
// .bss: present in memsz, absent from filesz, and the kernel assumes
// it is zeroed.
if (ph.p_memsz > ph.p_filesz)
zero((void *)(UINTN)(ph.p_paddr + ph.p_filesz),
(UINTN)(ph.p_memsz - ph.p_filesz));
done += ph.p_memsz;
ui_progress_tick((ui_u32)done);
if (ph.p_paddr < lowest) lowest = ph.p_paddr;
if (ph.p_paddr + ph.p_memsz > highest) highest = ph.p_paddr + ph.p_memsz;
}
if (highest == 0) die(L"kernel has no loadable segments", 0);
// Find the 64-bit entry the kernel publishes, by scanning the first 16 KB
// of what was just loaded -- the same way the BIOS loader finds the
// multiboot header. A magic in the image beats a symbol table: no ELF
// symbol parsing, and no fixed offset to drift.
UINT8 *img = (UINT8 *)(UINTN)lowest;
for (UINTN off = 0; off + sizeof(struct sch64_header) <= 16384; off += 8) {
struct sch64_header *h = (struct sch64_header *)(img + off);
if (h->magic0 == SCH64_MAGIC0 && h->magic1 == SCH64_MAGIC1)
return h->entry;
}
die(L"kernel has no SCH64 header -- too old for this loader", 0);
return 0;
}
// ---------------------------------------------------------------------------
// Framebuffer
// ---------------------------------------------------------------------------
// Offer a resolution choice, briefly. The UEFI half of the same feature the
// BIOS loader has -- see boot/bios/stage2.S.
//
// Different mechanism, same reasoning. GOP has no mode numbers worth
// hardcoding: the firmware publishes however many modes the hardware can do,
// and which index means what varies by machine. So this walks them, and the
// menu is built from what is actually there rather than from a table.
//
// A second and a half, then it boots on its own. This was four seconds and that
// 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.
// A key, or 0 if nothing was typed within `ms` milliseconds. Stall is the
// simplest clock available before boot services go away, and polling in 10 ms
// steps is far below anything a person can notice.
// Read a decimal number, echoing it. Ends on Enter (CR, or LF over a serial
// console). Non-digits are ignored rather than aborting, so a stray keystroke
// does not silently truncate the number.
// Pick a GOP mode. Returns the mode number to set, or the current one.
/* ---------------------------------------------------------------------------
* The boot menu: the same one the BIOS loader shows, on the same panel.
*
* Resolution is a row in here rather than a prompt of its own before it. Two
* questions asked at two different moments by two different mechanisms is what
* made the two firmwares feel like different programs.
*
* This is the half of it the UEFI side can do properly: GOP sets a mode at any
* point before ExitBootServices, so the framebuffer exists first and the
* chooser can be drawn in it. The BIOS side cannot -- see boot/bios/stage2.S.
* ------------------------------------------------------------------------- */
static void
u64_to_dec(UINT64 v, char *out, int cap)
{
char tmp[24];
int n = 0;
if (v == 0) tmp[n++] = '0';
while (v && n < 23) { tmp[n++] = (char)('0' + (v % 10)); v /= 10; }
int i = 0;
while (n > 0 && i < cap - 1) out[i++] = tmp[--n];
out[i] = 0;
}
static void
res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap)
{
char wa[24], ha[24];
u64_to_dec(gop->Mode->Info->HorizontalResolution, wa, sizeof wa);
u64_to_dec(gop->Mode->Info->VerticalResolution, ha, sizeof ha);
int i = 0;
for (const char *p = "Resolution: "; *p && i < cap - 1; p++) out[i++] = *p;
for (char *p = wa; *p && i < cap - 1; p++) out[i++] = *p;
if (i < cap - 1) out[i++] = 'x';
for (char *p = ha; *p && i < cap - 1; p++) out[i++] = *p;
out[i] = 0;
}
/* Step to the next 32bpp mode at or above 800x600, wrapping. Cycling one row
* needs no scrolling state to get wrong, and a loader does not need a list. */
/* Is this mode one we can actually draw in?
*
* The width has to be a multiple of 8, and that is not fussiness. OVMF
* advertises a 1366-wide mode; asking for it produces a framebuffer QEMU scans
* out at 1360, so every row we write lands six pixels further along than the
* one above and the whole screen shears. The firmware never says it rounded.
* Eight is the VGA scanout granularity, and every mode that satisfies it has
* been checked to come back the size it claimed.
*
* 32bpp because the whole system assumes it, and 800x600 as the floor because
* that is what the layout is built around.
*/
static int
mode_usable(EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi)
{
if (!mi) return 0;
if (mi->PixelFormat > 1) return 0;
if (mi->HorizontalResolution < 800 || mi->VerticalResolution < 600) return 0;
if (mi->HorizontalResolution % 8) return 0;
if (mi->PixelsPerScanLine != mi->HorizontalResolution) return 0;
return 1;
}
/* --- the resolution list ---------------------------------------------------
*
* Every mode the firmware offers that we can actually draw in, plus a row for
* typing one. A list rather than the single cycling row it replaced: cycling
* is fine for two options and useless for twenty-odd, which is what a real
* machine advertises.
*/
#define RES_MAX 24
static char RES_TEXT[RES_MAX + 1][24];
static UINT32 RES_MODE[RES_MAX];
static int RES_N;
static void
res_build(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{
RES_N = 0;
for (UINT32 i = 0; i < gop->Mode->MaxMode && RES_N < RES_MAX; i++) {
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = 0;
UINTN sz = 0;
if (EFI_ERROR(gop->QueryMode(gop, i, &sz, &mi)) || !mode_usable(mi))
continue;
/* Skip a size already listed: the firmware offers the same geometry
* several times over with different internals, and a menu with
* 1280x1024 in it three times is a menu nobody trusts. */
char buf[24];
char wa[12], ha[12];
u64_to_dec(mi->HorizontalResolution, wa, sizeof wa);
u64_to_dec(mi->VerticalResolution, ha, sizeof ha);
int k = 0;
for (char *q = wa; *q; q++) buf[k++] = *q;
buf[k++] = 'x';
for (char *q = ha; *q; q++) buf[k++] = *q;
buf[k] = 0;
int dup = 0;
for (int j = 0; j < RES_N; j++) {
int e = 1;
for (int c = 0; c < 24; c++) {
if (RES_TEXT[j][c] != buf[c]) { e = 0; break; }
if (!buf[c]) break;
}
if (e) { dup = 1; break; }
}
if (dup)
continue;
for (int c = 0; c <= k; c++)
RES_TEXT[RES_N][c] = buf[c];
RES_MODE[RES_N] = i;
RES_N++;
}
const char *custom = "Type a size...";
int c = 0;
for (const char *q = custom; *q; q++) RES_TEXT[RES_N][c++] = *q;
RES_TEXT[RES_N][c] = 0;
}
/* Read a decimal number on the panel, echoing it as it is typed. */
static UINT32
res_read_number(const char *prompt)
{
UINT32 v = 0;
char shown[32];
for (;;) {
int i = 0;
for (const char *q = prompt; *q && i < 24; q++) shown[i++] = *q;
char num[12];
u64_to_dec(v, num, sizeof num);
for (char *q = num; *q && i < 30; q++) shown[i++] = *q;
shown[i] = 0;
const char *rows[1] = { shown };
ui_menu("Custom resolution", rows, 1, 0, "digits, Enter to accept");
EFI_INPUT_KEY key;
for (;;) {
if (ST->ConIn &&
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
break;
BS->Stall(10000);
}
if (key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n')
return v;
if (key.UnicodeChar >= L'0' && key.UnicodeChar <= L'9')
v = v * 10 + (UINT32)(key.UnicodeChar - L'0');
if (key.UnicodeChar == 8 && v) /* backspace */
v /= 10;
}
}
static void
res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{
int sel = 0;
for (;;) {
struct ui_fb uifb;
uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase;
uifb.width = gop->Mode->Info->HorizontalResolution;
uifb.height = gop->Mode->Info->VerticalResolution;
uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4;
ui_init(&uifb);
res_build(gop);
const char *items[RES_MAX + 1];
for (int i = 0; i <= RES_N; i++)
items[i] = RES_TEXT[i];
if (sel > RES_N) sel = RES_N;
ui_menu("Resolution", items, RES_N + 1, sel,
"W/S to choose, Enter to apply, Esc to go back");
EFI_INPUT_KEY key;
for (;;) {
if (ST->ConIn &&
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
break;
BS->Stall(10000);
}
CHAR16 c = key.UnicodeChar;
if (key.ScanCode == 0x17 || c == 27) /* Esc */
return;
if (key.ScanCode == 0x01 || c == L'w' || c == L'W') {
sel = (sel + RES_N) % (RES_N + 1); continue;
}
if (key.ScanCode == 0x02 || c == L's' || c == L'S') {
sel = (sel + 1) % (RES_N + 1); continue;
}
if (c == L'\r' || c == L'\n') {
if (sel < RES_N) {
gop->SetMode(gop, RES_MODE[sel]);
return;
}
/* Typed. Matched against the same filtered list, so a size that
* would shear cannot be reached by typing it either. */
UINT32 w = res_read_number("Width: ");
UINT32 h = res_read_number("Height: ");
for (UINT32 i = 0; i < gop->Mode->MaxMode; i++) {
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = 0;
UINTN sz = 0;
if (EFI_ERROR(gop->QueryMode(gop, i, &sz, &mi)) ||
!mode_usable(mi))
continue;
if (mi->HorizontalResolution == w &&
mi->VerticalResolution == h) {
gop->SetMode(gop, i);
return;
}
}
/* Nothing matched. Saying so beats silently keeping the old mode
* and letting the user wonder whether the key registered. */
const char *msg[1] = { "No such mode -- keeping this one" };
ui_menu("Resolution", msg, 1, 0, "any key to go back");
for (;;) {
if (ST->ConIn &&
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
break;
BS->Stall(10000);
}
return;
}
}
}
static UINT32 build_e820(const UINT8 *mapbuf, UINTN mapsize, UINTN descsize,
struct e820entry *out);
/* Both defined below, next to the hand-off code they belong to. The system
* information panel reads the same two facts the kernel will be handed, on
* purpose: a panel with its own idea of what counts as usable memory, or its
* own ACPI search, would be a second implementation to keep in step. */
static UINT32 efi_type_to_e820(UINT32 t);
static UINT64 find_rsdp(void);
/* CPU brand string, from CPUID leaves 0x80000002..4. Forty-eight bytes of
* ASCII split across twelve registers, usually with leading spaces on Intel
* parts, so it is trimmed on the way out. A CPU that does not implement the
* extended leaves reports a maximum below 0x80000004, and there is nothing to
* substitute -- saying "unknown" is better than printing whatever registers
* happened to hold. */
static void
cpu_brand(char *out, int cap)
{
UINT32 eax = 0, ebx = 0, ecx = 0, edx = 0;
__asm__ volatile("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx)
: "a"(0x80000000u));
if (eax < 0x80000004u) {
int i = 0;
for (const char *q = "unknown"; *q && i < cap - 1; q++) out[i++] = *q;
out[i] = 0;
return;
}
char raw[49];
int n = 0;
for (UINT32 leaf = 0x80000002u; leaf <= 0x80000004u; leaf++) {
__asm__ volatile("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx)
: "a"(leaf));
UINT32 regs[4] = { eax, ebx, ecx, edx };
for (int r = 0; r < 4; r++)
for (int b = 0; b < 4; b++)
raw[n++] = (char)((regs[r] >> (b * 8)) & 0xFF);
}
raw[48] = 0;
const char *src = raw;
while (*src == ' ') src++;
int i = 0;
while (*src && i < cap - 1) out[i++] = *src++;
while (i > 0 && out[i - 1] == ' ') i--;
out[i] = 0;
}
/* Total conventional memory, in MB, from the UEFI memory map.
*
* This is a *look*, not the hand-off: GetMemoryMap is called again at exit
* because any allocation between here and there invalidates the map key, and
* calling it once for both purposes is how a loader gets EFI_INVALID_PARAMETER
* from ExitBootServices long after the mistake. The count is deliberately of
* the types the kernel will treat as usable, so it matches what the system
* later reports rather than what the firmware owns. */
static UINT64
memory_mb(void)
{
UINTN size = 0, mapkey = 0, dsize = 0;
UINT32 dver = 0;
static char buf[16384];
size = sizeof buf;
if (EFI_ERROR(BS->GetMemoryMap(&size, (EFI_MEMORY_DESCRIPTOR *)buf,
&mapkey, &dsize, &dver)))
return 0;
UINT64 pages = 0;
for (UINTN off = 0; off + dsize <= size; off += dsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(buf + off);
if (efi_type_to_e820(d->Type) == E820_USABLE)
pages += d->NumberOfPages;
}
return (pages * 4096u) / (1024u * 1024u);
}
/* Narrow a CHAR16 string into ASCII for the panel. The firmware vendor is the
* only UTF-16 string on this screen; anything outside ASCII becomes '?' rather
* than being silently dropped, so a name that does not fit still looks like a
* name. */
static void
narrow(const CHAR16 *src, char *out, int cap)
{
int i = 0;
if (src)
while (src[i] && i < cap - 1) {
CHAR16 c = src[i];
out[i] = (c >= 0x20 && c < 0x7F) ? (char)c : '?';
i++;
}
out[i] = 0;
}
/* The system information screen.
*
* Everything here is read at the moment it is shown rather than captured at
* startup: the resolution can have been changed by the row above it, and a
* panel that still reports the mode the machine booted with is worse than no
* panel. */
static void
sys_info(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{
char vendor[40], cpu[52], mode[32], mem[24], fw[24], spec[24], acpi[24];
narrow(ST->FirmwareVendor, vendor, sizeof vendor);
cpu_brand(cpu, sizeof cpu);
{
char w[16], h[16];
u64_to_dec(gop->Mode->Info->HorizontalResolution, w, sizeof w);
u64_to_dec(gop->Mode->Info->VerticalResolution, h, sizeof h);
int i = 0;
for (char *q = w; *q && i < (int)sizeof mode - 1; q++) mode[i++] = *q;
if (i < (int)sizeof mode - 1) mode[i++] = 'x';
for (char *q = h; *q && i < (int)sizeof mode - 1; q++) mode[i++] = *q;
mode[i] = 0;
}
{
char n[24];
u64_to_dec(memory_mb(), n, sizeof n);
int i = 0;
for (char *q = n; *q && i < (int)sizeof mem - 1; q++) mem[i++] = *q;
for (const char *q = " MB usable"; *q && i < (int)sizeof mem - 1; q++)
mem[i++] = *q;
mem[i] = 0;
}
/* The firmware's own revision, and the UEFI revision it implements. These
* disagree often and the difference matters when reading a bug report:
* one names the build, the other names the contract. */
{
char hi[16], lo[16];
u64_to_dec(ST->FirmwareRevision >> 16, hi, sizeof hi);
u64_to_dec(ST->FirmwareRevision & 0xFFFF, lo, sizeof lo);
int i = 0;
for (char *q = hi; *q && i < (int)sizeof fw - 1; q++) fw[i++] = *q;
if (i < (int)sizeof fw - 1) fw[i++] = '.';
for (char *q = lo; *q && i < (int)sizeof fw - 1; q++) fw[i++] = *q;
fw[i] = 0;
}
{
/* EFI_TABLE_HEADER is { UINT64 Signature; UINT32 Revision; ... }, so
* Revision is at offset 8. At offset 4 this read the upper half of the
* "IBI SYST" signature and displayed it as "21587.2286". */
UINT32 rev = *(UINT32 *)(ST->Hdr + 8);
char hi[16], lo[16];
u64_to_dec(rev >> 16, hi, sizeof hi);
u64_to_dec((rev & 0xFFFF) / 10, lo, sizeof lo);
int i = 0;
for (char *q = hi; *q && i < (int)sizeof spec - 1; q++) spec[i++] = *q;
if (i < (int)sizeof spec - 1) spec[i++] = '.';
for (char *q = lo; *q && i < (int)sizeof spec - 1; q++) spec[i++] = *q;
spec[i] = 0;
}
/* Whether ACPI was found, because its absence is not cosmetic: no RSDP
* means no MADT, no IOAPIC and no SMP, and that has already been debugged
* once from the far end. */
{
const char *v = find_rsdp() ? "present" : "NOT FOUND";
int i = 0;
for (const char *q = v; *q && i < (int)sizeof acpi - 1; q++) acpi[i++] = *q;
acpi[i] = 0;
}
static const char *const labels[] = {
"Firmware", "Firmware revision", "UEFI revision",
"Processor", "Memory", "Display", "ACPI",
};
const char *values[7];
values[0] = vendor;
values[1] = fw;
values[2] = spec;
values[3] = cpu;
values[4] = mem;
values[5] = mode;
values[6] = acpi;
ui_panel("System Information", labels, values, 7, "Any key to go back");
EFI_INPUT_KEY key;
for (;;) {
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
return;
BS->Stall(10000);
}
}
static int
boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{
int selected = 0;
char resbuf[48];
for (;;) {
/* Re-read the framebuffer every pass: choosing a resolution moves it,
* and drawing where it used to be is a black screen with no
* explanation. */
struct ui_fb uifb;
uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase;
uifb.width = gop->Mode->Info->HorizontalResolution;
uifb.height = gop->Mode->Info->VerticalResolution;
uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4;
ui_init(&uifb);
res_row(gop, resbuf, sizeof resbuf);
const char *items[5];
items[0] = "Boot SchOS";
items[1] = "Boot SchOS (verbose)";
items[2] = resbuf;
items[3] = "System Information";
items[4] = "Reboot to firmware";
ui_menu("Boot Manager", items, 5, selected,
"W/S or 1-5 to choose, Enter to select");
EFI_INPUT_KEY key;
CHAR16 c = 0;
UINT16 scan = 0;
for (;;) {
if (ST->ConIn &&
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) {
c = key.UnicodeChar; scan = key.ScanCode; break;
}
BS->Stall(10000);
}
if (scan == 0x01) { selected = (selected + 4) % 5; continue; } /* up */
if (scan == 0x02) { selected = (selected + 1) % 5; continue; } /* down */
if (c == L'w' || c == L'W') { selected = (selected + 4) % 5; continue; }
if (c == L's' || c == L'S') { selected = (selected + 1) % 5; continue; }
if (c >= L'1' && c <= L'5') { selected = (int)(c - L'1'); continue; }
if (c == L'\r' || c == L'\n') {
if (selected == 0) return 0;
if (selected == 1) return 1;
if (selected == 2) { res_choose(gop); continue; }
if (selected == 3) { sys_info(gop); continue; }
if (selected == 4) {
ui_clear();
ui_text_centered((int)ui_height() / 2, "Halted -- reset to reach firmware setup",
0xB9BEC7u);
for (;;) __asm__ volatile("hlt");
}
}
}
}
/* Wait briefly for F8, exactly as the BIOS loader does, and show the same
* menu if it comes. One prompt, one key, one screen -- the resolution chooser
* lives in that menu rather than in a second prompt of its own.
*/
static int
f8_window(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{
print(L"Press F8 for boot options... ");
EFI_INPUT_KEY key;
for (UINTN waited = 0; waited < 1500; waited += 10) {
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) {
/* SCAN_F8 is 0x12. A serial console sends no scan codes at all, so
* 'o' is accepted too -- otherwise this is unreachable for anyone
* working over a serial line, which is how it is usually driven. */
if (key.ScanCode == 0x12 || key.UnicodeChar == L'o' ||
key.UnicodeChar == L'O') {
print(L"\r\n");
return boot_menu(gop);
}
break;
}
BS->Stall(10000);
}
print(L"booting\r\n");
return 0;
}
static UINT32
setup_framebuffer(void)
{
EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
EFI_GRAPHICS_OUTPUT_PROTOCOL *gop = 0;
if (EFI_ERROR(BS->LocateProtocol(&gop_guid, 0, (void **)&gop)) || !gop) {
print(L"schos: no graphics output; console only\r\n");
return 0;
}
// F8 first, on whatever mode the firmware left us in, because the menu has
// to be visible in order to offer a different one. Anything it changes is
// reflected below, which is why nothing is read from gop->Mode until after.
VERBOSE_BOOT = f8_window(gop);
// Re-read after any mode change: Info and FrameBufferBase both describe the mode
// that is current, and the framebuffer can move when the mode changes.
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info = gop->Mode->Info;
UINT32 fb = (UINT32)gop->Mode->FrameBufferBase;
// The layout kernel/dev/vga.c reads, byte for byte. It is a VBE mode-info
// block as the BIOS path leaves it, and the kernel takes exactly four
// fields out of it -- so those four are what get written, at the offsets
// it uses.
UINT8 *vbe = (UINT8 *)SCHBOOT_VBE_INFO;
zero(vbe, 512);
UINT32 bpp = 32; // GOP's 8-bit formats are 32bpp
UINT16 stride = (UINT16)(info->PixelsPerScanLine * (bpp / 8));
*(UINT16 *)(vbe + 16) = stride; // bytes/line
*(UINT16 *)(vbe + 18) = (UINT16)info->HorizontalResolution;
*(UINT16 *)(vbe + 20) = (UINT16)info->VerticalResolution;
*(UINT8 *)(vbe + 25) = (UINT8)bpp;
*(UINT32 *)(vbe + 40) = fb;
// The same UI the BIOS loader draws, on the framebuffer this one just
// obtained. Everything above this point is firmware-specific; nothing below
// it is, which is the whole reason it can be shared.
struct ui_fb uifb;
uifb.base = (ui_u8 *)(UINTN)fb;
uifb.width = info->HorizontalResolution;
uifb.height = info->VerticalResolution;
uifb.stride = stride;
// Tell the firmware its console is empty before drawing over it. ConOut
// keeps its own idea of what is on screen and repaints that region when it
// feels like it -- a screenshot showed a black band across the top third,
// which is its text area put back on top of the splash.
if (ST && ST->ConOut)
ST->ConOut->ClearScreen(ST->ConOut);
ui_init(&uifb);
ui_splash(0);
// From here on, diagnostics go to COM1 rather than the console, or each one
// is drawn across the splash.
SPLASH_UP = ui_active();
print(L"schos: framebuffer "); print_hex(fb);
print(L" "); print_hex(info->HorizontalResolution);
print(L"x"); print_hex(info->VerticalResolution);
print(L"\r\n");
return fb;
}
// ---------------------------------------------------------------------------
// Memory map
//
// UEFI hands out EFI_MEMORY_DESCRIPTORs; the kernel parses E820 entries. The
// translation happens here rather than in the kernel, so that
// kernel/arch/x86_64/e820.c stays the one place that understands a memory map.
// ---------------------------------------------------------------------------
static UINT32
efi_type_to_e820(UINT32 t)
{
switch (t) {
// Boot services memory is ours the moment ExitBootServices returns, and
// the loader's own pages likewise -- the kernel is what runs next.
case EfiConventionalMemory:
case EfiBootServicesCode:
case EfiBootServicesData:
case EfiLoaderCode:
case EfiLoaderData:
return E820_USABLE;
case EfiACPIReclaimMemory:
return E820_ACPI;
case EfiACPIMemoryNVS:
return E820_NVS;
default:
// Runtime services, MMIO, unusable, persistent, reserved: all of it is
// memory the kernel must not allocate from.
return E820_RESERVED;
}
}
// ---------------------------------------------------------------------------
static int
guid_eq(const EFI_GUID *a, const EFI_GUID *b)
{
const UINT8 *p = (const UINT8 *)a, *q = (const UINT8 *)b;
for (UINTN i = 0; i < sizeof(EFI_GUID); i++)
if (p[i] != q[i])
return 0;
return 1;
}
// Translate a UEFI memory map into E820 entries.
//
// Adjacent runs of the same type are coalesced: a real UEFI map is routinely
// over a hundred descriptors and the kernel accepts E820_MAXENTRIES, so
// without this it would see a truncated map and believe the machine has less
// memory than it does. Boot services memory becomes usable here, which is what
// makes the coalescing effective -- it is most of the fragmentation.
//
// A function rather than inline code because it has to run twice: once before
// ExitBootServices, and again if that fails and the map is re-read.
static UINT32
build_e820(const UINT8 *mapbuf, UINTN mapsize, UINTN descsize,
struct e820entry *out)
{
UINT32 nent = 0;
for (UINTN off = 0; off + descsize <= mapsize && nent < E820_MAXENTRIES;
off += descsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off);
UINT32 type = efi_type_to_e820(d->Type);
if (nent > 0 && out[nent-1].type == type &&
out[nent-1].base + out[nent-1].length == d->PhysicalStart) {
out[nent-1].length += d->NumberOfPages * 0x1000;
continue;
}
out[nent].base = d->PhysicalStart;
out[nent].length = d->NumberOfPages * 0x1000;
out[nent].type = type;
out[nent].acpi_ext = 1;
nent++;
}
return nent;
}
// The ACPI RSDP, from the UEFI configuration table.
//
// The kernel's own search scans 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. The kernel therefore found no RSDP, so no MADT, so no IOAPIC,
// so nothing routed IRQ 4: it booted to a shell prompt on polled serial output
// and then ignored every key, which reads as a broken console rather than a
// missing table.
//
// ACPI 2.0 is preferred over 1.0 because its RSDP carries the XSDT, whose
// entries are 64-bit; the 1.0 RSDT is kept only as a fallback.
static UINT64
find_rsdp(void)
{
EFI_GUID acpi20 = EFI_ACPI_20_TABLE_GUID;
EFI_GUID acpi10 = EFI_ACPI_10_TABLE_GUID;
EFI_CONFIGURATION_TABLE *t =
(EFI_CONFIGURATION_TABLE *)ST->ConfigurationTable;
UINT64 fallback = 0;
for (UINTN i = 0; i < ST->NumberOfTableEntries; i++) {
if (guid_eq(&t[i].VendorGuid, &acpi20))
return (UINT64)(UINTN)t[i].VendorTable;
if (guid_eq(&t[i].VendorGuid, &acpi10))
fallback = (UINT64)(UINTN)t[i].VendorTable;
}
return fallback;
}
EFI_STATUS
EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{
ST = systab;
BS = systab->BootServices;
print(L"SchOS UEFI loader\r\n");
// The display comes first, before the kernel is read.
//
// It used to be the other way round, which meant the resolution prompt
// appeared after the slowest step and the progress bar was drawn into a
// framebuffer that did not exist yet -- every tick was silently discarded
// and then the splash painted over the result. Setting the mode first also
// puts the menu where a person expects it: at the start.
UINT32 fb = setup_framebuffer();
EFI_FILE_PROTOCOL *file = open_kernel(image);
UINT64 entry = load_elf(file);
ui_progress_end();
file->Close(file);
print(L"schos: kernel entry "); print_hex(entry); print(L"\r\n");
// Read while boot services still exist and the system table is guaranteed
// valid; written to low memory further down, after the firmware has let go
// of it, for the same reason the e820 map is.
UINT64 rsdp = find_rsdp();
print(L"schos: acpi rsdp "); print_hex(rsdp); print(L"\r\n");
// The memory map, and then out. GetMemoryMap must be the last thing before
// ExitBootServices: the key it returns is invalidated by any allocation,
// and AllocatePool for the map buffer is itself an allocation -- hence the
// static buffer and the retry.
static UINT8 mapbuf[32768];
UINTN mapsize = sizeof mapbuf, mapkey = 0, descsize = 0;
UINT32 descver = 0;
EFI_STATUS st;
st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf,
&mapkey, &descsize, &descver);
if (EFI_ERROR(st)) die(L"GetMemoryMap failed", st);
// Translate before leaving, while the map is still readable and while
// printing still works, but write it to low memory only after the firmware
// has let go -- 0x30000 may well be firmware data until then.
static struct e820entry e820[E820_MAXENTRIES];
UINT32 nent = build_e820(mapbuf, mapsize, descsize, e820);
// Truncation is silent data loss: phystop comes from the largest usable
// region, so dropping the tail of the map can leave the kernel believing a
// 16 GB machine has a fraction of that. Say so while there is still a
// console to say it on -- after ExitBootServices there is nowhere to
// report anything at all.
if (nent >= E820_MAXENTRIES)
print(L"schos: WARNING: memory map truncated, some RAM will be unused\r\n");
print(L"schos: exiting boot services\r\n");
st = BS->ExitBootServices(image, mapkey);
if (EFI_ERROR(st)) {
// The map changed underneath us between GetMemoryMap and here. Take it
// again and retry once; this is normal and the specification expects
// a loader to handle it.
mapsize = sizeof mapbuf;
st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf,
&mapkey, &descsize, &descver);
if (EFI_ERROR(st)) die(L"GetMemoryMap failed on retry", st);
// Translate again. The retry exists because the map *changed*, so
// handing the kernel the table built from the previous one describes
// memory the firmware has already rearranged. This path is normal on
// real firmware and never taken under OVMF, which is why it survived.
nent = build_e820(mapbuf, mapsize, descsize, e820);
st = BS->ExitBootServices(image, mapkey);
if (EFI_ERROR(st)) die(L"ExitBootServices failed", st);
}
// ---- no firmware services exist past this line ----
__asm__ volatile("cli");
copy((void *)SCHBOOT_E820_MAP, e820, nent * sizeof e820[0]);
*(UINT32 *)SCHBOOT_E820_COUNT = nent;
*(UINT64 *)SCHBOOT_RSDP_ADDR = rsdp;
*(UINT8 *)SCHBOOT_VERBOSE_FLAG = (UINT8)VERBOSE_BOOT;
// The page tables mboot_entry would have built. entry64low's first act is
// a movabs to 0xffff8000...., so the high half must be mapped before the
// jump, not after: identity alone triple faults on the first instruction.
UINT64 *pml4 = (UINT64 *)PML4_ADDR;
UINT64 *pdpt = (UINT64 *)PDPT_ADDR;
zero(pml4, 0x1000);
zero(pdpt, 0x1000);
pml4[0] = PDPT_ADDR | PTE_P | PTE_W; // identity
pml4[256] = PDPT_ADDR | PTE_P | PTE_W; // 0xffff800000000000
for (int i = 0; i < 4; i++) // 4 GB in 1 GB pages
pdpt[i] = ((UINT64)i << 30) | PTE_P | PTE_PS | PTE_W;
// RBX carries the framebuffer address into kmain, and must never be 1 --
// that value marks an application processor in entry64high. A physical
// MMIO address or zero, so it cannot collide.
//
// "rbx" is in the clobber list because this writes it, and leaving it out
// is not a tidiness problem -- it is a jump to the wrong address. Without
// it the compiler may allocate an *input* into rbx, and it did: clang 21
// put `entry` there, so the second instruction overwrote the jump target
// with the framebuffer address and the third jumped into video memory. The
// machine then executed pixels until KVM gave up.
//
// movq %rax, %cr3 <- %0, the page tables
// movq %rcx, %rbx <- %1, the framebuffer, into rbx
// jmpq *%rbx <- %2 was rbx, and now holds the framebuffer
//
// It had worked for as long as the compiler happened to choose differently.
// That is the same failure as the BIOS loader's EAX hand-off in
// docs/history/kernel.md section 4: a register the code depends on, that
// nothing in the source actually asks for.
__asm__ volatile(
"mov %0, %%cr3\n\t"
"mov %1, %%rbx\n\t"
"jmp *%2\n\t"
:
: "r"((UINT64)PML4_ADDR), "r"((UINT64)fb), "r"(entry)
: "rbx", "memory");
for (;;)
__asm__ volatile("hlt");
}