// SPDX-License-Identifier: GPL-2.0-or-later // // SchOS UEFI loader. // // The BIOS path and this one meet at a contract, not at shared code: see // boot/README.md. This loader's whole job is to arrive at that contract from // the other direction. // // 1. find the kernel on the volume this image was loaded from // 2. load its ELF PT_LOAD segments to their physical addresses // 3. record the framebuffer where kernel/dev/vga.c looks for it // 4. translate the UEFI memory map into the E820 entries // kernel/arch/x86_64/e820.c already parses // 5. ExitBootServices // 6. build the page tables the kernel's 64-bit entry expects, and jump // // Step 6 is why kernel/arch/x86_64/entry.S grew a `sch64_header`. The // multiboot entry is 32-bit code that climbs into long mode itself; the // firmware has already put us there, and dropping back down only to climb out // again is a mode switch whose failure mode is a triple fault with no console. // The kernel publishes its 64-bit entry instead, and this jumps straight to it. // // Nothing here allocates after ExitBootServices, and nothing calls a firmware // service after it either -- there are none. Everything the jump needs is // computed before. #include "efi.h" #include "../../include/arch/schboot.h" #include "../common/ui.h" // Where the kernel expects to find things comes from schboot.h, included // above: the hand-off is one contract and both loaders write the same one. #define E820_MAXENTRIES SCHBOOT_E820_MAX #define E820_USABLE 1 #define E820_RESERVED 2 #define E820_ACPI 3 #define E820_NVS 4 #define KERNEL_PATH L"\\schos\\kernel" #define KERNEL_PATH_ALT L"\\kernel" // Page tables are built here, exactly where mboot_entry builds them, so the // kernel's entry sees the state it was written for. // // **These are a contract, not this file's choice.** kernel/arch/x86_64/entry.S // builds its own bootstrap tables at the same two addresses and loads cr3 with // 0x1000 by name. Moving them here alone was tried, on the theory that // hardcoded low memory is a poor assumption under UEFI: the machine // triple-faulted one instruction after the jump, twice, for two different // reasons -- and would have needed the kernel's assembly to agree even once // both were fixed. If it ever does move, it moves in both files at once. #define PML4_ADDR 0x1000 #define PDPT_ADDR 0x2000 #define PTE_P 0x001 #define PTE_W 0x002 #define PTE_PS 0x080 struct e820entry { UINT64 base; UINT64 length; UINT32 type; UINT32 acpi_ext; } __attribute__((packed)); // ELF64, only what is needed to place segments. struct elf64_ehdr { UINT8 e_ident[16]; UINT16 e_type, e_machine; UINT32 e_version; UINT64 e_entry, e_phoff, e_shoff; UINT32 e_flags; UINT16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx; }; struct elf64_phdr { UINT32 p_type, p_flags; UINT64 p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align; }; #define PT_LOAD 1 static EFI_SYSTEM_TABLE *ST; static EFI_BOOT_SERVICES *BS; // --------------------------------------------------------------------------- // Console. Enough to say what went wrong, and no more. // --------------------------------------------------------------------------- static void mirrors_show(void); /* see "the other displays", further down */ static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary); static void describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag); static void explain_range(UINT64 lo, UINT64 hi); /* Once the splash is up, nothing may write to the console. * * ConOut on a graphical firmware renders text into the same framebuffer the * splash is drawn in, so every diagnostic after that point is scribbled across * it -- which is exactly what a screenshot showed: our mark centred, and * firmware text over the top of it from the left edge. * * The diagnostics are worth keeping, so they go to COM1 directly instead. That * is legal before ExitBootServices, costs nothing, and is where they were being * read from anyway. */ static int SPLASH_UP; /* Set by the boot menu; handed to the kernel through the same flag byte the * BIOS loader writes, so a verbose boot means the same thing either way. */ static int VERBOSE_BOOT; // Remote debugging, off unless somebody picks it out of the boot menu -- and // the entry that picks it only exists in a build made with DEBUG=1. See // SCHBOOT_DEBUG_FLAG in include/arch/schboot.h for why the consent is asked for at // this particular moment. static int DEBUG_BOOT; static void serial_putc(char c) { /* Bounded, for the same reason the BIOS loader's is: a machine with no * serial port reads 0xFF from the status register forever. */ for (int i = 0; i < 100000; i++) { UINT8 st; /* Through DX, not an immediate: the immediate form of in/out only * encodes ports 0-255, and COM1's registers are at 0x3F8. */ __asm__ volatile("inb %1, %0" : "=a"(st) : "d"((UINT16)0x3FD)); if (st & 0x20) break; } __asm__ volatile("outb %0, %1" : : "a"((UINT8)c), "d"((UINT16)0x3F8)); } /* The menus, on the serial line. * * The three screens below paint into the framebuffer, which is the right place * for them on a machine with a screen and nowhere at all on one driven over a * serial cable. That is how this loader is usually driven, and f8_window() * already went out of its way to support it -- it accepts 'o' as well as F8 * precisely because "a serial console sends no scan codes at all". So the * prompt invited a serial user in, drew them a menu they could not see, and * then blocked for ever on a key they had no way to choose. * * Straight to COM1 rather than through print(): print() goes to ConOut until * the splash is up, and ConOut on a machine with graphics draws its text over * the menu that was just painted there. * * A machine with no COM1 reads 0xFF from the status register, serial_putc() * gives up after its bounded wait, and the write goes nowhere. So this costs * nothing where it is useless and is the whole interface where it is not. */ static void serial_puts(const char *s) { for (; *s; s++) { if (*s == '\n') serial_putc('\r'); serial_putc(*s); } } /* One item, as the selected line or not. The marker is the same width either * way, so the list does not shift sideways as the selection moves. * * `numbered` is not decoration: the number is shown exactly where it is a key * you can press. The boot menu takes 1-5; the resolution list takes only W/S * and Enter, and is longer than nine entries -- numbering it printed ':', ';', * '<' past the ninth, which is a character nobody can type offering to select * something. */ static void serial_item(const char *const *items, int i, int sel, int numbered) { serial_puts(i == sel ? " > " : " "); if (numbered && i < 9) { char n[4]; n[0] = (char)('1' + i); n[1] = '.'; n[2] = ' '; n[3] = 0; serial_puts(n); } else if (numbered) { serial_puts(" "); /* keep the column */ } serial_puts(items[i]); serial_puts("\n"); } static void serial_menu(const char *title, const char *const *items, int n, int sel, const char *hint, int numbered) { serial_puts("\n-- "); serial_puts(title); serial_puts(" --\n"); for (int i = 0; i < n; i++) serial_item(items, i, sel, numbered); serial_puts(hint); serial_puts("\n"); } /* Diagnostics: only on a verbose boot. * * These four lines say where the framebuffer is, where the kernel went and * where the ACPI tables are. Worth having when a machine will not boot, and * worth nothing on the thousand boots that work -- and they are the first * thing on the serial line every time, so they set the tone for everything * after. vprint() is print() with that decision built in. */ static void vprint(const CHAR16 *s); static void vprint_hex(UINT64 v); static void print(const CHAR16 *s) { if (!s) return; if (SPLASH_UP) { for (int i = 0; s[i]; i++) serial_putc((char)(s[i] & 0x7F)); return; } if (ST && ST->ConOut) ST->ConOut->OutputString(ST->ConOut, (CHAR16 *)s); } // A number in the base a person reads sizes in. `u64_to_dec` is further down // and works on char; this is the CHAR16 the firmware console wants. static void print_dec(UINT64 v) { CHAR16 tmp[24], out[25]; int n = 0, i = 0; if (v == 0) tmp[n++] = L'0'; while (v && n < 23) { tmp[n++] = (CHAR16)(L'0' + (v % 10)); v /= 10; } while (n > 0) out[i++] = tmp[--n]; out[i] = 0; print(out); } static void print_hex(UINT64 v) { CHAR16 buf[19]; const CHAR16 *digits = L"0123456789abcdef"; buf[0] = L'0'; buf[1] = L'x'; for (int i = 0; i < 16; i++) buf[2 + i] = digits[(v >> ((15 - i) * 4)) & 0xf]; buf[18] = 0; print(buf); } static void vprint(const CHAR16 *s) { if (VERBOSE_BOOT) print(s); } static void vprint_hex(UINT64 v) { if (VERBOSE_BOOT) print_hex(v); } // A failure here cannot be recovered from and must not be silent: without this // the firmware simply returns to its boot menu and the user is told nothing. static EFI_STATUS die(const CHAR16 *msg, EFI_STATUS st) { print(L"schos: "); print(msg); print(L" (status "); print_hex(st); print(L")\r\n"); print(L"schos: halted.\r\n"); /* Say it on the screen too, if there is one. * * Once the splash is up, print() goes to the serial port and nowhere else. * That is right for a development machine and useless on a laptop, which * has no serial port -- a loader that failed after drawing the splash left * a black screen and the firmware's own cursor, with the explanation going * to a wire nobody was holding. A dead machine that says why is worth the * dozen lines. */ if (ui_active()) { char line[96]; int i = 0; for (const char *q = "Cannot start SchOS: "; *q && i < (int)sizeof line - 1; q++) line[i++] = *q; for (int j = 0; msg[j] && i < (int)sizeof line - 1; j++) line[i++] = (msg[j] >= 0x20 && msg[j] < 0x7F) ? (char)msg[j] : '?'; line[i] = 0; char code[32]; i = 0; for (const char *q = "EFI status 0x"; *q && i < (int)sizeof code - 1; q++) code[i++] = *q; /* Low nibbles are enough to identify the error and keep this short; * EFI_NOT_FOUND is 0x0E with the high bit set. */ const char *hex = "0123456789ABCDEF"; for (int sh = 12; sh >= 0; sh -= 4) if (i < (int)sizeof code - 1) code[i++] = hex[(st >> sh) & 0xF]; code[i] = 0; /* Clear a band first. Without it the message lands on top of the * logo, which is exactly the moment legibility matters most. */ int mid = (int)ui_height() / 2; ui_fill_rect(0, mid - 20, (int)ui_width(), 100, 0x0C0E13u); ui_text_centered(mid, line, 0xE8EAEEu); ui_text_centered(mid + 24, code, 0xB9BEC7u); ui_text_centered(mid + 56, "Halted -- power off or reset", 0x8A9099u); } for (;;) __asm__ volatile("hlt"); } static void zero(void *p, UINTN n) { UINT8 *b = (UINT8 *)p; while (n--) *b++ = 0; } static void copy(void *dst, const void *src, UINTN n) { UINT8 *d = (UINT8 *)dst; const UINT8 *s = (const UINT8 *)src; while (n--) *d++ = *s++; } // --------------------------------------------------------------------------- // Reading the kernel off the ESP // --------------------------------------------------------------------------- // The volume this loader came off, kept so that anything else read at boot is // read from the same place the kernel was. Set by open_kernel(). static EFI_FILE_PROTOCOL *boot_root; // 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/arch/schboot.h for why this is the loader's job // and not the kernel's. Everything here is best-effort: a machine with no // console.ttf on its ESP boots with the font in the kernel image, which is // the same font, and says nothing about it. // --------------------------------------------------------------------------- #define FONT_PATH L"\\schos\\console.ttf" #define FONT_PATH_ALT L"\\console.ttf" // Four megabytes. Large enough for any single-weight TrueType face -- the one // in the kernel image is 112 KB -- and small enough that a wrong file cannot // eat the machine's memory before the kernel has any. #define FONT_MAX (4u * 1024u * 1024u) static void load_console_font(void) { EFI_FILE_PROTOCOL *file = 0; EFI_STATUS st; UINT64 size = 0; EFI_PHYSICAL_ADDRESS at; UINTN read; // Absent until proven present: the kernel reads these two fields with no // other way to know whether the loader wrote them. *(UINT64 *)SCHBOOT_FONT_ADDR = 0; *(UINT32 *)SCHBOOT_FONT_LEN = 0; if (!boot_root) return; st = boot_root->Open(boot_root, &file, (CHAR16 *)FONT_PATH, EFI_FILE_MODE_READ, 0); if (EFI_ERROR(st)) st = boot_root->Open(boot_root, &file, (CHAR16 *)FONT_PATH_ALT, EFI_FILE_MODE_READ, 0); if (EFI_ERROR(st) || !file) return; /* no console.ttf; the built-in one stands */ // The size, without EFI_FILE_INFO and its GUID: seek past the end and ask // where that landed. The specification requires SetPosition beyond EOF to // clamp to the end, which is exactly the number wanted. if (EFI_ERROR(file->SetPosition(file, 0xFFFFFFFFFFFFFFFFULL)) || EFI_ERROR(file->GetPosition(file, &size)) || EFI_ERROR(file->SetPosition(file, 0))) { file->Close(file); return; } if (size < 4096 || size > FONT_MAX) { vprint(L"schos: console.ttf is an implausible size; ignoring it\r\n"); file->Close(file); return; } // Below 4 GB, because the kernel reaches this through its direct map and // that is how far the direct map goes -- a font above it would need an // ioremap the console cannot do before there is a console. // // Reserved rather than loader data: EfiLoaderData becomes E820_USABLE in // the handoff, on the argument that the loader's own pages are the // kernel's the moment it runs. True of the loader; not true of this. at = 0xFFFFFFFFULL; st = BS->AllocatePages(AllocateMaxAddress, EfiReservedMemoryType, (UINTN)((size + 0xFFF) / 0x1000), &at); if (EFI_ERROR(st)) { vprint(L"schos: no memory for console.ttf; using the built-in font\r\n"); file->Close(file); return; } read = (UINTN)size; st = file->Read(file, &read, (void *)(UINTN)at); file->Close(file); if (EFI_ERROR(st) || read != size) { vprint(L"schos: console.ttf would not read; using the built-in font\r\n"); return; /* the pages stay reserved; harmless */ } *(UINT64 *)SCHBOOT_FONT_ADDR = (UINT64)at; *(UINT32 *)SCHBOOT_FONT_LEN = (UINT32)size; vprint(L"schos: console font "); vprint_hex((UINT32)size); vprint(L" bytes at "); vprint_hex((UINT32)at); vprint(L"\r\n"); } // 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)); EFI_PHYSICAL_ADDRESS claimed_end = 0; /* see the AllocatePages below */ 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. // // From the end of what the previous segment claimed, not from this // one's first page. The linker packs segments without page-aligning // them -- .text ending at 0x1dc9fe and .rodata starting at 0x1dca00 -- // so two segments routinely share a page, and asking for that page a // second time is refused because it is ours already. That was the // "could not reserve" warning on every boot, about memory that was // reserved all along. EFI_PHYSICAL_ADDRESS at = ph.p_paddr & ~0xfffULL; EFI_PHYSICAL_ADDRESS end = (ph.p_paddr + ph.p_memsz + 0xfff) & ~0xfffULL; if (at < claimed_end) at = claimed_end; UINTN pages = at < end ? (UINTN)((end - at) / 0x1000) : 0; st = pages ? BS->AllocatePages(AllocateAddress, EfiLoaderData, pages, &at) : EFI_SUCCESS; if (end > claimed_end) claimed_end = end; 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"); explain_range(at, end); } 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; } } /* Set a mode, and keep it only if the firmware's own account of it adds up. * * A GOP can accept a mode and then describe it in a way the display does not * scan out: a row length that is not the one being shown, or an aperture too * small for the rows it claims. Drawn into anyway, that is the picture sheared * into dotted lines, or a logo repeated across the screen, both of which a * desktop with an NVIDIA card showed at sizes other than 800x600. The numbers * cannot prove a mode is right, but they can prove one is wrong, and a wrong * one is refused here, in the menu, where there is still a screen to say so on. */ static int mode_set_checked(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, UINT32 mode) { UINT32 was = gop->Mode->Mode; EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *want = 0; UINTN sz = 0; if (EFI_ERROR(gop->QueryMode(gop, mode, &sz, &want)) || !want) return 0; if (EFI_ERROR(gop->SetMode(gop, mode))) return 0; EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *got = gop->Mode->Info; UINT64 need = (UINT64)got->PixelsPerScanLine * 4 * got->VerticalResolution; if (got->HorizontalResolution == want->HorizontalResolution && got->VerticalResolution == want->VerticalResolution && got->PixelsPerScanLine >= got->HorizontalResolution && gop->Mode->FrameBufferBase && gop->Mode->FrameBufferSize >= need) return 1; describe_gop(gop, L"schos: mode does not add up, going back:"); gop->SetMode(gop, was); return 0; } /* Said on the screen that is still working, then back to the menu. */ static void res_refused(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) { struct ui_fb uifb; EFI_INPUT_KEY key; const char *msg[1] = { "The display refused that size -- keeping this one" }; uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase; uifb.width = gop->Mode->Info->HorizontalResolution; uifb.height = gop->Mode->Info->VerticalResolution; uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4; ui_init(&uifb); ui_menu("Resolution", msg, 1, 0, "any key to go back"); mirrors_show(); for (;;) { if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) break; BS->Stall(10000); } } static void res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) { int sel = 0; int echoed = -1; for (;;) { struct ui_fb uifb; uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase; uifb.width = gop->Mode->Info->HorizontalResolution; uifb.height = gop->Mode->Info->VerticalResolution; uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4; ui_init(&uifb); res_build(gop); const char *items[RES_MAX + 1]; for (int i = 0; i <= RES_N; i++) items[i] = RES_TEXT[i]; if (sel > RES_N) sel = RES_N; ui_menu("Resolution", items, RES_N + 1, sel, "W/S to choose, Enter to apply, Esc or Q to go back"); if (echoed < 0) { serial_menu("Resolution", items, RES_N + 1, sel, "W/S to choose, Enter to apply, Esc or Q to go back", 0); } else if (echoed != sel) { serial_item(items, sel, sel, 0); } echoed = sel; mirrors_show(); EFI_INPUT_KEY key; for (;;) { if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) break; BS->Stall(10000); } CHAR16 c = key.UnicodeChar; /* Q as well as Esc, for the same reason f8_window() accepts 'o' as * well as F8: on a serial line a lone ESC byte is indistinguishable * from the start of an escape sequence, so the terminal driver holds * it back waiting for the rest and it never arrives as a key. Esc is * the obvious thing to press on a screen and unreachable on a cable. */ if (key.ScanCode == 0x17 || c == 27 || c == L'q' || c == L'Q') return; if (key.ScanCode == 0x01 || c == L'w' || c == L'W') { sel = (sel + RES_N) % (RES_N + 1); continue; } if (key.ScanCode == 0x02 || c == L's' || c == L'S') { sel = (sel + 1) % (RES_N + 1); continue; } if (c == L'\r' || c == L'\n') { if (sel < RES_N) { if (!mode_set_checked(gop, RES_MODE[sel])) { res_refused(gop); return; } // The one other time the mirrors' modes are worth touching: // the person just said what size the display is. mirrors_match(gop); return; } /* Typed. Matched against the same filtered list, so a size that * would shear cannot be reached by typing it either. */ UINT32 w = res_read_number("Width: "); UINT32 h = res_read_number("Height: "); for (UINT32 i = 0; i < gop->Mode->MaxMode; i++) { EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = 0; UINTN sz = 0; if (EFI_ERROR(gop->QueryMode(gop, i, &sz, &mi)) || !mode_usable(mi)) continue; if (mi->HorizontalResolution == w && mi->VerticalResolution == h) { if (!mode_set_checked(gop, i)) { res_refused(gop); return; } mirrors_match(gop); return; } } /* Nothing matched. Saying so beats silently keeping the old mode * and letting the user wonder whether the key registered. */ const char *msg[1] = { "No such mode -- keeping this one" }; ui_menu("Resolution", msg, 1, 0, "any key to go back"); mirrors_show(); for (;;) { if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) break; BS->Stall(10000); } return; } } } static UINT32 build_e820(const UINT8 *mapbuf, UINTN mapsize, UINTN descsize, struct e820entry *out); /* Both defined below, next to the hand-off code they belong to. The system * information panel reads the same two facts the kernel will be handed, on * purpose: a panel with its own idea of what counts as usable memory, or its * own ACPI search, would be a second implementation to keep in step. */ static UINT32 efi_type_to_e820(UINT32 t); static UINT64 find_rsdp(void); /* CPU brand string, from CPUID leaves 0x80000002..4. Forty-eight bytes of * ASCII split across twelve registers, usually with leading spaces on Intel * parts, so it is trimmed on the way out. A CPU that does not implement the * extended leaves reports a maximum below 0x80000004, and there is nothing to * substitute -- saying "unknown" is better than printing whatever registers * happened to hold. */ static void cpu_brand(char *out, int cap) { UINT32 eax = 0, ebx = 0, ecx = 0, edx = 0; __asm__ volatile("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(0x80000000u)); if (eax < 0x80000004u) { int i = 0; for (const char *q = "unknown"; *q && i < cap - 1; q++) out[i++] = *q; out[i] = 0; return; } char raw[49]; int n = 0; for (UINT32 leaf = 0x80000002u; leaf <= 0x80000004u; leaf++) { __asm__ volatile("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(leaf)); UINT32 regs[4] = { eax, ebx, ecx, edx }; for (int r = 0; r < 4; r++) for (int b = 0; b < 4; b++) raw[n++] = (char)((regs[r] >> (b * 8)) & 0xFF); } raw[48] = 0; const char *src = raw; while (*src == ' ') src++; int i = 0; while (*src && i < cap - 1) out[i++] = *src++; while (i > 0 && out[i - 1] == ' ') i--; out[i] = 0; } /* Total conventional memory, in MB, from the UEFI memory map. * * This is a *look*, not the hand-off: GetMemoryMap is called again at exit * because any allocation between here and there invalidates the map key, and * calling it once for both purposes is how a loader gets EFI_INVALID_PARAMETER * from ExitBootServices long after the mistake. The count is deliberately of * the types the kernel will treat as usable, so it matches what the system * later reports rather than what the firmware owns. */ // The firmware's memory map, read into the one buffer there is. // // There used to be two of these, each with its own guess at how big a memory // map gets -- 32 KB where it mattered and 16 KB here -- and both guesses were // wrong on real hardware. The one here was the worse of the two, because it // did not fail loudly: GetMemoryMap answered EFI_BUFFER_TOO_SMALL, this // returned zero, and the System Information screen reported **0 MB usable** on // a machine with sixteen gigabytes in it. // // So: one buffer, one function, and the size comes from the firmware rather // than from an estimate. A call with a zero size fails on purpose and fills in // what is needed; it allocates nothing, so a caller that needs the map key for // ExitBootServices still has a valid one. #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); } /* Who owns [lo, hi), when it was not ours to take. * * "could not reserve" alone says where and not why, and the why decides * whether it matters: boot-services memory is the firmware's until * ExitBootServices and harmless to overwrite after it, while runtime services * or ACPI tables under the kernel are a machine that will corrupt itself. So * every descriptor in the range that is not free memory is named. */ static void explain_range(UINT64 lo, UINT64 hi) { UINTN size, key, dsize; UINT32 dver; if (EFI_ERROR(map_read(&size, &key, &dsize, &dver)) || !dsize) return; for (UINTN off = 0; off + dsize <= size; off += dsize) { EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off); UINT64 a = d->PhysicalStart, b = a + d->NumberOfPages * 4096; if (b <= lo || a >= hi || d->Type == EfiConventionalMemory) continue; print(L"schos: type "); print_dec(d->Type); print(L" at "); print_hex(a); print(L".."); print_hex(b); print(L"\r\n"); } } static UINT64 memory_mb(void) { UINTN size = 0, mapkey = 0, dsize = 0; UINT32 dver = 0; UINT64 pages = 0; if (EFI_ERROR(map_read(&size, &mapkey, &dsize, &dver))) return 0; for (UINTN off = 0; off + dsize <= size; off += dsize) { EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off); if (efi_type_to_e820(d->Type) == E820_USABLE) pages += d->NumberOfPages; } return (pages * 4096u) / (1024u * 1024u); } /* Narrow a CHAR16 string into ASCII for the panel. The firmware vendor is the * only UTF-16 string on this screen; anything outside ASCII becomes '?' rather * than being silently dropped, so a name that does not fit still looks like a * name. */ static void narrow(const CHAR16 *src, char *out, int cap) { int i = 0; if (src) while (src[i] && i < cap - 1) { CHAR16 c = src[i]; out[i] = (c >= 0x20 && c < 0x7F) ? (char)c : '?'; i++; } out[i] = 0; } /* The system information screen. * * Everything here is read at the moment it is shown rather than captured at * startup: the resolution can have been changed by the row above it, and a * panel that still reports the mode the machine booted with is worse than no * panel. */ static void sys_info(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) { char vendor[40], cpu[52], mode[32], mem[24], fw[24], spec[24], acpi[24]; narrow(ST->FirmwareVendor, vendor, sizeof vendor); cpu_brand(cpu, sizeof cpu); { char w[16], h[16]; u64_to_dec(gop->Mode->Info->HorizontalResolution, w, sizeof w); u64_to_dec(gop->Mode->Info->VerticalResolution, h, sizeof h); int i = 0; for (char *q = w; *q && i < (int)sizeof mode - 1; q++) mode[i++] = *q; if (i < (int)sizeof mode - 1) mode[i++] = 'x'; for (char *q = h; *q && i < (int)sizeof mode - 1; q++) mode[i++] = *q; mode[i] = 0; } { char n[24]; u64_to_dec(memory_mb(), n, sizeof n); int i = 0; for (char *q = n; *q && i < (int)sizeof mem - 1; q++) mem[i++] = *q; for (const char *q = " MB usable"; *q && i < (int)sizeof mem - 1; q++) mem[i++] = *q; mem[i] = 0; } /* The firmware's own revision, and the UEFI revision it implements. These * disagree often and the difference matters when reading a bug report: * one names the build, the other names the contract. */ { char hi[16], lo[16]; u64_to_dec(ST->FirmwareRevision >> 16, hi, sizeof hi); u64_to_dec(ST->FirmwareRevision & 0xFFFF, lo, sizeof lo); int i = 0; for (char *q = hi; *q && i < (int)sizeof fw - 1; q++) fw[i++] = *q; if (i < (int)sizeof fw - 1) fw[i++] = '.'; for (char *q = lo; *q && i < (int)sizeof fw - 1; q++) fw[i++] = *q; fw[i] = 0; } { /* EFI_TABLE_HEADER is { UINT64 Signature; UINT32 Revision; ... }, so * Revision is at offset 8. At offset 4 this read the upper half of the * "IBI SYST" signature and displayed it as "21587.2286". */ UINT32 rev = *(UINT32 *)(ST->Hdr + 8); char hi[16], lo[16]; u64_to_dec(rev >> 16, hi, sizeof hi); u64_to_dec((rev & 0xFFFF) / 10, lo, sizeof lo); int i = 0; for (char *q = hi; *q && i < (int)sizeof spec - 1; q++) spec[i++] = *q; if (i < (int)sizeof spec - 1) spec[i++] = '.'; for (char *q = lo; *q && i < (int)sizeof spec - 1; q++) spec[i++] = *q; spec[i] = 0; } /* Whether ACPI was found, because its absence is not cosmetic: no RSDP * means no MADT, no IOAPIC and no SMP, and that has already been debugged * once from the far end. */ { const char *v = find_rsdp() ? "present" : "NOT FOUND"; int i = 0; for (const char *q = v; *q && i < (int)sizeof acpi - 1; q++) acpi[i++] = *q; acpi[i] = 0; } static const char *const labels[] = { "Firmware", "Firmware revision", "UEFI revision", "Processor", "Memory", "Display", "ACPI", }; const char *values[7]; values[0] = vendor; values[1] = fw; values[2] = spec; values[3] = cpu; values[4] = mem; values[5] = mode; values[6] = acpi; ui_panel("System Information", labels, values, 7, "Any key to go back"); serial_puts("\n-- System Information --\n"); for (int i = 0; i < 7; i++) { serial_puts(" "); serial_puts(labels[i]); serial_puts(" "); serial_puts(values[i]); serial_puts("\n"); } serial_puts("Any key to go back\n"); mirrors_show(); EFI_INPUT_KEY key; for (;;) { if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) return; BS->Stall(10000); } } static int boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) { int selected = 0; int echoed = -1; // the serial copy: header once, then moves char resbuf[48]; for (;;) { /* Re-read the framebuffer every pass: choosing a resolution moves it, * and drawing where it used to be is a black screen with no * explanation. */ struct ui_fb uifb; uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase; uifb.width = gop->Mode->Info->HorizontalResolution; uifb.height = gop->Mode->Info->VerticalResolution; uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4; ui_init(&uifb); res_row(gop, resbuf, sizeof resbuf); // Six entries in a debug build, five in a release one. // // The debugger entry is compiled out rather than hidden, because a // menu item that is present and refuses is an invitation to work out // how to make it not refuse. An ISO built for anybody else has no way // to turn a network debugger on, because the code that would set the // flag is not in it. const char *items[6]; int nitems = 5; items[0] = "Boot SchOS"; items[1] = "Boot SchOS (verbose)"; items[2] = resbuf; items[3] = "System Information"; items[4] = "Reboot to firmware"; #ifdef SCHOS_DEBUG items[5] = DEBUG_BOOT ? "Remote debugging: on (schdbgd, port 4444)" : "Remote debugging: off"; nitems = 6; #endif ui_menu("Boot Manager", items, nitems, selected, nitems == 6 ? "W/S or 1-6 to choose, Enter to select" : "W/S or 1-5 to choose, Enter to select"); // The same menu on the serial line: the whole thing once, then one // line per *move*. Reprinting on every keypress would be correct and // unreadable -- a serial terminal has no cursor to paint over, so an // unrecognised key would scroll the selection past again for nothing. if (echoed < 0) { serial_menu("Boot Manager", items, nitems, selected, nitems == 6 ? "W/S or 1-6 to choose, Enter to select" : "W/S or 1-5 to choose, Enter to select", 1); } else if (echoed != selected) { serial_item(items, selected, selected, 1); } echoed = selected; mirrors_show(); EFI_INPUT_KEY key; CHAR16 c = 0; UINT16 scan = 0; for (;;) { if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) { c = key.UnicodeChar; scan = key.ScanCode; break; } BS->Stall(10000); } if (scan == 0x01) { selected = (selected + nitems - 1) % nitems; continue; } if (scan == 0x02) { selected = (selected + 1) % nitems; continue; } if (c == L'w' || c == L'W') { selected = (selected + nitems - 1) % nitems; continue; } if (c == L's' || c == L'S') { selected = (selected + 1) % 5; continue; } if (c >= L'1' && c <= L'0' + nitems) { selected = (int)(c - L'1'); continue; } if (c == L'\r' || c == L'\n') { if (selected == 0) return 0; if (selected == 1) return 1; if (selected == 2) { res_choose(gop); continue; } if (selected == 3) { sys_info(gop); continue; } #ifdef SCHOS_DEBUG // A toggle rather than an action: picking it changes what the // entry says and leaves you in the menu, so the state you booted // with is the state you can see before you boot. if (selected == 5) { DEBUG_BOOT = !DEBUG_BOOT; echoed = -1; continue; } #endif if (selected == 4) { ui_clear(); ui_text_centered((int)ui_height() / 2, "Halted -- reset to reach firmware setup", 0xB9BEC7u); for (;;) __asm__ volatile("hlt"); } } } } /* --- the other displays ----------------------------------------------------- * * A machine with two adapters has two screens, and the loader drew on one of * them. The other was black through the splash, black through the F8 menu, and * black until vgpud started several seconds into userspace -- which from the * outside is indistinguishable from a display that does not work. * * LocateProtocol() answers with the first GOP and offers no way to ask for the * rest, so the rest are found by handle. Each one is then fed the *primary's * framebuffer* as a Blt source: the loader keeps drawing into one piece of * memory exactly as before, and this copies it outward at the points where a * screen is finished. * * Blt rather than a second framebuffer to draw into, because a virtio GOP does * not have one -- it reports a FrameBufferBase of zero and implements Blt and * nothing else. Which is the same fact that made a virtio-only machine * reboot-loop; see kernel/core/main.c. */ #define MAX_MIRRORS 4 static EFI_GRAPHICS_OUTPUT_PROTOCOL *mirror_gop[MAX_MIRRORS]; static EFI_GRAPHICS_OUTPUT_PROTOCOL *primary_gop; static UINTN mirror_n; static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary); static void describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag); static void mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary) { EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; EFI_HANDLE *handles = 0; UINTN count = 0; primary_gop = primary; // An off switch, because this is a nicety and booting is not. // // Everything below talks to display adapters the machine may be using for // something else, through firmware drivers of wildly varying quality. If // that goes wrong there is no console left to say so on and no way to ask // a person anything -- so the answer has to already be on the volume. // `\schos\nomirror` is an empty file whose only job is to exist. if (boot_file_exists(L"\\schos\\nomirror")) { print(L"schos: \\schos\\nomirror present; not mirroring\r\n"); return; } if (!BS->LocateHandleBuffer || EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0, &count, &handles)) || !handles) return; for (UINTN i = 0; i < count && mirror_n < MAX_MIRRORS; i++) { EFI_GRAPHICS_OUTPUT_PROTOCOL *g = 0; if (EFI_ERROR(BS->HandleProtocol(handles[i], &gop_guid, (void **)&g))) continue; if (!g || g == primary || !g->Blt || !g->Mode || !g->Mode->Info) continue; // Not a second display: the console splitter. Firmware installs one // GOP of its own on a virtual handle and forwards every Blt to all of // the real ones -- the primary included. Mirroring onto it copied the // primary onto itself through the splitter's idea of the mode, which // stops being the primary's the moment the resolution menu changes // it: the picture landed again, offset and with the wrong row length, // over the one already there. That was the doubled, sheared screen with // the logo's edges repeated down it, at every size but the one the // firmware booted in. A real adapter's GOP is on a handle with a path // to the device; the splitter's has none. void *dp = 0; EFI_GUID dp_guid = EFI_DEVICE_PATH_PROTOCOL_GUID; if (EFI_ERROR(BS->HandleProtocol(handles[i], &dp_guid, &dp)) || !dp) continue; // Nor is a second GOP whose aperture is the primary's: one panel // reached twice, which laptops do. Blitting onto it rewrites the // primary's own memory in another mode's layout. UINT64 pb = primary->Mode->FrameBufferBase; UINT64 pe = pb + primary->Mode->FrameBufferSize; UINT64 gb = g->Mode->FrameBufferBase; UINT64 ge = gb + g->Mode->FrameBufferSize; if (gb && pb && gb < pe && pb < ge) continue; mirror_gop[mirror_n++] = g; } if (mirror_n) mirrors_match(primary); } /* Put every mirror into the primary's mode, if it has one. * * Without this, choosing 1024x768 at the F8 menu changed the screen it was * chosen on and left the other one at whatever the firmware had picked -- so * the menu and then the splash appeared on it as a small rectangle in the * middle of a larger black one. The resolution in that menu is a statement * about the machine's display, not about one adapter of it. * * Cheap to call on every frame: the common case is two integer comparisons and * no mode set at all. A mirror with no matching mode keeps the one it has and * gets the centred-and-cropped treatment below, which is still better than * nothing. */ static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary) { // Called from mirrors_find(), before anything has been drawn, and again // when the resolution menu sets a mode -- never per frame. It used to be // called from mirrors_show() -- which is to say on // every menu redraw and on every segment of the kernel load -- and that is // not a safe thing to do to another display adapter. // // Setting a mode is not a cheap call on real hardware. It can take the // best part of a second, it can blank a panel while it happens, and on a // laptop the "other" adapter is quite often the *same* panel reached // through a second GOP -- so doing it repeatedly, in the middle of drawing // a progress bar, is a good way to produce a machine that shows a // background colour and nothing else while the loader waits on a firmware // call that is not coming back. UINT32 w = primary->Mode->Info->HorizontalResolution; UINT32 h = primary->Mode->Info->VerticalResolution; for (UINTN i = 0; i < mirror_n; i++) { EFI_GRAPHICS_OUTPUT_PROTOCOL *g = mirror_gop[i]; if (g->Mode->Info->HorizontalResolution == w && g->Mode->Info->VerticalResolution == h) continue; for (UINT32 m = 0; m < g->Mode->MaxMode; m++) { EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = 0; UINTN sz = 0; if (EFI_ERROR(g->QueryMode(g, m, &sz, &mi)) || !mi) continue; if (mi->HorizontalResolution == w && mi->VerticalResolution == h) { g->SetMode(g, m); break; } } } } /* Put what is on the primary onto all of them. Centred when the sizes differ, * cropped when the other display is smaller -- the same rules the kernel's * console mirror and vgpud's both use, because it is the same picture. */ static void mirrors_show(void) { EFI_GRAPHICS_OUTPUT_PROTOCOL *primary = primary_gop; if (!mirror_n || !primary || !primary->Mode || !primary->Mode->Info) return; if (!primary->Mode->FrameBufferBase) return; /* nothing to copy *from* */ UINTN sw = primary->Mode->Info->HorizontalResolution; UINTN sh = primary->Mode->Info->VerticalResolution; UINTN delta = (UINTN)primary->Mode->Info->PixelsPerScanLine * 4; for (UINTN i = 0; i < mirror_n; i++) { EFI_GRAPHICS_OUTPUT_PROTOCOL *g = mirror_gop[i]; UINTN w = g->Mode->Info->HorizontalResolution; UINTN h = g->Mode->Info->VerticalResolution; UINTN cw = w < sw ? w : sw; UINTN ch = h < sh ? h : sh; // One refusal and the whole feature stops for this boot. A firmware // Blt that fails once will fail every time, and retrying it on every // screen is how a cosmetic feature turns into a hang. if (EFI_ERROR(g->Blt(g, (void *)(UINTN)primary->Mode->FrameBufferBase, EfiBltBufferToVideo, 0, 0, (w - cw) / 2, (h - ch) / 2, cw, ch, delta))) { mirror_n = 0; return; } } } // --------------------------------------------------------------------------- // Choosing a display, and saying what was chosen // // LocateProtocol answers with *a* GOP. On a laptop with switchable graphics // there are several -- the integrated adapter, the discrete one, sometimes a // console splitter on top of both -- and which one comes back first is not // something the specification says anything about. Drawing into the wrong one // is a black screen with the firmware's cursor still blinking on it, which is // indistinguishable from a loader that crashed. // // So all of them are looked at, and the choice is made on what they report // rather than on the order they were handed over. // --------------------------------------------------------------------------- static const CHAR16 * fmt_name(UINT32 f) { switch (f) { case 0: return L"RGBX"; case 1: return L"BGRX"; case 2: return L"bitmask"; default: return L"Blt-only"; } } static void describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag) { EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = g->Mode ? g->Mode->Info : 0; print(tag); if (!mi) { print(L" (no mode information)\r\n"); return; } print(L" "); print_dec(mi->HorizontalResolution); print(L"x"); print_dec(mi->VerticalResolution); print(L" "); print(fmt_name(mi->PixelFormat)); print(L" scanline "); print_dec(mi->PixelsPerScanLine); print(L" fb "); print_hex(g->Mode->FrameBufferBase); print(L" size "); print_hex(g->Mode->FrameBufferSize); print(L"\r\n"); } // Better, for this purpose, means: it has an aperture we can write into, its // current mode is one we can read, and failing a tie, it is bigger. A GOP with // no framebuffer address is not a candidate at all -- the kernel needs a // linear aperture and cannot Blt. static int gop_score(EFI_GRAPHICS_OUTPUT_PROTOCOL *g) { EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *mi = g->Mode ? g->Mode->Info : 0; if (!mi || !g->Mode->FrameBufferBase || !g->Mode->FrameBufferSize) return 0; if (!mode_usable(mi)) return 1; /* has memory; mode needs changing */ return 2; } static EFI_GRAPHICS_OUTPUT_PROTOCOL * pick_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *first) { EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; EFI_GRAPHICS_OUTPUT_PROTOCOL *best = first; EFI_HANDLE *handles = 0; UINTN count = 0, i; int best_score = first ? gop_score(first) : 0; UINT64 best_area = 0; if (first && first->Mode && first->Mode->Info) best_area = (UINT64)first->Mode->Info->HorizontalResolution * first->Mode->Info->VerticalResolution; if (!BS->LocateHandleBuffer || EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0, &count, &handles)) || !handles) return best; print(L"schos: "); print_dec((UINT32)count); print(L" display adapter(s)\r\n"); for (i = 0; i < count; i++) { EFI_GRAPHICS_OUTPUT_PROTOCOL *g = 0; int score; UINT64 area; if (EFI_ERROR(BS->HandleProtocol(handles[i], &gop_guid, (void **)&g)) || !g || !g->Mode) continue; describe_gop(g, L" gop:"); score = gop_score(g); area = g->Mode->Info ? (UINT64)g->Mode->Info->HorizontalResolution * g->Mode->Info->VerticalResolution : 0; if (score > best_score || (score == best_score && area > best_area)) { best = g; best_score = score; best_area = area; } } return best; } /* Wait briefly for F8, exactly as the BIOS loader does, and show the same * menu if it comes. One prompt, one key, one screen -- the resolution chooser * lives in that menu rather than in a second prompt of its own. */ static int f8_window(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) { print(L"Press F8 for boot options... "); EFI_INPUT_KEY key; for (UINTN waited = 0; waited < 1500; waited += 10) { if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) { /* SCAN_F8 is 0x12. A serial console sends no scan codes at all, so * 'o' is accepted too -- otherwise this is unreachable for anyone * working over a serial line, which is how it is usually driven. */ if (key.ScanCode == 0x12 || key.UnicodeChar == L'o' || key.UnicodeChar == L'O') { print(L"\r\n"); return boot_menu(gop); } break; } BS->Stall(10000); } print(L"booting\r\n"); return 0; } static 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) { // Sorted by address first. The specification does not promise an ordered // map, firmware that hands one over unsorted exists, and merging only // finds neighbours that sit next to each other in the list. static struct e820entry raw[1024]; UINT32 nraw = 0, nent = 0; for (UINTN off = 0; off + descsize <= mapsize && nraw < 1024; off += descsize) { EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off); struct e820entry e = { d->PhysicalStart, d->NumberOfPages * 0x1000, efi_type_to_e820(d->Type), 1 }; UINT32 i = nraw++; while (i > 0 && raw[i - 1].base > e.base) { raw[i] = raw[i - 1]; i--; } raw[i] = e; } for (UINT32 i = 0; i < nraw && nent < E820_MAXENTRIES; i++) { if (nent > 0 && out[nent-1].type == raw[i].type && out[nent-1].base + out[nent-1].length == raw[i].base) { out[nent-1].length += raw[i].length; continue; } out[nent++] = raw[i]; } return nent; } // The ACPI RSDP, from the UEFI configuration table. // // The kernel's own search scans the EBDA and 0xE0000-0x100000. That is a BIOS // convention, and UEFI firmware is under no obligation to leave a copy there -- // OVMF does not. The kernel therefore found no RSDP, so no MADT, so no IOAPIC, // so nothing routed IRQ 4: it booted to a shell prompt on polled serial output // and then ignored every key, which reads as a broken console rather than a // missing table. // // ACPI 2.0 is preferred over 1.0 because its RSDP carries the XSDT, whose // entries are 64-bit; the 1.0 RSDT is kept only as a fallback. static UINT64 find_rsdp(void) { EFI_GUID acpi20 = EFI_ACPI_20_TABLE_GUID; EFI_GUID acpi10 = EFI_ACPI_10_TABLE_GUID; EFI_CONFIGURATION_TABLE *t = (EFI_CONFIGURATION_TABLE *)ST->ConfigurationTable; UINT64 fallback = 0; for (UINTN i = 0; i < ST->NumberOfTableEntries; i++) { if (guid_eq(&t[i].VendorGuid, &acpi20)) return (UINT64)(UINTN)t[i].VendorTable; if (guid_eq(&t[i].VendorGuid, &acpi10)) fallback = (UINT64)(UINTN)t[i].VendorTable; } return fallback; } // The SMBIOS entry point, the same way: 3.0's 64-bit one first, since a // table above 4 GB is out of reach of the 2.x one. static UINT64 find_smbios(void) { EFI_GUID sm3 = SMBIOS3_TABLE_GUID; EFI_GUID sm2 = SMBIOS_TABLE_GUID; EFI_CONFIGURATION_TABLE *t = (EFI_CONFIGURATION_TABLE *)ST->ConfigurationTable; UINT64 fallback = 0; for (UINTN i = 0; i < ST->NumberOfTableEntries; i++) { if (guid_eq(&t[i].VendorGuid, &sm3)) return (UINT64)(UINTN)t[i].VendorTable; if (guid_eq(&t[i].VendorGuid, &sm2)) fallback = (UINT64)(UINTN)t[i].VendorTable; } return fallback; } EFI_STATUS EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) { ST = systab; BS = systab->BootServices; print(L"SchOS UEFI loader\r\n"); // The display comes first, before the kernel is read. // // It used to be the other way round, which meant the resolution prompt // appeared after the slowest step and the progress bar was drawn into a // framebuffer that did not exist yet -- every tick was silently discarded // and then the splash painted over the result. Setting the mode first also // puts the menu where a person expects it: at the start. // The volume first: what is on it decides some of what the display does, // and the display is the only thing that can report a failure afterwards. open_boot_volume(image); 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"); UINT64 smbios = find_smbios(); // The memory map, and then out. GetMemoryMap must be the last thing before // ExitBootServices: the key it returns is invalidated by any allocation, // and AllocatePool for the map buffer is itself an allocation -- hence the // static buffer and the retry. ui_progress_label("Reading the memory map"); // A quarter of a megabyte, and the size is not a guess. // // This was 32 KB, which is about 680 descriptors, and on real firmware // that is not enough: GetMemoryMap answered EFI_BUFFER_TOO_SMALL (0x5) and // the loader stopped there. A modern machine's map is not a handful of // regions -- it is every reserved range the firmware has ever carved out, // and the count is a property of the vendor rather than of the RAM. // // Asking first is what makes the size defensible rather than a bigger // guess: a call with a zero size fails on purpose and fills in how much is // actually needed. It allocates nothing, so the key from the real call // below is still valid -- which is the whole constraint this code is // written around. UINTN mapsize = 0, mapkey = 0, descsize = 0; UINT32 descver = 0; EFI_STATUS st; st = map_read(&mapsize, &mapkey, &descsize, &descver); if (st == EFI_BUFFER_TOO_SMALL && mapsize > sizeof mapbuf) { // Say the number. A loader that dies on "too small" without saying how // small is a loader that costs somebody a rebuild to find out. print(L"schos: the firmware's memory map needs "); print_dec(mapsize); print(L" bytes and this loader has "); print_dec(sizeof mapbuf); print(L"\r\n"); die(L"memory map larger than the loader's buffer", st); } if (EFI_ERROR(st)) die(L"GetMemoryMap failed", st); // Translate before leaving, while the map is still readable and while // printing still works, but write it to low memory only after the firmware // has let go -- 0x30000 may well be firmware data until then. static struct e820entry e820[E820_MAXENTRIES]; UINT32 nent = build_e820(mapbuf, mapsize, descsize, e820); // The hand-off block is written after ExitBootServices into memory nobody // allocated, which is safe only while the firmware keeps nothing there // that outlives it. Say so if it does, since the alternative is a kernel // that dies in a way nothing points back at. for (UINT32 i = 0; i < nent; i++) if (e820[i].type != E820_USABLE && e820[i].base < SCHBOOT_E820_MAP + SCHBOOT_E820_MAX * 24 && e820[i].base + e820[i].length > SCHBOOT_BASE) { print(L"schos: WARNING: firmware memory overlaps the hand-off " L"block at 0x30000\r\n"); ui_progress_label("Firmware memory at 0x30000 -- may not boot"); BS->Stall(2000000); break; } // Truncation is silent data loss: phystop comes from the largest usable // region, so dropping the tail of the map can leave the kernel believing a // 16 GB machine has a fraction of that. Say so while there is still a // console to say it on -- after ExitBootServices there is nowhere to // report anything at all. if (nent >= E820_MAXENTRIES) { print(L"schos: WARNING: memory map truncated, some RAM will be unused\r\n"); // And on the screen, because by this point print() goes to a serial // port that a laptop does not have. The entries that get dropped are // the high ones -- the map arrives sorted, and phystop is capped below // 4 GB anyway -- so this is a warning rather than a failure. It is // still not a thing to discover months later from a memory total that // never looked quite right. ui_progress_label("Memory map truncated -- some RAM unused"); BS->Stall(2000000); } ui_progress_label("Leaving the firmware"); vprint(L"schos: exiting boot services\r\n"); st = BS->ExitBootServices(image, mapkey); if (EFI_ERROR(st)) { // The map changed underneath us between GetMemoryMap and here. Take it // again and retry once; this is normal and the specification expects // a loader to handle it. st = map_read(&mapsize, &mapkey, &descsize, &descver); if (EFI_ERROR(st)) die(L"GetMemoryMap failed on retry", st); // Translate again. The retry exists because the map *changed*, so // handing the kernel the table built from the previous one describes // memory the firmware has already rearranged. This path is normal on // real firmware and never taken under OVMF, which is why it survived. nent = build_e820(mapbuf, mapsize, descsize, e820); st = BS->ExitBootServices(image, mapkey); if (EFI_ERROR(st)) die(L"ExitBootServices failed", st); } // ---- no firmware services exist past this line ---- // // ConOut is gone, but the *framebuffer* is not: it is memory, the firmware // has stopped using it, and ui_progress_label() is a write to it and // nothing more. That distinction is worth making rather than assuming -- // an earlier version of this comment claimed nothing could report from // here, and that claim cost a round trip to a laptop and back to learn // which of three steps a machine had stopped in. __asm__ volatile("cli"); copy((void *)SCHBOOT_E820_MAP, e820, nent * sizeof e820[0]); *(UINT32 *)SCHBOOT_E820_COUNT = nent; *(UINT64 *)SCHBOOT_RSDP_ADDR = rsdp; *(UINT64 *)SCHBOOT_SMBIOS_ADDR = smbios; *(UINT32 *)SCHBOOT_EFI_FLAG = SCHBOOT_EFI_MAGIC; *(UINT8 *)SCHBOOT_VERBOSE_FLAG = (UINT8)VERBOSE_BOOT; *(UINT8 *)SCHBOOT_DEBUG_FLAG = (UINT8)DEBUG_BOOT; // The page tables mboot_entry would have built. entry64low's first act is // a movabs to 0xffff8000...., so the high half must be mapped before the // jump, not after: identity alone triple faults on the first instruction. UINT64 *pml4 = (UINT64 *)PML4_ADDR; UINT64 *pdpt = (UINT64 *)PDPT_ADDR; zero(pml4, 0x1000); zero(pdpt, 0x1000); pml4[0] = PDPT_ADDR | PTE_P | PTE_W; // identity pml4[256] = PDPT_ADDR | PTE_P | PTE_W; // 0xffff800000000000 // **All 512 entries, not four.** // // Four gigabytes was enough for as long as everything that mattered lived // below four gigabytes -- and on a machine with a lot of RAM, one thing // does not: *this loader*. The firmware places the image where it likes, // and on an 8 GB machine it lands above 4 GB. The instruction after // // mov %rax, %cr3 // // is then an instruction fetch from an address these tables do not map, // and the machine dies between one instruction and the next, with the // splash still on screen and nothing said: // // X64 Exception Type - 0E(#PF - Page-Fault) // ExceptionData - 0x10 I:1 P:0 // RIP - 0000000140004DEE <- the loader, at 5.4 GB // RAX - 0000000000001000 <- the page tables it just installed // RCX - 00000000001000F0 <- the kernel it was about to jump to // // That is the whole of "it freezes at Starting the kernel". It reproduces // on QEMU with `-m 8G` and never on `-m 1G`, which is why every test here // passed. // // The fix costs nothing: the PDPT is one page whether four entries are // filled or all of them, and 512 GB of identity map covers any machine // whose firmware is going to load us somewhere surprising. The kernel // replaces all of this in kvmalloc() a few hundred instructions later. for (int i = 0; i < 512; i++) pdpt[i] = ((UINT64)i << 30) | PTE_P | PTE_PS | PTE_W; 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"); }