SchOS/boot/uefi/main.c
Nexoniarz c4dd76cc66 boot/uefi: the memory map the kernel gets is the one the firmware agreed to
Two bugs on the ExitBootServices path, both invisible under OVMF and both
aimed squarely at real firmware.

The retry did not re-translate. If ExitBootServices returns an error the
loader re-reads the memory map and tries again -- correct, and the
specification expects it -- but the e820 table handed to the kernel was
still the one built from the *previous* map. The retry exists precisely
because the map changed, so the kernel was being given a description of
memory the firmware had already rearranged. QEMU never takes this path,
which is why it survived. Translation is now a function called on both.

Truncation was silent. phystop comes from the largest usable region, so
dropping the tail of the map can leave the kernel believing a large
machine has a fraction of its RAM -- and nothing said so, because the
loop simply stopped at the cap. It now warns while there is still a
console to warn on; after ExitBootServices there is nowhere to report
anything.

SCHBOOT_E820_MAX goes from 32 to 40. The map sits at BASE+0x208 and the
next field is at BASE+0x600, so 1016 bytes are reserved and 42 entries of
24 bytes fit. Real firmware emits far more descriptors than QEMU's; the
headroom was already allocated and unused.

Coalescing adjacent same-type runs is what keeps any of this in budget,
and it works because boot services memory becomes usable at this point --
that is most of the fragmentation in a UEFI map.

110 [ok], no failures, and the map still reads 7 regions under OVMF.
2026-08-11 11:42:22 +02:00

1138 lines
42 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");
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.
__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)
: "memory");
for (;;)
__asm__ volatile("hlt");
}