debugger: reachable from the console, and named on the crash screen

Two things, both about the debugger being available at the moment you
actually need it rather than once a shell exists.

**Ctrl-G on the console runs every view.** No filesystem, no /dev, no
shell -- it is the last thing main() sets up before entering the scheduler,
so it answers from early in boot, which is exactly when boot problems
happen. It sits beside the older Ctrl-P, and the difference between them is
the interesting part: procdump() takes no lock at all, deliberately,
because a lock is a way to hang while reporting a hang. The views cannot
follow that rule -- a free list read torn is worse than not read -- so they
take locks, and a lock must never be taken from an interrupt handler that
may have interrupted its holder. The key therefore *requests* and a kernel
thread *performs*. A scheduler too wedged to run a thread will not answer
Ctrl-G; Ctrl-P still will, and that division is the honest one.

**PSOGM says where the report went.** Its closing line used to be an
addr2line command, which the call chain no longer needs -- it prints names
now. What replaced it is the thing nobody would guess:

    This report is in /proc/kmsg after a restart.
    schdbg, or Ctrl-G on the console, asks more.
    PSOGM  --  Purple Screen of Guru Meditation

"Restart and read it" beats "photograph this screen" by a wide margin, and
until the log outlived the kernel it was not true. Both panic.c and
graphics.md had claimed in writing that the screen was the only artefact of
a crash; neither says so now. The QR code stays, because it is still the
only channel that works when the machine will not come back at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-14 01:29:30 +02:00
commit 415df0fd4d
7 changed files with 142 additions and 16 deletions

View file

@ -200,10 +200,24 @@ fine.
| | Works when | | | Works when |
|---|---| |---|---|
| `sysrq g` | always — every view to the console, no process, no filesystem, no client program | | **Ctrl-G** on the console | there is a scheduler — so from early in boot, with nothing mounted and nothing in userspace started |
| `/dev/dbg` | there is a scheduler and a filesystem | | `sysrq g` | `/proc` is mounted |
| `/dev/dbg` | `/dev` is mounted |
| `schdbg` | there is a shell | | `schdbg` | there is a shell |
**Ctrl-G is the one that matters for boot problems**, because it is the only
one that does not need a filesystem. It sits beside the older Ctrl-P, which
dumps the process table — and the difference between them is worth knowing.
`procdump()` takes **no lock at all**, deliberately: a lock is a way to hang
while reporting a hang. The views cannot follow that rule, because a free list
or a socket table read torn is worse than not read. So they take locks, and a
lock must never be taken from an interrupt handler that may have interrupted
its holder.
The key therefore *requests* and a kernel thread *performs*. The cost is stated
plainly: a scheduler too wedged to run a thread will not answer Ctrl-G. Ctrl-P
still will, and that division of labour is the honest one.
A watchpoint's *hits* go to the console rather than to any of these, because A watchpoint's *hits* go to the console rather than to any of these, because
they happen while you are not looking, which is the point of having them. they happen while you are not looking, which is the point of having them.
@ -342,6 +356,10 @@ something that reports nonsense.
watchpoint on something `cprintf` itself writes is silently dropped rather watchpoint on something `cprintf` itself writes is silently dropped rather
than looping; the handler guards against re-entering itself per CPU. than looping; the handler guards against re-entering itself per CPU.
- **A watchpoint takes up to one timer tick** to reach the other CPUs. - **A watchpoint takes up to one timer tick** to reach the other CPUs.
- **Ctrl-G needs a working scheduler**, per the note above. Before
`dbg_thread_start()` — which is the last thing `main()` does before entering
the scheduler — nothing answers but the log, and the log is enough because it
is read on the *next* boot.
- **No network transport.** The plan puts a TCP bridge in a userspace daemon, - **No network transport.** The plan puts a TCP bridge in a userspace daemon,
deliberately outside the kernel; it does not exist, so the debugger is local deliberately outside the kernel; it does not exist, so the debugger is local
or serial only. or serial only.

View file

