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:
parent
113595f80b
commit
415df0fd4d
7 changed files with 142 additions and 16 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue