Nexoniarz is the sole author and SchOS was never published under Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text, every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE, README.md, docs/licensing.md and the site say so. projects/schfs-linux follows the rest of the tree. The GPL gives copyleft: a fork has to ship its source. It also lifts the old "Linux drivers, never" rule, since GPL-2.0-only code combines with GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still has to pick one version, so docs/licensing.md now keeps Apache-2.0 and GPLv3 code out of the kernel to keep Linux code possible there. BSD 4-clause code goes from "avoid" to "no", because the advertising clause is GPL-incompatible. LDLS still ships no .ko. That boundary is now project policy rather than a legal requirement, and ldls.md, devices.md and licensing.md section 6 say so. refmod declares license=GPL, and ldls-check passes (27 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1940 lines
75 KiB
C
1940 lines
75 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/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 */
|
|
|
|
/* 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/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;
|
|
|
|
// 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);
|
|
|
|
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/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");
|
|
}
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
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");
|
|
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.
|
|
#define MAPBUF_BYTES 262144
|
|
static UINT8 mapbuf[MAPBUF_BYTES];
|
|
|
|
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);
|
|
}
|
|
|
|
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
|
|
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;
|
|
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 exactly once, from mirrors_find(), before anything has been
|
|
// drawn. 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 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;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
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.
|
|
// 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);
|
|
|
|
UINT32 fb = setup_framebuffer();
|
|
|
|
EFI_FILE_PROTOCOL *file = open_kernel(image);
|
|
UINT64 entry = load_elf(file);
|
|
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");
|
|
|
|
// 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);
|
|
|
|
// 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;
|
|
*(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;
|
|
|
|
ui_progress_label("Starting the kernel");
|
|
|
|
// RBX carries the framebuffer address into kmain, and must never be 1 --
|
|
// that value marks an application processor in entry64high. A physical
|
|
// MMIO address or zero, so it cannot collide.
|
|
//
|
|
// "rbx" is in the clobber list because this writes it, and leaving it out
|
|
// is not a tidiness problem -- it is a jump to the wrong address. Without
|
|
// it the compiler may allocate an *input* into rbx, and it did: clang 21
|
|
// put `entry` there, so the second instruction overwrote the jump target
|
|
// with the framebuffer address and the third jumped into video memory. The
|
|
// machine then executed pixels until KVM gave up.
|
|
//
|
|
// movq %rax, %cr3 <- %0, the page tables
|
|
// movq %rcx, %rbx <- %1, the framebuffer, into rbx
|
|
// jmpq *%rbx <- %2 was rbx, and now holds the framebuffer
|
|
//
|
|
// It had worked for as long as the compiler happened to choose differently.
|
|
// That is the same failure as the BIOS loader's EAX hand-off in
|
|
// docs/history/kernel.md section 4: a register the code depends on, that
|
|
// nothing in the source actually asks for.
|
|
__asm__ volatile(
|
|
"mov %0, %%cr3\n\t"
|
|
"mov %1, %%rbx\n\t"
|
|
"jmp *%2\n\t"
|
|
:
|
|
: "r"((UINT64)PML4_ADDR), "r"((UINT64)fb), "r"(entry)
|
|
: "rbx", "memory");
|
|
|
|
for (;;)
|
|
__asm__ volatile("hlt");
|
|
}
|