SchOS/boot/uefi/main.c
Nexoniarz 726091000e uefi: copy the kernel home and jump from a page it cannot reach
The staged segments were copied home after ExitBootServices by the
loader itself -- on code, a stack and a copy list the firmware had
placed wherever it liked. On an HP all-in-one some of that was in the
kernel's range: "Placing the kernel", then a triple fault and a reset.

The copy and the jump now run from one page below the kernel's lowest
address, allocated as loader code before the firmware exits: a short
position-independent routine, the copy list, and a stack. It switches
to the loader's page tables and its own stack before copying anything.
Called sysv_abi, because UEFI code is the Microsoft convention and the
first try loaded the framebuffer address into rsp.

The framebuffer address handed over in rbx is 64-bit now too; the
kernel already read the full one from the hand-off block.

Also write.sh: writes schos.iso to a USB stick, after showing the
device and asking for its name to be typed back; refuses partitions,
the system disk and non-removable disks; reads the result back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 11:00:02 +02:00

2308 lines
90 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// 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/arch/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.
//
// **These are a contract, not this file's choice.** kernel/arch/x86_64/entry.S
// builds its own bootstrap tables at the same two addresses and loads cr3 with
// 0x1000 by name. Moving them here alone was tried, on the theory that
// hardcoded low memory is a poor assumption under UEFI: the machine
// triple-faulted one instruction after the jump, twice, for two different
// reasons -- and would have needed the kernel's assembly to agree even once
// both were fixed. If it ever does move, it moves in both files at once.
#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.
// ---------------------------------------------------------------------------
static void mirrors_show(void); /* see "the other displays", further down */
static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary);
static void describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag);
static void explain_range(UINT64 lo, UINT64 hi);
static EFI_STATUS map_read(UINTN *size, UINTN *key, UINTN *dsize, UINT32 *dver);
#define MAPBUF_BYTES 262144
static UINT8 mapbuf[MAPBUF_BYTES];
/* 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;
// Remote debugging, off unless somebody picks it out of the boot menu -- and
// the entry that picks it only exists in a build made with DEBUG=1. See
// SCHBOOT_DEBUG_FLAG in include/arch/schboot.h for why the consent is asked for at
// this particular moment.
static int DEBUG_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));
}
/* The menus, on the serial line.
*
* The three screens below paint into the framebuffer, which is the right place
* for them on a machine with a screen and nowhere at all on one driven over a
* serial cable. That is how this loader is usually driven, and f8_window()
* already went out of its way to support it -- it accepts 'o' as well as F8
* precisely because "a serial console sends no scan codes at all". So the
* prompt invited a serial user in, drew them a menu they could not see, and
* then blocked for ever on a key they had no way to choose.
*
* 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 over
* the menu that was just painted there.
*
* A machine with no COM1 reads 0xFF from the status register, serial_putc()
* gives up after its bounded wait, and the write goes nowhere. So this costs
* nothing where it is useless and is the whole interface where it is not. */
static void
serial_puts(const char *s)
{
for (; *s; s++) {
if (*s == '\n')
serial_putc('\r');
serial_putc(*s);
}
}
/* One item, as the selected line or not. The marker is the same width either
* way, so the list does not shift sideways as the selection moves.
*
* `numbered` is not decoration: the number is shown exactly 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 -- numbering it printed ':', ';',
* '<' past the ninth, which is a character nobody can type offering to select
* something. */
static void
serial_item(const char *const *items, int i, int sel, int numbered)
{
serial_puts(i == sel ? " > " : " ");
if (numbered && i < 9) {
char n[4];
n[0] = (char)('1' + i);
n[1] = '.';
n[2] = ' ';
n[3] = 0;
serial_puts(n);
} else if (numbered) {
serial_puts(" "); /* keep the column */
}
serial_puts(items[i]);
serial_puts("\n");
}
static void
serial_menu(const char *title, const char *const *items, int n, int sel,
const char *hint, int numbered)
{
serial_puts("\n-- ");
serial_puts(title);
serial_puts(" --\n");
for (int i = 0; i < n; i++)
serial_item(items, i, sel, numbered);
serial_puts(hint);
serial_puts("\n");
}
/* Diagnostics: only on a verbose boot.
*
* These four lines say where the framebuffer is, where the kernel went and
* where the ACPI tables are. Worth having when a machine will not boot, and
* worth nothing on the thousand boots that work -- and they are the first
* thing on the serial line every time, so they set the tone for everything
* after. vprint() is print() with that decision built in. */
static void vprint(const CHAR16 *s);
static void vprint_hex(UINT64 v);
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);
}
// A number in the base a person reads sizes in. `u64_to_dec` is further down
// and works on char; this is the CHAR16 the firmware console wants.
static void
print_dec(UINT64 v)
{
CHAR16 tmp[24], out[25];
int n = 0, i = 0;
if (v == 0) tmp[n++] = L'0';
while (v && n < 23) { tmp[n++] = (CHAR16)(L'0' + (v % 10)); v /= 10; }
while (n > 0) out[i++] = tmp[--n];
out[i] = 0;
print(out);
}
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);
}
static void
vprint(const CHAR16 *s)
{
if (VERBOSE_BOOT)
print(s);
}
static void
vprint_hex(UINT64 v)
{
if (VERBOSE_BOOT)
print_hex(v);
}
// 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
// ---------------------------------------------------------------------------
// The volume this loader came off, kept so that anything else read at boot is
// read from the same place the kernel was. Set by open_kernel().
static EFI_FILE_PROTOCOL *boot_root;
// Where the firmware put this loader, for load_elf() to check the kernel is
// not about to be copied over it. Set by open_boot_volume().
static UINT64 self_base, self_size;
// Opened before the display is touched, because what is on the volume decides
// some of what the display does -- see nomirror in mirrors_find().
static void
open_boot_volume(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_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);
self_base = (UINT64)(UINTN)li->ImageBase;
self_size = li->ImageSize;
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, &boot_root);
if (EFI_ERROR(st)) die(L"cannot open the boot volume", st);
}
// Is there a file of this name on the boot volume? Used for the switches that
// have to be answerable before there is any way to ask a person.
static int
boot_file_exists(const CHAR16 *path)
{
EFI_FILE_PROTOCOL *f = 0;
if (!boot_root)
return 0;
if (EFI_ERROR(boot_root->Open(boot_root, &f, (CHAR16 *)path,
EFI_FILE_MODE_READ, 0)) || !f)
return 0;
f->Close(f);
return 1;
}
static EFI_FILE_PROTOCOL *
open_kernel(EFI_HANDLE image)
{
EFI_FILE_PROTOCOL *root = boot_root, *file = 0;
EFI_STATUS st;
(void)image;
if (!root) die(L"boot volume was not opened", 0);
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;
}
// ---------------------------------------------------------------------------
// The console typeface
//
// See SCHBOOT_FONT_ADDR in include/arch/schboot.h for why this is the loader's job
// and not the kernel's. Everything here is best-effort: a machine with no
// console.ttf on its ESP boots with the font in the kernel image, which is
// the same font, and says nothing about it.
// ---------------------------------------------------------------------------
#define FONT_PATH L"\\schos\\console.ttf"
#define FONT_PATH_ALT L"\\console.ttf"
// Four megabytes. Large enough for any single-weight TrueType face -- the one
// in the kernel image is 112 KB -- and small enough that a wrong file cannot
// eat the machine's memory before the kernel has any.
#define FONT_MAX (4u * 1024u * 1024u)
static void
load_console_font(void)
{
EFI_FILE_PROTOCOL *file = 0;
EFI_STATUS st;
UINT64 size = 0;
EFI_PHYSICAL_ADDRESS at;
UINTN read;
// Absent until proven present: the kernel reads these two fields with no
// other way to know whether the loader wrote them.
*(UINT64 *)SCHBOOT_FONT_ADDR = 0;
*(UINT32 *)SCHBOOT_FONT_LEN = 0;
if (!boot_root)
return;
st = boot_root->Open(boot_root, &file, (CHAR16 *)FONT_PATH,
EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st))
st = boot_root->Open(boot_root, &file, (CHAR16 *)FONT_PATH_ALT,
EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st) || !file)
return; /* no console.ttf; the built-in one stands */
// The size, without EFI_FILE_INFO and its GUID: seek past the end and ask
// where that landed. The specification requires SetPosition beyond EOF to
// clamp to the end, which is exactly the number wanted.
if (EFI_ERROR(file->SetPosition(file, 0xFFFFFFFFFFFFFFFFULL)) ||
EFI_ERROR(file->GetPosition(file, &size)) ||
EFI_ERROR(file->SetPosition(file, 0))) {
file->Close(file);
return;
}
if (size < 4096 || size > FONT_MAX) {
vprint(L"schos: console.ttf is an implausible size; ignoring it\r\n");
file->Close(file);
return;
}
// 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.
//
// Reserved rather than loader data: EfiLoaderData becomes E820_USABLE in
// the handoff, on the argument that the loader's own pages are the
// kernel's the moment it runs. True of the loader; not true of this.
at = 0xFFFFFFFFULL;
st = BS->AllocatePages(AllocateMaxAddress, EfiReservedMemoryType,
(UINTN)((size + 0xFFF) / 0x1000), &at);
if (EFI_ERROR(st)) {
vprint(L"schos: no memory for console.ttf; using the built-in font\r\n");
file->Close(file);
return;
}
read = (UINTN)size;
st = file->Read(file, &read, (void *)(UINTN)at);
file->Close(file);
if (EFI_ERROR(st) || read != size) {
vprint(L"schos: console.ttf would not read; using the built-in font\r\n");
return; /* the pages stay reserved; harmless */
}
*(UINT64 *)SCHBOOT_FONT_ADDR = (UINT64)at;
*(UINT32 *)SCHBOOT_FONT_LEN = (UINT32)size;
vprint(L"schos: console font "); vprint_hex((UINT32)size);
vprint(L" bytes at "); vprint_hex((UINT32)at); vprint(L"\r\n");
}
/* ---------------------------------------------------------------------------
* Where the kernel goes, while the firmware is still using it
*
* The kernel is placed at fixed physical addresses, 1 MB up to about 34 MB
* with the RAM disk inside it, and nothing promises that range is free while
* boot services run. On OVMF most of it is; on an HP all-in-one from 2012 it
* was not, and the loader used to say "could not reserve" and write the
* kernel there anyway -- over the firmware's own data, with the firmware
* still running on it. The machine froze at the start of the loading bar.
*
* So: every free page of the range is claimed first, which also stops the
* firmware handing any of it out as a staging buffer. A segment wholly on
* claimed pages is read straight home. One that is not is read into a buffer
* anywhere else and copied home in place_staged(), after ExitBootServices,
* when boot-services memory is the loader's to overwrite.
* ------------------------------------------------------------------------- */
#define NCLAIM 64
static struct { UINT64 lo, hi; } claims[NCLAIM];
static UINTN nclaim;
#define NLATER 16
static struct { UINT64 dst, src, len; } later[NLATER];
static UINTN nlater;
static void
claim_span(UINT64 lo, UINT64 hi)
{
UINTN size, key, dsize;
UINT32 dver;
int foreign = 0;
if (EFI_ERROR(map_read(&size, &key, &dsize, &dver)) || !dsize)
return;
/* A copy, because every AllocatePages below changes the map. */
static UINT8 snap[MAPBUF_BYTES];
copy(snap, mapbuf, size);
for (UINTN off = 0; off + dsize <= size; off += dsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(snap + off);
UINT64 a = d->PhysicalStart, b = a + d->NumberOfPages * 4096;
if (b <= lo || a >= hi)
continue;
if (a < lo) a = lo;
if (b > hi) b = hi;
if (d->Type != EfiConventionalMemory) {
/* Boot-services memory is free once the firmware exits; anything
* else under the kernel outlives it, and is worth saying. */
if (d->Type != EfiBootServicesCode && d->Type != EfiBootServicesData)
foreign = 1;
continue;
}
EFI_PHYSICAL_ADDRESS at = a;
if (nclaim < NCLAIM &&
!EFI_ERROR(BS->AllocatePages(AllocateAddress, EfiLoaderData,
(UINTN)((b - a) / 4096), &at))) {
claims[nclaim].lo = a;
claims[nclaim].hi = b;
nclaim++;
}
}
if (foreign) {
print(L"schos: warning: firmware memory that outlives it lies under the kernel:\r\n");
explain_range(lo, hi);
}
/* This loader, or the stack it runs on, inside the range would be copied
* over by place_staged() -- the firmware chose both, and there is no
* recovering from it here. Said, so a machine that dies there says why. */
UINT64 sp = (UINT64)(UINTN)&size;
if ((self_base < hi && self_base + self_size > lo) || (sp >= lo && sp < hi))
print(L"schos: warning: the loader itself sits where the kernel goes\r\n");
}
/* Whether every page of [lo, hi) is one claim_span() took. */
static int
claim_covers(UINT64 lo, UINT64 hi)
{
while (lo < hi) {
UINTN i;
for (i = 0; i < nclaim; i++)
if (claims[i].lo <= lo && lo < claims[i].hi)
break;
if (i == nclaim)
return 0;
lo = claims[i].hi;
}
return 1;
}
/* ---------------------------------------------------------------------------
* The last jump, from somewhere the kernel cannot land
*
* The staged segments are copied home after ExitBootServices -- and this
* loader is still running then, on code, a stack and a copy list the firmware
* placed wherever it liked. On an HP all-in-one some of that was inside the
* kernel's range, so the copy overwrote the loader while it ran: the screen
* said "Placing the kernel" and the machine reset, a triple fault, with
* "Starting the kernel" never drawn.
*
* So the copy and the jump run from one page of their own, below the kernel's
* lowest address and so outside anything it occupies: this routine at the start of it,
* the copy list after it, a stack at the end. It is position independent --
* every address it uses arrives in a register -- so it runs wherever it is
* copied. It switches to the loader's page tables (which map all memory 1:1)
* and its own stack first, so nothing it needs afterwards is anywhere the
* copies can reach. The buffers being copied *from* are outside the kernel's
* range too: claim_span() took every free page inside it.
*
* rdi copy list: { dst, src, len } x n rsi n
* rdx kernel entry rcx framebuffer, for rbx
* r8 page tables r9 stack top
*
* RBX carries the framebuffer address into kmain and must never be 1 -- that
* value marks an application processor in entry64high. A physical address or
* zero cannot collide. (The jump this replaces was inline asm, where leaving
* rbx out of the clobbers once let clang put the jump target in it; written
* out in full here, every register is spelled.)
* ------------------------------------------------------------------------- */
__asm__(
".text\n"
".globl schos_tramp\n"
".globl schos_tramp_end\n"
"schos_tramp:\n"
" mov %r9, %rsp\n"
" mov %r8, %cr3\n"
" mov %rdx, %r10\n"
" mov %rcx, %rbx\n"
" mov %rdi, %r11\n"
" mov %rsi, %r9\n"
" cld\n"
"1: test %r9, %r9\n"
" jz 2f\n"
" mov 0(%r11), %rdi\n"
" mov 8(%r11), %rsi\n"
" mov 16(%r11), %rcx\n"
" rep movsb\n"
" add $24, %r11\n"
" dec %r9\n"
" jmp 1b\n"
"2: jmp *%r10\n"
"schos_tramp_end:\n");
extern const UINT8 schos_tramp[], schos_tramp_end[];
#define TRAMP_LIST 256 /* the copy list's offset in the page */
static EFI_PHYSICAL_ADDRESS tramp_page;
static UINT64 kern_lo; /* the kernel's lowest address; set by load_elf() */
/* Before ExitBootServices: the page, below the kernel and above the first
* megabyte -- the hand-off block and the page tables are written down there
* after the firmware exits. Loader *code*, not data: a firmware that maps
* data pages no-execute (OVMF does) would fault on the first instruction. */
static void
tramp_reserve(void)
{
EFI_PHYSICAL_ADDRESS a = kern_lo - 1;
if (EFI_ERROR(BS->AllocatePages(AllocateMaxAddress, EfiLoaderCode, 1, &a)) ||
a < 0x100000)
die(L"no page below 16 MB for the final jump", 0);
tramp_page = a;
}
/* After ExitBootServices, and the last thing this loader does. */
static void __attribute__((noreturn))
tramp_go(UINT64 entry, UINT64 fb)
{
UINT8 *page = (UINT8 *)(UINTN)tramp_page;
copy(page, schos_tramp, (UINTN)(schos_tramp_end - schos_tramp));
copy(page + TRAMP_LIST, later, nlater * sizeof later[0]);
// sysv_abi: the registers above are System V's argument order. This
// loader is compiled for UEFI, which is the *Microsoft* convention (rcx,
// rdx, r8, r9, then the stack), and without the attribute the first try
// loaded the framebuffer address into rsp and a kernel address into cr3.
typedef void __attribute__((sysv_abi)) tramp_fn(void *, UINT64, UINT64,
UINT64, UINT64, UINT64);
tramp_fn *go = (tramp_fn *)page;
go(page + TRAMP_LIST, nlater, entry, fb, PML4_ADDR, tramp_page + 4096);
for (;;)
__asm__ volatile("hlt");
}
// Load PT_LOAD segments to p_paddr. Physical addresses, not virtual: the
// kernel is linked at 0xffff800001000000 but must be *placed* at 0x1000000, 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;
UINT64 lowest_paddr = ~0ULL, span_hi = 0;
UINT8 *first = 0; /* the lowest segment's bytes, wherever they are */
// 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 && ph.p_memsz) {
total += ph.p_memsz;
if (ph.p_paddr < lowest_paddr) lowest_paddr = ph.p_paddr;
if (ph.p_paddr + ph.p_memsz > span_hi) span_hi = ph.p_paddr + ph.p_memsz;
}
}
ui_progress_begin("Loading SchOS", (ui_u32)(total ? total : 1));
if (span_hi)
claim_span(lowest_paddr & ~0xfffULL, (span_hi + 0xfff) & ~0xfffULL);
kern_lo = lowest_paddr & ~0xfffULL;
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;
// Where this segment's bytes go now: home, when every page of it is
// ours, and a buffer anywhere else when it is not -- see claim_span()
// for why writing home regardless froze an HP all-in-one.
EFI_PHYSICAL_ADDRESS at = ph.p_paddr & ~0xfffULL;
EFI_PHYSICAL_ADDRESS end = (ph.p_paddr + ph.p_memsz + 0xfff) & ~0xfffULL;
UINT8 *dst = (UINT8 *)(UINTN)ph.p_paddr;
if (!claim_covers(at, end)) {
EFI_PHYSICAL_ADDRESS buf = 0;
UINTN pages = (UINTN)((ph.p_memsz + 0xfff) / 0x1000);
if (nlater == NLATER)
die(L"too many kernel segments to stage", 0);
st = BS->AllocatePages(AllocateAnyPages, EfiLoaderData, pages, &buf);
if (EFI_ERROR(st))
die(L"no memory to stage a kernel segment in", st);
vprint(L"schos: staging "); vprint_hex(ph.p_paddr);
vprint(L" at "); vprint_hex(buf); vprint(L"\r\n");
later[nlater].dst = ph.p_paddr;
later[nlater].src = buf;
later[nlater].len = ph.p_memsz;
nlater++;
dst = (UINT8 *)(UINTN)buf;
}
if (ph.p_paddr == lowest_paddr)
first = dst;
// A megabyte at a time, so the bar moves through a 30 MB segment
// instead of sitting at its start: on a machine that stops here, where
// it stopped is the only clue there is. Old firmware's USB stacks are
// also happier with reads that size than with one of 30 MB.
UINT64 left = ph.p_filesz, off = 0;
if (left) {
st = file->SetPosition(file, ph.p_offset);
if (EFI_ERROR(st)) die(L"cannot seek a segment", st);
}
while (left) {
n = (UINTN)(left < 0x100000 ? left : 0x100000);
UINTN want = n;
st = file->Read(file, &n, dst + off);
if (EFI_ERROR(st) || n != want)
die(L"short read loading a segment", st);
off += n;
left -= n;
ui_progress_tick((ui_u32)(done + off));
}
// .bss: present in memsz, absent from filesz, and the kernel assumes
// it is zeroed.
if (ph.p_memsz > ph.p_filesz)
zero(dst + 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 = first ? first : (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;
// Formats 0 and 1 are the two with a linear 32-bit layout. 2 is a bitmask
// format nobody ships and 3 is Blt-only, which has no aperture at all --
// see the virtio GOP, and the reboot loop it once caused.
if (mi->PixelFormat > 1) return 0;
if (mi->HorizontalResolution < 640 || mi->VerticalResolution < 480) return 0;
// A padded row is not a broken mode. This used to require
// PixelsPerScanLine == HorizontalResolution, which is true of QEMU and
// routinely false of real hardware: a GPU is entitled to align each row,
// and 1920x1080 arriving with a scanline of 1984 or 2048 pixels is
// ordinary. The stride is carried through to the kernel, so the only
// thing this rejected was every machine that padded -- including,
// probably, the laptop that could not be offered 1920x1080.
if (mi->PixelsPerScanLine < mi->HorizontalResolution) return 0;
// The width-divisible-by-eight rule went with it. It excluded 1366x768,
// which is the panel on a very large number of laptops, and protected
// nothing: the drawing code steps by the stride and has done for as long
// as there has been a stride to step by.
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");
mirrors_show();
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;
}
}
/* Set a mode, and keep it only if the firmware's own account of it adds up.
*
* A GOP can accept a mode and then describe it in a way the display does not
* scan out: a row length that is not the one being shown, or an aperture too
* small for the rows it claims. Drawn into anyway, that is the picture sheared
* into dotted lines, or a logo repeated across the screen, both of which a
* desktop with an NVIDIA card showed at sizes other than 800x600. The numbers
* cannot prove a mode is right, but they can prove one is wrong, and a wrong
* one is refused here, in the menu, where there is still a screen to say so on.
*/
static int
mode_set_checked(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, UINT32 mode)
{
UINT32 was = gop->Mode->Mode;
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *want = 0;
UINTN sz = 0;
if (EFI_ERROR(gop->QueryMode(gop, mode, &sz, &want)) || !want)
return 0;
if (EFI_ERROR(gop->SetMode(gop, mode)))
return 0;
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *got = gop->Mode->Info;
UINT64 need = (UINT64)got->PixelsPerScanLine * 4 * got->VerticalResolution;
if (got->HorizontalResolution == want->HorizontalResolution &&
got->VerticalResolution == want->VerticalResolution &&
got->PixelsPerScanLine >= got->HorizontalResolution &&
gop->Mode->FrameBufferBase &&
gop->Mode->FrameBufferSize >= need)
return 1;
describe_gop(gop, L"schos: mode does not add up, going back:");
gop->SetMode(gop, was);
return 0;
}
/* Said on the screen that is still working, then back to the menu. */
static void
res_refused(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{
struct ui_fb uifb;
EFI_INPUT_KEY key;
const char *msg[1] = { "The display refused that size -- keeping this one" };
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);
ui_menu("Resolution", msg, 1, 0, "any key to go back");
mirrors_show();
for (;;) {
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
break;
BS->Stall(10000);
}
}
static void
res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{
int sel = 0;
int echoed = -1;
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 or Q to go back");
if (echoed < 0) {
serial_menu("Resolution", items, RES_N + 1, sel,
"W/S to choose, Enter to apply, Esc or Q to go back", 0);
} else if (echoed != sel) {
serial_item(items, sel, sel, 0);
}
echoed = sel;
mirrors_show();
EFI_INPUT_KEY key;
for (;;) {
if (ST->ConIn &&
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
break;
BS->Stall(10000);
}
CHAR16 c = key.UnicodeChar;
/* Q as well as Esc, for the same reason f8_window() accepts 'o' as
* well as F8: on a serial line a lone ESC byte is indistinguishable
* from the start of an escape sequence, so the terminal driver holds
* it back waiting for the rest and it never arrives as a key. Esc is
* the obvious thing to press on a screen and unreachable on a cable. */
if (key.ScanCode == 0x17 || c == 27 || c == L'q' || c == L'Q')
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) {
if (!mode_set_checked(gop, RES_MODE[sel])) {
res_refused(gop);
return;
}
// The one other time the mirrors' modes are worth touching:
// the person just said what size the display is.
mirrors_match(gop);
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) {
if (!mode_set_checked(gop, i)) {
res_refused(gop);
return;
}
mirrors_match(gop);
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");
mirrors_show();
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. */
// The firmware's memory map, read into the one buffer there is.
//
// There used to be two of these, each with its own guess at how big a memory
// map gets -- 32 KB where it mattered and 16 KB here -- and both guesses were
// wrong on real hardware. The one here was the worse of the two, because it
// did not fail loudly: GetMemoryMap answered EFI_BUFFER_TOO_SMALL, this
// returned zero, and the System Information screen reported **0 MB usable** on
// a machine with sixteen gigabytes in it.
//
// So: one buffer, one function, and the size comes from the firmware rather
// than from an estimate. A call with a zero size fails on purpose and fills in
// what is needed; it allocates nothing, so a caller that needs the map key for
// ExitBootServices still has a valid one.
static EFI_STATUS
map_read(UINTN *size, UINTN *key, UINTN *dsize, UINT32 *dver)
{
EFI_STATUS st;
*size = 0;
st = BS->GetMemoryMap(size, (EFI_MEMORY_DESCRIPTOR *)mapbuf,
key, dsize, dver);
if (st == EFI_BUFFER_TOO_SMALL && *size > sizeof mapbuf)
return st; /* the caller decides how loudly to complain */
*size = sizeof mapbuf;
return BS->GetMemoryMap(size, (EFI_MEMORY_DESCRIPTOR *)mapbuf,
key, dsize, dver);
}
/* Who owns [lo, hi), when it was not ours to take.
*
* claim_span() calls this when something under the kernel outlives the
* firmware. Which thing decides whether it matters: boot-services memory is
* the firmware's until ExitBootServices and the kernel's after it, while
* runtime services or ACPI tables under the kernel are a machine that will
* corrupt itself. So every descriptor in the range that is not free, and not
* the loader's own, is named. */
static void
explain_range(UINT64 lo, UINT64 hi)
{
UINTN size, key, dsize;
UINT32 dver;
if (EFI_ERROR(map_read(&size, &key, &dsize, &dver)) || !dsize)
return;
for (UINTN off = 0; off + dsize <= size; off += dsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off);
UINT64 a = d->PhysicalStart, b = a + d->NumberOfPages * 4096;
/* Free memory, and what claim_span() just took, are not in the way. */
if (b <= lo || a >= hi || d->Type == EfiConventionalMemory ||
d->Type == EfiLoaderData)
continue;
print(L"schos: type "); print_dec(d->Type);
print(L" at "); print_hex(a); print(L".."); print_hex(b);
print(L"\r\n");
}
}
static UINT64
memory_mb(void)
{
UINTN size = 0, mapkey = 0, dsize = 0;
UINT32 dver = 0;
UINT64 pages = 0;
if (EFI_ERROR(map_read(&size, &mapkey, &dsize, &dver)))
return 0;
for (UINTN off = 0; off + dsize <= size; off += dsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + 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");
serial_puts("\n-- System Information --\n");
for (int i = 0; i < 7; i++) {
serial_puts(" ");
serial_puts(labels[i]);
serial_puts(" ");
serial_puts(values[i]);
serial_puts("\n");
}
serial_puts("Any key to go back\n");
mirrors_show();
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;
int echoed = -1; // the serial copy: header once, then moves
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);
// Six entries in a debug build, five in a release one.
//
// The debugger entry is compiled out rather than hidden, because a
// menu item that is present and refuses is an invitation to work out
// how to make it not refuse. An ISO built for anybody else has no way
// to turn a network debugger on, because the code that would set the
// flag is not in it.
const char *items[6];
int nitems = 5;
items[0] = "Boot SchOS";
items[1] = "Boot SchOS (verbose)";
items[2] = resbuf;
items[3] = "System Information";
items[4] = "Reboot to firmware";
#ifdef SCHOS_DEBUG
items[5] = DEBUG_BOOT ? "Remote debugging: on (schdbgd, port 4444)"
: "Remote debugging: off";
nitems = 6;
#endif
ui_menu("Boot Manager", items, nitems, selected,
nitems == 6 ? "W/S or 1-6 to choose, Enter to select"
: "W/S or 1-5 to choose, Enter to select");
// The same menu on the serial line: the whole thing once, then one
// line per *move*. Reprinting on every keypress would be correct and
// unreadable -- a serial terminal has no cursor to paint over, so an
// unrecognised key would scroll the selection past again for nothing.
if (echoed < 0) {
serial_menu("Boot Manager", items, nitems, selected,
nitems == 6 ? "W/S or 1-6 to choose, Enter to select"
: "W/S or 1-5 to choose, Enter to select", 1);
} else if (echoed != selected) {
serial_item(items, selected, selected, 1);
}
echoed = selected;
mirrors_show();
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 + nitems - 1) % nitems; continue; }
if (scan == 0x02) { selected = (selected + 1) % nitems; continue; }
if (c == L'w' || c == L'W') { selected = (selected + nitems - 1) % nitems; continue; }
if (c == L's' || c == L'S') { selected = (selected + 1) % 5; continue; }
if (c >= L'1' && c <= L'0' + nitems) { 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; }
#ifdef SCHOS_DEBUG
// A toggle rather than an action: picking it changes what the
// entry says and leaves you in the menu, so the state you booted
// with is the state you can see before you boot.
if (selected == 5) { DEBUG_BOOT = !DEBUG_BOOT; echoed = -1; continue; }
#endif
if (selected == 4) {
ui_clear();
ui_text_centered((int)ui_height() / 2, "Halted -- reset to reach firmware setup",
0xB9BEC7u);
for (;;) __asm__ volatile("hlt");
}
}
}
}
/* --- the other displays -----------------------------------------------------
*
* A machine with two adapters has two screens, and the loader drew on one of
* them. The other was black through the splash, black through the F8 menu, and
* black until vgpud started several seconds into userspace -- which from the
* outside is indistinguishable from a display that does not work.
*
* LocateProtocol() answers with the first GOP and offers no way to ask for the
* rest, so the rest are found by handle. Each one is then fed the *primary's
* framebuffer* as a Blt source: the loader keeps drawing into one piece of
* memory exactly as before, and this copies it outward at the points where a
* screen is finished.
*
* Blt rather than a second framebuffer to draw into, because a virtio GOP does
* not have one -- it reports a FrameBufferBase of zero and implements Blt and
* nothing else. Which is the same fact that made a virtio-only machine
* reboot-loop; see kernel/core/main.c.
*/
#define MAX_MIRRORS 4
static EFI_GRAPHICS_OUTPUT_PROTOCOL *mirror_gop[MAX_MIRRORS];
static EFI_GRAPHICS_OUTPUT_PROTOCOL *primary_gop;
static UINTN mirror_n;
static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary);
static void describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag);
static void
mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
{
EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
EFI_HANDLE *handles = 0;
UINTN count = 0;
primary_gop = primary;
// An off switch, because this is a nicety and booting is not.
//
// Everything below talks to display adapters the machine may be using for
// something else, through firmware drivers of wildly varying quality. If
// that goes wrong there is no console left to say so on and no way to ask
// a person anything -- so the answer has to already be on the volume.
// `\schos\nomirror` is an empty file whose only job is to exist.
if (boot_file_exists(L"\\schos\\nomirror")) {
print(L"schos: \\schos\\nomirror present; not mirroring\r\n");
return;
}
if (!BS->LocateHandleBuffer ||
EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0,
&count, &handles)) || !handles)
return;
for (UINTN i = 0; i < count && mirror_n < MAX_MIRRORS; i++) {
EFI_GRAPHICS_OUTPUT_PROTOCOL *g = 0;
if (EFI_ERROR(BS->HandleProtocol(handles[i], &gop_guid, (void **)&g)))
continue;
if (!g || g == primary || !g->Blt || !g->Mode || !g->Mode->Info)
continue;
// Not a second display: the console splitter. Firmware installs one
// GOP of its own on a virtual handle and forwards every Blt to all of
// the real ones -- the primary included. Mirroring onto it copied the
// primary onto itself through the splitter's idea of the mode, which
// stops being the primary's the moment the resolution menu changes
// it: the picture landed again, offset and with the wrong row length,
// over the one already there. That was the doubled, sheared screen with
// the logo's edges repeated down it, at every size but the one the
// firmware booted in. A real adapter's GOP is on a handle with a path
// to the device; the splitter's has none.
void *dp = 0;
EFI_GUID dp_guid = EFI_DEVICE_PATH_PROTOCOL_GUID;
if (EFI_ERROR(BS->HandleProtocol(handles[i], &dp_guid, &dp)) || !dp)
continue;
// Nor is a second GOP whose aperture is the primary's: one panel
// reached twice, which laptops do. Blitting onto it rewrites the
// primary's own memory in another mode's layout.
UINT64 pb = primary->Mode->FrameBufferBase;
UINT64 pe = pb + primary->Mode->FrameBufferSize;
UINT64 gb = g->Mode->FrameBufferBase;
UINT64 ge = gb + g->Mode->FrameBufferSize;
if (gb && pb && gb < pe && pb < ge)
continue;
mirror_gop[mirror_n++] = g;
}
if (mirror_n)
mirrors_match(primary);
}
/* Put every mirror into the primary's mode, if it has one.
*
* Without this, choosing 1024x768 at the F8 menu changed the screen it was
* chosen on and left the other one at whatever the firmware had picked -- so
* the menu and then the splash appeared on it as a small rectangle in the
* middle of a larger black one. The resolution in that menu is a statement
* about the machine's display, not about one adapter of it.
*
* Cheap to call on every frame: the common case is two integer comparisons and
* no mode set at all. A mirror with no matching mode keeps the one it has and
* gets the centred-and-cropped treatment below, which is still better than
* nothing.
*/
static void
mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary)
{
// Called from mirrors_find(), before anything has been drawn, and again
// when the resolution menu sets a mode -- never per frame. It used to be
// called from mirrors_show() -- which is to say on
// every menu redraw and on every segment of the kernel load -- and that is
// not a safe thing to do to another display adapter.
//
// Setting a mode is not a cheap call on real hardware. It can take the
// best part of a second, it can blank a panel while it happens, and on a
// laptop the "other" adapter is quite often the *same* panel reached
// through a second GOP -- so doing it repeatedly, in the middle of drawing
// a progress bar, is a good way to produce a machine that shows a
// background colour and nothing else while the loader waits on a firmware
// call that is not coming back.
UINT32 w = primary->Mode->Info->HorizontalResolution;
UINT32 h = primary->Mode->Info->VerticalResolution;
for (UINTN i = 0; i < mirror_n; i++) {
EFI_GRAPHICS_OUTPUT_PROTOCOL *g = mirror_gop[i];
if (g->Mode->Info->HorizontalResolution == w &&
g->Mode->Info->VerticalResolution == h)
continue;
for (UINT32 m = 0; m < g->Mode->MaxMode; m++) {
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = 0;
UINTN sz = 0;
if (EFI_ERROR(g->QueryMode(g, m, &sz, &mi)) || !mi)
continue;
if (mi->HorizontalResolution == w && mi->VerticalResolution == h) {
g->SetMode(g, m);
break;
}
}
}
}
/* Put what is on the primary onto all of them. Centred when the sizes differ,
* cropped when the other display is smaller -- the same rules the kernel's
* console mirror and vgpud's both use, because it is the same picture. */
static void
mirrors_show(void)
{
EFI_GRAPHICS_OUTPUT_PROTOCOL *primary = primary_gop;
if (!mirror_n || !primary || !primary->Mode || !primary->Mode->Info)
return;
if (!primary->Mode->FrameBufferBase)
return; /* nothing to copy *from* */
UINTN sw = primary->Mode->Info->HorizontalResolution;
UINTN sh = primary->Mode->Info->VerticalResolution;
UINTN delta = (UINTN)primary->Mode->Info->PixelsPerScanLine * 4;
for (UINTN i = 0; i < mirror_n; i++) {
EFI_GRAPHICS_OUTPUT_PROTOCOL *g = mirror_gop[i];
UINTN w = g->Mode->Info->HorizontalResolution;
UINTN h = g->Mode->Info->VerticalResolution;
UINTN cw = w < sw ? w : sw;
UINTN ch = h < sh ? h : sh;
// One refusal and the whole feature stops for this boot. A firmware
// Blt that fails once will fail every time, and retrying it on every
// screen is how a cosmetic feature turns into a hang.
if (EFI_ERROR(g->Blt(g, (void *)(UINTN)primary->Mode->FrameBufferBase,
EfiBltBufferToVideo, 0, 0,
(w - cw) / 2, (h - ch) / 2, cw, ch, delta))) {
mirror_n = 0;
return;
}
}
}
// ---------------------------------------------------------------------------
// Choosing a display, and saying what was chosen
//
// LocateProtocol answers with *a* GOP. On a laptop with switchable graphics
// there are several -- the integrated adapter, the discrete one, sometimes a
// console splitter on top of both -- and which one comes back first is not
// something the specification says anything about. Drawing into the wrong one
// is a black screen with the firmware's cursor still blinking on it, which is
// indistinguishable from a loader that crashed.
//
// So all of them are looked at, and the choice is made on what they report
// rather than on the order they were handed over.
// ---------------------------------------------------------------------------
static const CHAR16 *
fmt_name(UINT32 f)
{
switch (f) {
case 0: return L"RGBX";
case 1: return L"BGRX";
case 2: return L"bitmask";
default: return L"Blt-only";
}
}
static void
describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag)
{
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = g->Mode ? g->Mode->Info : 0;
print(tag);
if (!mi) { print(L" (no mode information)\r\n"); return; }
print(L" "); print_dec(mi->HorizontalResolution);
print(L"x"); print_dec(mi->VerticalResolution);
print(L" "); print(fmt_name(mi->PixelFormat));
print(L" scanline "); print_dec(mi->PixelsPerScanLine);
print(L" fb "); print_hex(g->Mode->FrameBufferBase);
print(L" size "); print_hex(g->Mode->FrameBufferSize);
print(L"\r\n");
}
// Better, for this purpose, means: it has an aperture we can write into, its
// current mode is one we can read, and failing a tie, it is bigger. A GOP with
// no framebuffer address is not a candidate at all -- the kernel needs a
// linear aperture and cannot Blt.
static int
gop_score(EFI_GRAPHICS_OUTPUT_PROTOCOL *g)
{
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = g->Mode ? g->Mode->Info : 0;
if (!mi || !g->Mode->FrameBufferBase || !g->Mode->FrameBufferSize)
return 0;
if (!mode_usable(mi))
return 1; /* has memory; mode needs changing */
return 2;
}
static EFI_GRAPHICS_OUTPUT_PROTOCOL *
pick_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *first)
{
EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
EFI_GRAPHICS_OUTPUT_PROTOCOL *best = first;
EFI_HANDLE *handles = 0;
UINTN count = 0, i;
int best_score = first ? gop_score(first) : 0;
UINT64 best_area = 0;
if (first && first->Mode && first->Mode->Info)
best_area = (UINT64)first->Mode->Info->HorizontalResolution *
first->Mode->Info->VerticalResolution;
if (!BS->LocateHandleBuffer ||
EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0,
&count, &handles)) || !handles)
return best;
print(L"schos: "); print_dec((UINT32)count); print(L" display adapter(s)\r\n");
for (i = 0; i < count; i++) {
EFI_GRAPHICS_OUTPUT_PROTOCOL *g = 0;
int score;
UINT64 area;
if (EFI_ERROR(BS->HandleProtocol(handles[i], &gop_guid, (void **)&g)) ||
!g || !g->Mode)
continue;
describe_gop(g, L" gop:");
score = gop_score(g);
area = g->Mode->Info
? (UINT64)g->Mode->Info->HorizontalResolution *
g->Mode->Info->VerticalResolution
: 0;
if (score > best_score || (score == best_score && area > best_area)) {
best = g;
best_score = score;
best_area = area;
}
}
return best;
}
/* 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 UINT64
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;
}
// Which adapter, on a machine that has more than one. Everything this
// prints goes to the firmware's text console, which is cleared a few lines
// below before the splash is drawn -- so on a boot that works nobody ever
// sees it, and on a boot that dies before the splash it is the only thing
// on the screen.
gop = pick_gop(gop);
describe_gop(gop, L"schos: display");
// No aperture anywhere means no graphical console, and saying so beats
// leaving the firmware's cursor blinking on a black screen while the
// kernel starts behind it.
if (!gop->Mode || !gop->Mode->FrameBufferBase || !gop->Mode->FrameBufferSize) {
print(L"schos: this display has no linear framebuffer, only Blt.\r\n"
L" The kernel needs an aperture it can write to, so the\r\n"
L" console will be serial-only. Boot continues.\r\n");
return 0;
}
// The other displays, before anything is drawn: the F8 menu is the first
// thing on screen and there is no reason for it to be the one screen that
// does not reach them.
mirrors_find(gop);
// 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.
//
// And say so again. The description printed before the menu is of the mode
// the firmware left; this one is of the mode actually being handed to the
// kernel, and on a machine where changing the resolution loses the splash
// entirely, the difference between the two lines is the bug report. It
// costs nothing on a boot that works -- ClearScreen is a few lines below.
describe_gop(gop, L"schos: booting with");
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info = gop->Mode->Info;
// **Sixty-four bits, and not truncated.** `(UINT32)FrameBufferBase` is
// what this was, and on a machine whose aperture sits above 4 GB -- any
// laptop with a discrete GPU -- it produced a plausible-looking address
// pointing at nothing. The splash went into it, the kernel was handed it,
// and the screen kept the firmware's own text cursor blinking on black.
UINT64 fb = gop->Mode->FrameBufferBase;
UINT64 fbsz = gop->Mode->FrameBufferSize;
UINT32 bpp = 32; // GOP's 8-bit formats are 32bpp
UINT32 pitch = info->PixelsPerScanLine * (bpp / 8);
// The display block, which can describe a real machine. See
// SCHBOOT_FB_BASE for what the VBE one cannot say.
*(UINT64 *)SCHBOOT_FB_BASE = fb;
*(UINT64 *)SCHBOOT_FB_SIZE = fbsz;
*(UINT32 *)SCHBOOT_FB_WIDTH = info->HorizontalResolution;
*(UINT32 *)SCHBOOT_FB_HEIGHT = info->VerticalResolution;
*(UINT32 *)SCHBOOT_FB_STRIDE = pitch;
*(UINT32 *)SCHBOOT_FB_FORMAT = info->PixelFormat;
// The VBE block as well, byte for byte as the BIOS path leaves it: it is
// what the kernel read before this block existed, and what the BIOS loader
// still writes. Anything that does not fit is left at zero rather than
// wrapped -- a truncated address in here is exactly the bug above, and a
// zero is at least honest.
UINT8 *vbe = (UINT8 *)SCHBOOT_VBE_INFO;
zero(vbe, 512);
if (pitch <= 0xFFFF)
*(UINT16 *)(vbe + 16) = (UINT16)pitch; // bytes/line
*(UINT16 *)(vbe + 18) = (UINT16)info->HorizontalResolution;
*(UINT16 *)(vbe + 20) = (UINT16)info->VerticalResolution;
*(UINT8 *)(vbe + 25) = (UINT8)bpp;
if (fb <= 0xFFFFFFFFULL)
*(UINT32 *)(vbe + 40) = (UINT32)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.
//
// UINTN, so the pointer is the whole address. The loader runs with the
// firmware's identity map, so a physical address above 4 GB is directly
// usable here -- it is the *kernel* that needs an ioremap for one.
struct ui_fb uifb;
uifb.base = (ui_u8 *)(UINTN)fb;
uifb.width = info->HorizontalResolution;
uifb.height = info->VerticalResolution;
uifb.stride = pitch;
// 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);
mirrors_show();
// From here on, diagnostics go to COM1 rather than the console, or each one
// is drawn across the splash.
SPLASH_UP = ui_active();
vprint(L"schos: framebuffer "); vprint_hex(fb);
vprint(L" "); vprint_hex(info->HorizontalResolution);
vprint(L"x"); vprint_hex(info->VerticalResolution);
vprint(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)
{
// Sorted by address first. The specification does not promise an ordered
// map, firmware that hands one over unsorted exists, and merging only
// finds neighbours that sit next to each other in the list.
static struct e820entry raw[1024];
UINT32 nraw = 0, nent = 0;
for (UINTN off = 0; off + descsize <= mapsize && nraw < 1024;
off += descsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off);
struct e820entry e = { d->PhysicalStart, d->NumberOfPages * 0x1000,
efi_type_to_e820(d->Type), 1 };
UINT32 i = nraw++;
while (i > 0 && raw[i - 1].base > e.base) {
raw[i] = raw[i - 1];
i--;
}
raw[i] = e;
}
for (UINT32 i = 0; i < nraw && nent < E820_MAXENTRIES; i++) {
if (nent > 0 && out[nent-1].type == raw[i].type &&
out[nent-1].base + out[nent-1].length == raw[i].base) {
out[nent-1].length += raw[i].length;
continue;
}
out[nent++] = raw[i];
}
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;
}
// The SMBIOS entry point, the same way: 3.0's 64-bit one first, since a
// table above 4 GB is out of reach of the 2.x one.
static UINT64
find_smbios(void)
{
EFI_GUID sm3 = SMBIOS3_TABLE_GUID;
EFI_GUID sm2 = SMBIOS_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, &sm3))
return (UINT64)(UINTN)t[i].VendorTable;
if (guid_eq(&t[i].VendorGuid, &sm2))
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.
// The volume first: what is on it decides some of what the display does,
// and the display is the only thing that can report a failure afterwards.
open_boot_volume(image);
UINT64 fb = setup_framebuffer();
EFI_FILE_PROTOCOL *file = open_kernel(image);
UINT64 entry = load_elf(file);
tramp_reserve();
ui_progress_end();
file->Close(file);
// After the kernel rather than before it: the font is optional and the
// kernel is not, so a machine that is going to fail should fail on the
// thing that matters.
//
// Every stage from here to the jump names itself under the progress bar.
// On a desktop this is invisible -- the stages take microseconds -- and on
// a machine that stops, the label is the last thing on the screen and says
// which call did not come back. That is the only diagnostic a laptop with
// no serial port can give, and it costs a string.
ui_progress_label("Reading the console font");
load_console_font();
vprint(L"schos: kernel entry "); vprint_hex(entry); vprint(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();
vprint(L"schos: acpi rsdp "); vprint_hex(rsdp); vprint(L"\r\n");
UINT64 smbios = find_smbios();
// 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.
ui_progress_label("Reading the memory map");
// A quarter of a megabyte, and the size is not a guess.
//
// This was 32 KB, which is about 680 descriptors, and on real firmware
// that is not enough: GetMemoryMap answered EFI_BUFFER_TOO_SMALL (0x5) and
// the loader stopped there. A modern machine's map is not a handful of
// regions -- it is every reserved range the firmware has ever carved out,
// and the count is a property of the vendor rather than of the RAM.
//
// Asking first is what makes the size defensible rather than a bigger
// guess: a call with a zero size fails on purpose and fills in how much is
// actually needed. It allocates nothing, so the key from the real call
// below is still valid -- which is the whole constraint this code is
// written around.
UINTN mapsize = 0, mapkey = 0, descsize = 0;
UINT32 descver = 0;
EFI_STATUS st;
st = map_read(&mapsize, &mapkey, &descsize, &descver);
if (st == EFI_BUFFER_TOO_SMALL && mapsize > sizeof mapbuf) {
// Say the number. A loader that dies on "too small" without saying how
// small is a loader that costs somebody a rebuild to find out.
print(L"schos: the firmware's memory map needs ");
print_dec(mapsize);
print(L" bytes and this loader has ");
print_dec(sizeof mapbuf);
print(L"\r\n");
die(L"memory map larger than the loader's buffer", st);
}
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);
// The hand-off block is written after ExitBootServices into memory nobody
// allocated, which is safe only while the firmware keeps nothing there
// that outlives it. Say so if it does, since the alternative is a kernel
// that dies in a way nothing points back at.
for (UINT32 i = 0; i < nent; i++)
if (e820[i].type != E820_USABLE &&
e820[i].base < SCHBOOT_E820_MAP + SCHBOOT_E820_MAX * 24 &&
e820[i].base + e820[i].length > SCHBOOT_BASE) {
print(L"schos: WARNING: firmware memory overlaps the hand-off "
L"block at 0x30000\r\n");
ui_progress_label("Firmware memory at 0x30000 -- may not boot");
BS->Stall(2000000);
break;
}
// 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");
// And on the screen, because by this point print() goes to a serial
// port that a laptop does not have. The entries that get dropped are
// the high ones -- the map arrives sorted, and phystop is capped below
// 4 GB anyway -- so this is a warning rather than a failure. It is
// still not a thing to discover months later from a memory total that
// never looked quite right.
ui_progress_label("Memory map truncated -- some RAM unused");
BS->Stall(2000000);
}
ui_progress_label("Leaving the firmware");
vprint(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.
st = map_read(&mapsize, &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 ----
//
// 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. That distinction is worth making rather than assuming --
// an earlier version of this comment claimed nothing could report from
// here, and that claim cost a round trip to a laptop and back to learn
// which of three steps a machine had stopped in.
__asm__ volatile("cli");
copy((void *)SCHBOOT_E820_MAP, e820, nent * sizeof e820[0]);
*(UINT32 *)SCHBOOT_E820_COUNT = nent;
*(UINT64 *)SCHBOOT_RSDP_ADDR = rsdp;
*(UINT64 *)SCHBOOT_SMBIOS_ADDR = smbios;
*(UINT32 *)SCHBOOT_EFI_FLAG = SCHBOOT_EFI_MAGIC;
*(UINT8 *)SCHBOOT_VERBOSE_FLAG = (UINT8)VERBOSE_BOOT;
*(UINT8 *)SCHBOOT_DEBUG_FLAG = (UINT8)DEBUG_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
// **All 512 entries, not four.**
//
// Four gigabytes was enough for as long as everything that mattered lived
// below four gigabytes -- and on a machine with a lot of RAM, one thing
// does not: *this loader*. The firmware places the image where it likes,
// and on an 8 GB machine it lands above 4 GB. The instruction after
//
// mov %rax, %cr3
//
// is then an instruction fetch from an address these tables do not map,
// and the machine dies between one instruction and the next, with the
// splash still on screen and nothing said:
//
// X64 Exception Type - 0E(#PF - Page-Fault)
// ExceptionData - 0x10 I:1 P:0
// RIP - 0000000140004DEE <- the loader, at 5.4 GB
// RAX - 0000000000001000 <- the page tables it just installed
// RCX - 00000000001000F0 <- the kernel it was about to jump to
//
// That is the whole of "it freezes at Starting the kernel". It reproduces
// on QEMU with `-m 8G` and never on `-m 1G`, which is why every test here
// passed.
//
// The fix costs nothing: the PDPT is one page whether four entries are
// filled or all of them, and 512 GB of identity map covers any machine
// whose firmware is going to load us somewhere surprising. The kernel
// replaces all of this in kvmalloc() a few hundred instructions later.
for (int i = 0; i < 512; i++)
pdpt[i] = ((UINT64)i << 30) | PTE_P | PTE_PS | PTE_W;
// The kernel segments that could not be read straight home are copied
// there now, by tramp_go(), from a page they cannot overwrite -- and the
// jump into the kernel is made from the same page.
ui_progress_label(nlater ? "Placing and starting the kernel"
: "Starting the kernel");
tramp_go(entry, fb);
}