// SPDX-License-Identifier: GPL-2.0-or-later // // PSOGM — the SchOS crash screen. // // The name is a nod to the Amiga's "Guru Meditation", but nothing else about // this is a nod to 1985. Three things it is built around: // // *It takes the whole screen.* An earlier version dimmed what was there and // floated a panel over it, on the argument that a crash screen which wipes the // display tells you the machine is gone while one that leaves your work visible // tells you it stopped. That looked good and was wrong: the thing is called a // Purple Screen, and half a screen of somebody's abandoned work behind the // report is a distraction at the worst possible moment. // // *It is not frightening.* No red, no capitals, no "FATAL". A calm sentence, // the system's own mark, and then the facts. Whoever is reading this is // already having a bad time. // // *It says considerably more than it needs to.* The screen is the only artefact // of a kernel panic — there is no core dump and no log to go back to — so // everything cheap to collect is collected: which CPU, which process, how far // into the boot, where the kernel is loaded, and a call chain. Anything left // out here is gone. // // Everything is also written to the serial console, in the same order. That is // what tools/bootcheck.ps1 reads, and it is what you get when the crash happens // before there is a framebuffer at all. #include "core/types.h" #include "dev/kbd.h" #include "arch/x86.h" #include "core/defs.h" #include "gfx/gfx.h" #include "core/param.h" #include "arch/memlayout.h" #include "arch/mmu.h" #include "arch/x86.h" #include "core/proc.h" #include "core/spinlock.h" // Purple, because the name says so, and white on it because nothing else is // legible at every brightness a monitor might be set to. The palette lives in // theme.h with everything else; this file used to keep its own copy under the // same names but with different values, which meant theme.h described a crash // screen nobody had ever seen. #include "gfx/theme.h" #include "gfx/logo.h" /* LOGO_H -- the mark's height, for the layout below */ #include "gfx/qr.h" #include "dev/acpi.h" extern uint ticks; extern int panicked; /* ------------------------------------------------------------------ */ /* Small formatting helpers */ /* */ /* cprintf goes to the console, which during a panic is also the screen */ /* being painted over -- so the panel builds its strings itself and */ /* places them with gfx_text_at. */ /* ------------------------------------------------------------------ */ static char * put_str(char *p, char *end, const char *s) { while (*s && p < end - 1) *p++ = *s++; *p = '\0'; return p; } static char * put_dec(char *p, char *end, uint64 v) { char tmp[24]; int i = 0; if (v == 0) tmp[i++] = '0'; while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; } while (i-- && p < end - 1) *p++ = tmp[i]; *p = '\0'; return p; } static char * put_hex(char *p, char *end, uint64 v, int digits) { const char *d = "0123456789abcdef"; if (p < end - 2) { *p++ = '0'; *p++ = 'x'; } for (int i = digits - 1; i >= 0 && p < end - 1; i--) *p++ = d[(v >> (i * 4)) & 0xF]; *p = '\0'; return p; } /* ------------------------------------------------------------------ */ /* The report */ /* ------------------------------------------------------------------ */ struct field { const char *label; char value[72]; }; #define NFIELD 10 static struct field fields[NFIELD]; static int nfields; static struct field * field(const char *label) { if (nfields >= NFIELD) return &fields[NFIELD - 1]; struct field *f = &fields[nfields++]; f->label = label; f->value[0] = '\0'; return f; } // Collect everything worth knowing while the machine is still in the state // that produced the fault. Interrupts are already off and no lock is taken: // a panic that deadlocks trying to report itself tells you nothing at all. // A short, shoutable name for the failure, derived from the panic message // rather than declared at every call site. // // Windows stop codes are genuinely useful: they are the thing you search for, // and they stay the same across builds while the prose around them changes. // Taking the message up to its first colon (or its first few words) gets that // for free -- "sysrq: crash requested" becomes SYSRQ, "short filewrite" becomes // SHORT_FILEWRITE -- with no risk of a call site forgetting to supply one. static void stop_code(char *out, int cap, const char *reason) { int n = 0; for (int i = 0; reason[i] && n < cap - 1; i++) { char c = reason[i]; if (c == ':') break; if (c == ' ' || c == '\t') { if (n == 0 || out[n-1] == '_') continue; out[n++] = '_'; continue; } if (c >= 'a' && c <= 'z') c = c - 'a' + 'A'; if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') out[n++] = c; } while (n > 0 && out[n-1] == '_') n--; if (n == 0) { out[n++] = 'P'; out[n++] = 'A'; out[n++] = 'N'; out[n++] = 'I'; out[n++] = 'C'; } out[n] = '\0'; } static void gather(char *reason, addr_t *pcs, int npcs) { char *e; struct field *f; nfields = 0; f = field("Stop code"); stop_code(f->value, sizeof(f->value), reason); f = field("Reason"); put_str(f->value, f->value + sizeof(f->value), reason); f = field("CPU"); e = f->value + sizeof(f->value); put_dec(f->value, e, mycpu() ? (uint64)mycpu()->id : 0); if (!mycpu()) put_str(f->value, e, "unknown (very early boot)"); f = field("Process"); e = f->value + sizeof(f->value); if (myproc()) { char *p = put_str(f->value, e, myproc()->name); p = put_str(p, e, " pid "); put_dec(p, e, (uint64)myproc()->pid); } else { put_str(f->value, e, "none -- in the kernel, not on anybody's behalf"); } f = field("Uptime"); e = f->value + sizeof(f->value); { char *p = put_dec(f->value, e, (uint64)ticks); p = put_str(p, e, " ticks (about "); p = put_dec(p, e, (uint64)ticks / HZ); put_str(p, e, " s)"); } // Every address below is a link-time address. Printing where the kernel // actually sits is what lets addr2line resolve them afterwards, and it is // the single most useful line here when the trace is all you have. f = field("Kernel base"); e = f->value + sizeof(f->value); put_hex(f->value, e, (uint64)KERNBASE, 16); f = field("Fault PC"); e = f->value + sizeof(f->value); if (npcs > 0 && pcs[0]) put_hex(f->value, e, (uint64)pcs[0], 16); else put_str(f->value, e, "unavailable"); } /* ------------------------------------------------------------------ */ /* The QR code */ /* */ /* A panic report is only worth what can be carried away from it, and */ /* at this point there is no network, no log to write to, and often no */ /* second machine on the desk. Photographing the screen works but */ /* turns a call chain into something that has to be retyped by hand, */ /* with sixteen hex digits per frame and no way to tell 8 from B. */ /* */ /* A phone camera is a working out-of-band channel that needs no */ /* driver on our side at all. See kernel/core/qr.c. */ /* ------------------------------------------------------------------ */ // Deliberately not a URL. This encodes the report itself, so it goes wherever // the person holding the phone decides -- and reaches nothing on its own. // // Compact because the budget is 134 bytes for the largest symbol this encoder // builds, and a crash with a deep call chain is exactly when the budget is // tightest. Two decisions buy most of the room: frames are stored as offsets // from the kernel base rather than absolute addresses, which halves 16 hex // digits to 8, and the fields are single letters. The base is included, so // nothing is lost -- addr2line wants the link-time address and can be given it. // // Fields are appended in priority order and the loop simply stops when the // buffer is full, so a long reason string costs call-chain depth rather than // producing a report that will not encode. static int build_qr_payload(char *buf, int cap, char *reason, addr_t *pcs, int npcs) { char *p = buf, *end = buf + cap; // Magic and format version, so whatever reads this later can tell what it is // looking at and can change the layout without ambiguity. p = put_str(p, end, "SXPSOGM1\n"); // The reason, not the stop code: the stop code is derived from the reason by // stop_code() and carries strictly less, so encoding both would spend bytes // on something the reader can recompute. // // Held back from the end of the buffer so that a long message costs some of // the call chain but never all of it. A report with a sentence and no // addresses is much harder to act on than one with addresses and a truncated // sentence. { char *reason_end = end - 34; // three frames and their separators if (reason_end > p) p = put_str(p, reason_end, reason); } p = put_str(p, end, "\nc"); p = put_dec(p, end, mycpu() ? (uint64)mycpu()->id : 0); p = put_str(p, end, " p"); p = put_dec(p, end, myproc() ? (uint64)myproc()->pid : 0); p = put_str(p, end, " t"); p = put_dec(p, end, (uint64)ticks); if (myproc()) { p = put_str(p, end, " "); p = put_str(p, end, myproc()->name); } p = put_str(p, end, "\nb"); p = put_hex(p, end, (uint64)KERNBASE, 16); p = put_str(p, end, "\n"); for (int i = 0; i < npcs && pcs[i]; i++) { // Nine bytes: eight hex digits and the separator. Stopping here rather than // letting put_hex truncate matters -- half an address in a crash report is // worse than one fewer address, because it looks like a real one. if (end - p < 10) break; if (i) p = put_str(p, end, " "); // put_hex prefixes "0x", which is 14 wasted bytes across seven frames. { const char *d = "0123456789abcdef"; uint64 off = (uint64)pcs[i] - (uint64)KERNBASE; for (int k = 7; k >= 0; k--) *p++ = d[(off >> (k * 4)) & 0xF]; *p = '\0'; } } return (int)(p - buf); } // One static symbol rather than a local: struct qr is about 3.4 KB and the // panic path runs on whatever stack the fault left behind, which may not have // that much room. It is only ever touched here, after everything else has // stopped. static struct qr panic_qr; // Where the card and its caption are, once drawn: the text column must stop // short of it on the rows they share. The reason and the call chain used to // run straight underneath it. static int qr_x0 = -1, qr_y0, qr_y1; static void text_at(int x, int y, const char *s, uint32 color) { char buf[128]; int n = 0, cap = sizeof(buf) - 1; if (qr_x0 >= 0 && y + gfx_char_h() > qr_y0 && y < qr_y1) { int fit = (qr_x0 - gfx_char_w() - x) / gfx_char_w(); if (fit < cap) cap = fit < 0 ? 0 : fit; } while (s[n] && n < cap) { buf[n] = s[n]; n++; } buf[n] = 0; gfx_text_at(x, y, buf, color); } // Returns the width used, so the text column knows what it may not overlap. static int draw_qr_panel(int sw, int sh, int ch, char *reason, addr_t *pcs, int npcs) { char payload[QR_CARD_BYTES]; int n = build_qr_payload(payload, sizeof(payload), reason, pcs, npcs); if (n <= 0 || qr_encode(&panic_qr, (unsigned char *)payload, n) != 0) return 0; // Four modules of quiet zone, which the standard requires and which decoders // genuinely need -- without it many will not find the symbol at all. const int quiet = 4; int cells = panic_qr.size + 2 * quiet; // Scale to about a quarter of the display, then clamp. The lower bound is // what a phone can still resolve from a photograph of a monitor; the upper // bound stops the thing dominating a screen whose actual job is the text. int scale = (sw / 4) / cells; if (scale < 3) scale = 3; if (scale > 8) scale = 8; int card = cells * scale; int x0 = sw - card - sw / 24; int y0 = sh / 8; gfx_fill_rect(x0, y0, card, card, PSOGM_CARD); for (int y = 0; y < panic_qr.size; y++) for (int x = 0; x < panic_qr.size; x++) if (panic_qr.mod[y][x]) gfx_fill_rect(x0 + (x + quiet) * scale, y0 + (y + quiet) * scale, scale, scale, PSOGM_INK); gfx_text_at(x0, y0 + card + ch, "Scan for a text copy", PSOGM_DIM); gfx_text_at(x0, y0 + card + ch * 2 + 2, "of this report.", PSOGM_DIM); qr_x0 = x0; qr_y0 = y0; qr_y1 = y0 + card + ch * 3 + 2; return card + sw / 24; } // The frame pointers, one per line, as one string per row. static void trace_line(char *buf, int cap, int i, addr_t pc) { char *e = buf + cap; char *p = put_str(buf, e, " #"); p = put_dec(p, e, (uint64)i); p = put_str(p, e, i < 10 ? " " : " "); p = put_hex(p, e, (uint64)pc, 16); // And the name, when the kernel is carrying a symbol table. This is the // difference between a crash screen you photograph and then decode against // a disassembly, and one you can read. See kernel/core/ksym.c. if (ksym_ready()) { char name[64]; if (ksym_lookup((uint64)pc, 0)) { ksym_fmt(name, sizeof(name), (uint64)pc); p = put_str(p, e, " "); put_str(p, e, name); } } } /* ------------------------------------------------------------------ */ static void paint(char *reason, addr_t *pcs, int npcs) { int sw, sh; gfx_dims(&sw, &sh); int ch = gfx_char_h(); int cw = gfx_char_w(); // The whole display, one colour. An earlier version dimmed the previous // screen and floated a panel over it, which looked good and was wrong: the // thing is called a Purple Screen, and half a screen of somebody's abandoned // work behind the report is a distraction at the worst possible moment. gfx_clear(PSOGM_BG); // Glyphs are drawn with their background filled, so the console's cream has // to become the background or every line of text arrives in its own box. gfx_set_bg(PSOGM_BG); // The symbol goes first, because it decides how much width the text has. It // is drawn before anything can fail: if the encoder refuses the payload the // screen simply has no card and the report is unchanged. draw_qr_panel(sw, sh, ch, reason, pcs, npcs); int left = sw / 8; if (left > 110) left = 110; int y = sh / 8; // Everything above the three closing lines has to fit above them. At // 800x600 with a 21-pixel cell it did not: the call chain ran down into the // footer, the footer was drawn over it, and what showed was the tail of a // trace line poking out past the end of a sentence ("asks more.0x5d") with // the tops of "Call chain" underlining the line above. So the layout is // measured first -- the text, and room for two frames of the chain -- and // what gives way is the margin, then the logo. Never the report. int foot = sh - ch * 4 - ch / 2; int body = (ch + ch / 2) + ch * 2 + ch + nfields * (ch + 2) + ch + (ch + 2) + ch * 2; int logo = 1; if (y + LOGO_H + ch * 2 + body > foot) y = ch; if (y + LOGO_H + ch * 2 + body > foot) logo = 0; // Shift, the system's own mark, from the same include/gfx/logo.h the loader // splash and the boot console draw. It replaces Guru, a chamfered face that // lived only here and was drawn from primitives a few lines up this file. // // Guru was an *icon*: a pictogram denoting one state, with a face. The // argument for it was identity -- ":(" is recognisably Microsoft's, and a // crash screen is the single screenshot most likely to leave the machine, so // it is the worst place to be wearing somebody else's face. That argument // holds. It just does not follow that the answer is a second mark. The logo // already signs the system, it is already the thing on the splash the // machine started with, and a reader who has seen it twice before the crash // recognises it faster than a face they have never seen. // // A fixed 128 px rather than scaled: the mask is 128x128 and there is no // resampler here. The face was scalable and that bought nothing -- it was // drawn at one size. if (logo) { gfx_draw_logo(left, y, PSOGM_FG, PSOGM_BG); y += LOGO_H + ch * 2; } // Left-aligned, not centred. Centred text reads as a poster; this is a // report, and a report is read down the left edge. text_at(left, y, "SchOS ran into a problem and needs to restart.", PSOGM_FG); y += ch + ch / 2; text_at(left, y, "Nothing on your disks was touched by this.", PSOGM_DIM); // A rule in the boot splash's accent, the same width as the one under the // wordmark there, tying this screen to the one the machine started with. // // Equal space above and below on purpose: at any less it sits close enough to // the line above to read as an underline of that sentence rather than as a // divider before the facts. y += ch * 2; gfx_fill_rect(left, y, 120, 2, SCH_ACCENT); y += ch; // Facts: quiet labels down the left, bright values in a second column. int vx = left + 14 * cw; for (int i = 0; i < nfields; i++) { text_at(left, y, fields[i].label, PSOGM_DIM); text_at(vx, y, fields[i].value, PSOGM_FG); y += ch + 2; } y += ch; text_at(left, y, "Call chain", PSOGM_DIM); y += ch + 2; for (int i = 0; i < npcs && pcs[i] && y + ch <= foot; i++) { char line[48]; trace_line(line, sizeof(line), i, pcs[i]); text_at(vx - 3 * cw, y, line, PSOGM_FG); y += ch; } // Three closing lines, bottom left. The first used to be an addr2line // command, which the call chain above no longer needs -- it prints names. // // What replaces it is the thing nobody would guess: **this screen is no // longer the only copy.** The log survives into the next boot (see // include/core/klog.h), so the right instruction on a crash screen is now // "restart and read it" rather than "photograph this". // // Kept under about 45 characters each: the QR card is on the right, and the // 62-character command line this replaces used to run underneath it. text_at(left, sh - ch * 4, "This report is in /proc/kmsg after a restart.", PSOGM_DIM); // A verbose boot keeps the screen up for ever; any other powers off after // PANIC_OFF_MS (see panic_blink), and says so rather than surprising anyone. text_at(left, sh - ch * 4 + ch + 4, boot_verbose ? "schdbg, or Ctrl-G on the console, asks more." : "The machine powers itself off in 3 minutes.", PSOGM_DIM); text_at(left, sh - ch * 4 + (ch + 4) * 2, "PSOGM -- Purple Screen of Guru Meditation", PSOGM_DIM); gfx_flush_all(); } // Same report, down the serial line. This is the copy that survives: it is what // bootcheck reads, and the only one that exists when the framebuffer does not. static void report_serial(char *reason, addr_t *pcs, int npcs) { cprintf("\n"); cprintf(" :(\n\n"); cprintf(" SchOS ran into a problem and needs to restart.\n"); cprintf(" PSOGM -- Purple Screen of Guru Meditation\n\n"); for (int i = 0; i < nfields; i++) { // Pad the label out so the values line up, without a printf width // specifier -- cprintf has no such thing. int n = 0; while (fields[i].label[n]) n++; cprintf(" %s", fields[i].label); for (int pad = n; pad < 14; pad++) cprintf(" "); cprintf("%s\n", fields[i].value); } cprintf("\n Call chain\n"); for (int i = 0; i < npcs && pcs[i]; i++) { char name[64]; ksym_fmt(name, sizeof(name), (uint64)pcs[i]); cprintf(" #%d %p %s\n", i, pcs[i], name); } if (!ksym_ready()) cprintf("\n Resolve with: addr2line -e build/kernel/kernel
\n"); else cprintf("\n For a line number: addr2line -e build/kernel/kernel
\n"); // Where to go next. Worth printing even though the machine is about to stop, // because this text is also what ends up in the log -- and the log is the // thing being pointed at. cprintf(" This report is in /proc/kmsg after a restart.\n"); cprintf(" Then: schdbg whose -- who owns a page\n"); cprintf(" schdbg watch -- who writes to one\n"); cprintf(" Ctrl-G on the console -- every view, no shell needed\n"); cprintf(" Reason: %s\n", reason); cprintf(" System halted.\n"); } static void panic_blink(void) __attribute__((noreturn)); __attribute__((noreturn)) void panic(char *s) { addr_t pcs[10]; cli(); // Stop every other path from writing to the console. consputc checks this // and spins, which is what keeps another CPU from painting over the report // while it is being drawn. cons_panic_mode(); for (int i = 0; i < 10; i++) pcs[i] = 0; getcallerpcs(&s, pcs); gather(s, pcs, 10); report_serial(s, pcs, 10); if (gfx_ready()) { paint(s, pcs, 10); // And on every other display the machine has, including the ones the // kernel handed to a process. Deliberately after the console's copy: a // fault in there must not cost the screen that is already right. display_panic_present(); } panicked = 1; 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 // How long the report stays up before the machine powers itself off, on an // ordinary boot. A crashed laptop otherwise sits with its fans up -- the other // processors are still running -- until somebody holds the power button, and // three minutes is long enough to photograph the screen or scan the QR code. // A verbose boot is somebody debugging on purpose, and keeps it up for ever. #define PANIC_OFF_MS (3 * 60 * 1000) static void panic_blink(void) { int on = 0; uint elapsed = 0; for (;;) { if (!boot_verbose && elapsed >= PANIC_OFF_MS) { acpi_poweroff_now(); elapsed = 0; // still on: blink on, and try again later } 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); elapsed += 300; } }