Three things, and the first one is the freeze.
**The loader jumped out from under itself.** A machine with a lot of RAM
stopped at "Starting the kernel" with the splash still up: no panic, no reset,
nothing. The firmware's own exception handler had the answer the whole time,
once a machine was found that printed it:
X64 Exception Type - 0E(#PF - Page-Fault)
ExceptionData - 0x10 I:1 P:0
RIP - 0000000140004DEE <- the loader's own code, at 5.4 GB
RAX - 0000000000001000 <- the page tables it had just installed
RCX - 00000000001000F0 <- the kernel it was about to jump to
I:1 P:0 is an instruction fetch from a page that is not present. The loader
built page tables covering four gigabytes, loaded cr3 -- and the firmware had
placed the loader image *above* four gigabytes, so the next instruction fetch
was unmapped. It reproduces exactly on `-m 8G` and never on `-m 1G`, which is
why every test here passed.
The PDPT is one page whether four entries are filled or all 512, so all 512
are: 512 GB of identity map, replaced by kvmalloc() a few hundred instructions
later.
**And 3 GB was never the limit, it was a circle.** PHYSTOP_MAX was 0xC0000000
because kvmalloc() mapped exactly four 1 GB pages, and kvmalloc mapped four
because that was the cap -- each holding the other up, throwing away thirteen
gigabytes of a sixteen gigabyte machine. kvmalloc now maps as many gigabytes as
the machine has RAM and tells ioremap where the direct map stopped, instead of
ioremap assuming it stopped at four.
Freeing memory becomes two passes, and the ordering is forced rather than
chosen: kinit1() runs *before* kvmalloc because kvmalloc calls kalloc for its
own page tables, and freeing a page means writing into it -- so kinit1 can only
touch what the loader mapped. kinit_high() adds the rest afterwards.
`phystop` and the new `lowtop` are now two different numbers: the DMA arena
stays below 4 GB because a device is not obliged to reach the top of memory,
and an RTL8139's buffer address register is thirty-two bits wide.
**And the 0 MB.** The System Information screen reported "0 MB usable" on a
machine with sixteen gigabytes in it, because memory_mb() had its own 16 KB
guess at how big a memory map gets, and returned zero when GetMemoryMap said
EFI_BUFFER_TOO_SMALL. That is the same bug as the previous commit's, in a
second copy, and this one failed silently. There is one buffer and one
map_read() now.
Verified at 1 GB, 8 GB and 16 GB: all three boot, no traps, no panics, socktest
and memfdtest green in each. 8 GB and 16 GB did not boot at all before -- and
16 GB now reports 17408 MB with "mem: 13312 MB above 4 GB added to the
allocator", where the old kernel would have used three. Both display
configurations still clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1940 lines
75 KiB
C
1940 lines
75 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.
|
|
//
|
|
// **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");
|
|
}
|