@ -179,17 +179,36 @@ you it stopped — which is both truer and much less alarming.
face that is obviously a cartoon, then the facts. Whoever is reading this is face that is obviously a cartoon, then the facts. Whoever is reading this is
already having a bad time. already having a bad time.
**It says considerably more than it needs to.** The screen is the only artefact **It says considerably more than it needs to.** Everything cheap to collect is
of a panic — no core dump, no log to go back to — so everything cheap to collect collected: stop code, fault address, which CPU, which process, how far into the
is collected: stop code, fault address, which CPU, which process, how far into boot, where the kernel is loaded, and a call chain — and since
the boot, where the kernel is loaded, and a call chain. Anything left out is [debugging.md](debugging.md)'s symbol table, the call chain is *names*:
gone. All of it goes to the serial line in the same order, which is what `panic+0x5f`, `proc_sysrq_cmd+0xb3`, `sys_writev+0x12b`. All of it goes to the
`bootcheck` reads and what you get when the crash happens before there is a serial line in the same order, which is what `bootcheck` reads and what you get
framebuffer at all. when the crash happens before there is a framebuffer at all.
Once painted, `panicked` is set and `consputc` spins rather than writing, so Once painted, `panicked` is set and `consputc` spins rather than writing, so
nothing can scribble over the report. nothing can scribble over the report.
### It is no longer the only copy
This screen used to be the only artefact of a panic — this file said so, and so
did `panic.c`'s own header. That stopped being true when the kernel log started
outliving the kernel, and the closing lines changed to match:
```
This report is in /proc/kmsg after a restart.
schdbg, or Ctrl-G on the console, asks more.
PSOGM -- Purple Screen of Guru Meditation
```
The first line replaced an `addr2line` command, which the call chain no longer
needs. It is the more useful instruction by a wide margin, and it is the one
nobody would guess: **restart and read it** beats photographing the screen.
The QR code below is still drawn, and still earns its place — it is the only
channel that works when the machine will not come back at all.
### The QR code ### The QR code
`kernel/core/qr.c`. **A panic report is worth nothing if it cannot leave a `kernel/core/qr.c`. **A panic report is worth nothing if it cannot leave a

View file

@ -373,6 +373,8 @@ int watch_trap(struct trapframe *tf);
// dbg.c -- /dev/dbg, the introspection registry. See include/dbg.h. // dbg.c -- /dev/dbg, the introspection registry. See include/dbg.h.
int dbg_read(struct inode*, uint64, char*, int); int dbg_read(struct inode*, uint64, char*, int);
void dbg_request_dump(void);
void dbg_thread_start(void);
int dbg_write(struct inode*, uint64, char*, int); int dbg_write(struct inode*, uint64, char*, int);
// klog.c -- the kernel log that outlives the kernel. See include/klog.h. // klog.c -- the kernel log that outlives the kernel. See include/klog.h.

View file

