diff --git a/boot/uefi/main.c b/boot/uefi/main.c index c0de95c..48c2e07 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -1509,6 +1509,13 @@ setup_framebuffer(void) // 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 diff --git a/docs/architecture/boot.md b/docs/architecture/boot.md index 1692673..849eb8d 100644 --- a/docs/architecture/boot.md +++ b/docs/architecture/boot.md @@ -122,6 +122,44 @@ when the early console is set up. So a low framebuffer is attached exactly as before, and a high one is attached a few dozen instructions later, immediately after `kvmalloc()` — still thousands of lines before anything else happens. +### Two signals for a machine that cannot speak + +A laptop has no serial port. When it stops with the loader's splash still on +screen, three very different things look identical: the jump never landed, the +kernel started and hung, or the kernel started and *panicked* with a full +report drawn into a framebuffer nobody can see. Each calls for a different next +step, so the kernel now produces two signals that need no console at all. + +**A bar, as the kernel's first act.** Eight lines of accent colour across the +top of the framebuffer, written straight through the handoff block before a +single subsystem exists — no gfx, no scanout, no allocator. If it appears over +the loader's splash, the jump landed and the kernel is executing; if it does +not, nothing below that point is worth looking at. It is skipped when the +framebuffer is above the direct map, because a fault there would leave the +machine quieter still, and it costs nothing on a boot that works: the console +clears the screen a few lines later. + +**Blinking keyboard lights, on panic.** `panic_blink()` toggles Scroll, Num and +Caps together, about three times a second, for ever. It is the one output every +PC has had since before any of this existed: no driver, no interrupt, no +memory, and it works when the graphics do not. Linux does the same thing for +the same reason. + +Every wait in it is bounded. It runs after `panicked` is set, so there is +nothing left to protect — but a machine that hangs inside its own death rattle +tells you less than one that stops quietly, so no loop in there can be entered +that cannot be left. The delay is `microdelay`, which spins on the TSC and +needs no timer, no interrupt and no calibration — all three of which a kernel +that died during boot may not have. + +So the three cases separate: + +| What you see | What happened | +| --- | --- | +| No bar, no blink | The jump did not land. The loader's last label says where. | +| Bar, no blink | The kernel is executing and hung. Not a panic. | +| Blinking lights | The kernel panicked. The report is in `/proc/kmsg` after a restart. | + ### The loader says which stage it is in After the splash, `print()` goes to the serial port and nowhere else — writing diff --git a/kernel/core/main.c b/kernel/core/main.c index 69f80c1..ddf766e 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -50,6 +50,34 @@ kmain(uint32 fb_addr) boot_verbose = *(uchar*)P2V(SCHBOOT_VERBOSE_FLAG); boot_debug = *(uchar*)P2V(SCHBOOT_DEBUG_FLAG); + // The kernel's first act: put a mark on the screen. + // + // Eight lines, before a single subsystem exists, because "the loader + // finished and the machine froze" has two completely different causes and + // nothing else here can tell them apart. If this bar appears over the + // loader's splash, the jump landed, the page tables are good and the kernel + // is executing; if it does not, none of those are true and everything below + // is the wrong place to look. + // + // It writes straight through the handoff block rather than through gfx, + // because gfx needs a scanout attached and attaching one is itself a thing + // that can fail. Skipped when the framebuffer is above the direct map -- a + // fault here would be a triple fault with the machine even quieter than it + // was. It costs nothing on a boot that works: the console clears the screen + // a few lines later. + { + uint64 pa = *(uint64*)P2V(SCHBOOT_FB_BASE); + uint32 w = *(uint32*)P2V(SCHBOOT_FB_WIDTH); + uint32 st = *(uint32*)P2V(SCHBOOT_FB_STRIDE); + + if (pa && w && st && pa + (uint64)st * 8 <= 4ULL << 30) { + volatile uint32 *px = (volatile uint32 *)P2V(pa); + for (uint32 y = 0; y < 8; y++) + for (uint32 x = 0; x < w; x++) + px[y * (st / 4) + x] = 0x7C5CFF; /* SCH_ACCENT */ + } + } + // On a verbose boot, put the kernel log on the screen from the first line. // // Everything below talks to the serial port and nothing else until the first diff --git a/kernel/core/panic.c b/kernel/core/panic.c index 444fad1..f6380c6 100644 --- a/kernel/core/panic.c +++ b/kernel/core/panic.c @@ -27,6 +27,8 @@ // before there is a framebuffer at all. #include "types.h" +#include "kbd.h" +#include "x86.h" #include "defs.h" #include "gfx.h" #include "param.h" @@ -506,6 +508,8 @@ report_serial(char *reason, addr_t *pcs, int npcs) cprintf(" System halted.\n"); } +static void panic_blink(void) __attribute__((noreturn)); + __attribute__((noreturn)) void panic(char *s) @@ -534,6 +538,78 @@ panic(char *s) } panicked = 1; - for (;;) - hlt(); + panic_blink(); +} + +// The last thing a dead kernel can say, on a machine that cannot say anything. +// +// A laptop has no serial port. If the panic screen cannot be drawn -- no +// framebuffer yet, the wrong framebuffer, a display the firmware handed over +// in a state we did not expect -- then everything above this has produced a +// black screen and a machine that appears to have hung. "Frozen" and "panicked +// with a full report nobody can see" look identical, and they call for +// completely different next steps. +// +// So: blink the keyboard lights. It is the one output every PC has had since +// before any of this existed, it needs no driver, no interrupt and no memory, +// and it works when the graphics do not. Linux does the same thing and for the +// same reason. +// +// Every wait here is bounded. This runs after `panicked` is set, so there is +// nothing left to protect and nothing left to return to -- but a machine that +// hangs inside its own death rattle tells you even less than one that stops +// quietly, so no loop in here can be entered that cannot be left. +static void +kbd_out(uchar byte) +{ + // Wait for the controller to have room, then write. Bit 1 of the status + // port is "input buffer full"; writing while it is set loses the byte. + for (int i = 0; i < 100000; i++) { + if (!(inb(KBSTATP) & 0x02)) { + outb(KBDATAP, byte); + return; + } + microdelay(10); + } +} + +static void +kbd_drain_ack(void) +{ + // The keyboard answers 0xFA. Reading it matters less than not leaving it in + // the buffer for the next command to mistake for its own answer. + for (int i = 0; i < 100000; i++) { + if (inb(KBSTATP) & KBS_DIB) { + (void)inb(KBDATAP); + return; + } + microdelay(10); + } +} + +// Scroll, Num and Caps, which is bits 0, 1 and 2 of the byte after 0xED. +#define LED_ALL 0x07 + +static void +panic_blink(void) +{ + int on = 0; + + for (;;) { + kbd_out(0xED); + kbd_drain_ack(); + kbd_out(on ? LED_ALL : 0x00); + kbd_drain_ack(); + on = !on; + + // About a third of a second either way. Slow enough to read across a room + // as deliberate rather than as a stuck light, fast enough that nobody + // watching a machine they think has hung will have walked away first. + // + // microdelay spins on the TSC, which needs no timer, no interrupt and no + // calibration that may not have happened yet -- all three of which a + // kernel that died during boot may be missing. + for (int i = 0; i < 300; i++) + microdelay(1000); + } }