@ -2,6 +2,7 @@
#include "types.h" #include "types.h"
#include "defs.h" #include "defs.h"
#include "klog.h" #include "klog.h"
#include "dbg.h"
#include "vt.h" #include "vt.h"
#include "kbd.h" /* KEY_VT_SWITCH -- the terminal-switch chords */ #include "kbd.h" /* KEY_VT_SWITCH -- the terminal-switch chords */
#include "gfx.h" #include "gfx.h"
@ -462,6 +463,19 @@ consoleintr(int (*getc)(void))
continue; continue;
} }
// Ctrl-G: every debug view, on the console.
//
// The same thing `sysrq g` and `schdbg all` produce, reachable with no
// filesystem mounted and no shell running -- which is the point. It only
// asks; a kernel thread does the work, because the views take locks and
// this is an interrupt handler. See dbg_request_dump().
if (c == C('G')) {
release(&input_lock);
dbg_request_dump();
acquire(&input_lock);
continue;
}
if (v->tio.c_lflag & ICANON) { if (v->tio.c_lflag & ICANON) {
if (c == v->tio.c_cc[VERASE] || c == C('H')) { if (c == v->tio.c_cc[VERASE] || c == C('H')) {
erase_one(v); erase_one(v);

View file

@ -217,6 +217,12 @@ kmain(uint32 fb_addr)
// until mpmain() below enters the scheduler. // until mpmain() below enters the scheduler.
netinit(); netinit();
if (boot_verbose) cprintf("verbose boot: netinit done\n"); if (boot_verbose) cprintf("verbose boot: netinit done\n");
// The debugger's own thread, for the same reason netinit's comes after
// sinit: kthread_create needs a usable process table. From here on Ctrl-G
// on the console answers, with no filesystem mounted and nothing in
// userspace started -- which is the whole point of putting it this early.
dbg_thread_start();
if (boot_verbose) cprintf("verbose boot: dbg thread started\n");
mpmain(); mpmain();
} }

View file

@ -497,16 +497,25 @@ paint(char *reason, addr_t *pcs, int npcs)
trace_line(line, sizeof(line), i, pcs[i]); trace_line(line, sizeof(line), i, pcs[i]);
gfx_text_at(vx - 3 * cw, y, line, PSOGM_FG); gfx_text_at(vx - 3 * cw, y, line, PSOGM_FG);
y += ch; y += ch;
if (y > sh - ch * 5) if (y > sh - ch * 6)
break; break;
} }
// Two closing lines, bottom left: what to do with the numbers above, and // Three closing lines, bottom left. The first used to be an addr2line
// what this screen is called, so it can be searched for by name. Shortened // command, which the call chain above no longer needs -- it prints names.
// from the full command line, which at 62 characters ran under the QR card. //
gfx_text_at(left, sh - ch * 3, // What replaces it is the thing nobody would guess: **this screen is no
"Resolve: addr2line -e kernel <address>", PSOGM_DIM); // longer the only copy.** The log survives into the next boot (see
gfx_text_at(left, sh - ch * 3 + ch + 4, // include/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.
gfx_text_at(left, sh - ch * 4,
"This report is in /proc/kmsg after a restart.", PSOGM_DIM);
gfx_text_at(left, sh - ch * 4 + ch + 4,
"schdbg, or Ctrl-G on the console, asks more.", PSOGM_DIM);
gfx_text_at(left, sh - ch * 4 + (ch + 4) * 2,
"PSOGM -- Purple Screen of Guru Meditation", PSOGM_DIM); "PSOGM -- Purple Screen of Guru Meditation", PSOGM_DIM);
} }
@ -542,6 +551,14 @@ report_serial(char *reason, addr_t *pcs, int npcs)
cprintf("\n Resolve with: addr2line -e build/kernel/kernel <address>\n"); cprintf("\n Resolve with: addr2line -e build/kernel/kernel <address>\n");
else else
cprintf("\n For a line number: addr2line -e build/kernel/kernel <address>\n"); cprintf("\n For a line number: addr2line -e build/kernel/kernel <address>\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 <addr> -- who owns a page\n");
cprintf(" schdbg watch <addr> -- who writes to one\n");
cprintf(" Ctrl-G on the console -- every view, no shell needed\n");
cprintf(" Reason: %s\n", reason); cprintf(" Reason: %s\n", reason);
cprintf(" System halted.\n"); cprintf(" System halted.\n");
} }

View file

@ -328,6 +328,56 @@ fill(const char *cmd)
return sb.len; return sb.len;
} }
// --- the console key --------------------------------------------------------
//
// Ctrl-G runs the debugger with no filesystem, no shell and no client program,
// which is what makes it available *during boot* rather than once userspace is
// up. docs/plan/debugger.md calls this level 0, and it is the level that
// matters when the thing you are debugging is the boot.
//
// It cannot do the work in the interrupt handler, though, and the reason is
// worth stating because the neighbouring Ctrl-P does exactly that. procdump()
// reads the process table with no lock held, deliberately: a lock is a way to
// hang while reporting a hang. The views cannot follow that rule -- they read
// free lists and socket tables that are meaningless torn -- so they take locks,
// and taking a lock from an interrupt handler deadlocks against whatever was
// interrupted while holding it.
//
// So the key *requests* and a kernel thread *performs*. The cost is that a
// scheduler too wedged to run a thread will not answer; Ctrl-P still does, and
// that division is the honest one.
static int dbg_wanted;
void
dbg_request_dump(void)
{
dbg_wanted = 1;
wakeup(&dbg_wanted);
}
static void
dbg_thread(void)
{
for (;;) {
acquire(&dbglock);
while (!dbg_wanted)
sleep(&dbg_wanted, &dbglock);
dbg_wanted = 0;
release(&dbglock);
cprintf("\n--- debug views, requested from the console ---\n");
dbg_dump_all();
}
}
void
dbg_thread_start(void)
{
if (!dbginit) { initlock(&dbglock, "dbg"); dbginit = 1; }
if (kthread_create(dbg_thread, "dbgd") < 0)
cprintf("dbg: no kernel thread; Ctrl-G will not work\n");
}
void void
dbg_dump_all(void) dbg_dump_all(void)
{ {