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
459 lines
13 KiB
C
459 lines
13 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// /dev/dbg -- asking the kernel a question. See include/dbg.h.
|
|
//
|
|
// The device is a conversation: write a line, read the answer.
|
|
//
|
|
// # echo dma > /dev/dbg && cat /dev/dbg
|
|
//
|
|
// That shape is chosen so the debugger is usable with the tools already on the
|
|
// machine, before `schdbg` is built and on a system where it will not start.
|
|
// docs/plan/debugger.md argues the case at length; the short version is that a
|
|
// protocol you cannot drive by hand is a protocol you cannot debug.
|
|
//
|
|
// One conversation at a time, deliberately. The answer buffer is global and
|
|
// the lock is held only while it is being filled, so two programs asking at
|
|
// once get one answer each, in some order, and neither gets half of the
|
|
// other's. Making it per-open would mean a struct file hook for something that
|
|
// has exactly one user.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "fs.h"
|
|
#include "file.h"
|
|
#include "errno.h"
|
|
#include "sbuf.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "dbg.h"
|
|
|
|
#define NVIEW 24
|
|
#define DBGBUF (16 * 1024)
|
|
|
|
struct view {
|
|
const char *name;
|
|
dbg_view_fn fn;
|
|
const char *what;
|
|
};
|
|
|
|
static struct view views[NVIEW];
|
|
static int nview;
|
|
|
|
struct claimant {
|
|
const char *name;
|
|
dbg_whose_fn fn;
|
|
};
|
|
static struct claimant claimants[NVIEW];
|
|
static int nclaim;
|
|
|
|
static int streq(const char *a, const char *b);
|
|
|
|
// Static, and 16 KB of it. A view of every process on a 64-slot table is the
|
|
// largest thing here and comes to a few kilobytes; the alternative -- kalloc
|
|
// on every read -- would put an allocation on the path of the tool you reach
|
|
// for when the allocator is what you are investigating.
|
|
static char dbgbuf[DBGBUF];
|
|
static int dbglen;
|
|
static struct spinlock dbglock;
|
|
static int dbginit;
|
|
|
|
void
|
|
dbg_register(const char *name, dbg_view_fn fn, const char *what)
|
|
{
|
|
if (nview >= NVIEW || !name || !fn)
|
|
return;
|
|
// Registering the same name twice is a bug in the caller -- it means an init
|
|
// that runs once per CPU, which is how `watch` first appeared in the list
|
|
// twice. Refusing keeps the list honest whoever makes that mistake next.
|
|
for (int i = 0; i < nview; i++)
|
|
if (streq(views[i].name, name))
|
|
return;
|
|
views[nview].name = name;
|
|
views[nview].fn = fn;
|
|
views[nview].what = what ? what : "";
|
|
nview++;
|
|
}
|
|
|
|
void
|
|
dbg_register_whose(const char *name, dbg_whose_fn fn)
|
|
{
|
|
if (nclaim >= NVIEW || !name || !fn)
|
|
return;
|
|
for (int i = 0; i < nclaim; i++)
|
|
if (streq(claimants[i].name, name))
|
|
return;
|
|
claimants[nclaim].name = name;
|
|
claimants[nclaim].fn = fn;
|
|
nclaim++;
|
|
}
|
|
|
|
static int
|
|
streq(const char *a, const char *b)
|
|
{
|
|
while (*a && *b && *a == *b) { a++; b++; }
|
|
return *a == 0 && *b == 0;
|
|
}
|
|
|
|
static void
|
|
do_help(struct sbuf *out)
|
|
{
|
|
sb_puts(out, "views this kernel publishes:\n");
|
|
for (int i = 0; i < nview; i++) {
|
|
sb_puts(out, " ");
|
|
sb_pad(out, views[i].name, 10);
|
|
sb_puts(out, views[i].what);
|
|
sb_putc(out, '\n');
|
|
}
|
|
sb_puts(out, "\n whose <addr> every subsystem's claim on one page\n");
|
|
sb_puts(out, " all every view above\n");
|
|
sb_puts(out, " help this\n");
|
|
}
|
|
|
|
// "0x3eceb000", or "3eceb000", or a decimal. Returns 0 if there was no number.
|
|
static int
|
|
parse_addr(const char *s, uint64 *out)
|
|
{
|
|
uint64 v = 0;
|
|
int any = 0, hex = 0;
|
|
|
|
while (*s == ' ')
|
|
s++;
|
|
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { hex = 1; s += 2; }
|
|
|
|
// A bare string of hex digits is taken as hex: every address in this
|
|
// kernel's own output is printed without the 0x, and retyping one and
|
|
// getting a decimal reading of it would be a trap.
|
|
if (!hex) {
|
|
const char *p = s;
|
|
while (*p) {
|
|
if ((*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F')) { hex = 1; break; }
|
|
if (!(*p >= '0' && *p <= '9')) break;
|
|
p++;
|
|
}
|
|
if (!hex) {
|
|
// All decimal digits, but an address that long is hex in practice.
|
|
int n = 0;
|
|
const char *q = s;
|
|
while (*q >= '0' && *q <= '9') { n++; q++; }
|
|
if (n > 10) hex = 1;
|
|
}
|
|
}
|
|
|
|
while (*s) {
|
|
int d;
|
|
if (*s >= '0' && *s <= '9') d = *s - '0';
|
|
else if (*s >= 'a' && *s <= 'f') d = *s - 'a' + 10;
|
|
else if (*s >= 'A' && *s <= 'F') d = *s - 'A' + 10;
|
|
else break;
|
|
if (!hex && d > 9)
|
|
break;
|
|
v = hex ? (v << 4) + d : v * 10 + d;
|
|
any = 1;
|
|
s++;
|
|
}
|
|
|
|
*out = v;
|
|
return any;
|
|
}
|
|
|
|
static void
|
|
do_whose(struct sbuf *out, const char *arg)
|
|
{
|
|
struct whose w;
|
|
uint64 addr, kva;
|
|
|
|
if (!parse_addr(arg, &addr)) {
|
|
sb_puts(out, "whose <addr> -- a physical address, or a kernel virtual one\n");
|
|
return;
|
|
}
|
|
|
|
// Which kind of address is it? Anything at or above KERNBASE is a kernel
|
|
// virtual address and is translated; anything below is taken as physical.
|
|
//
|
|
// A *user* virtual address cannot be resolved here and is not guessed at:
|
|
// it means nothing without saying which address space, and answering for
|
|
// whichever one happens to be current would be wrong in a way that looks
|
|
// right. `vma <pid>` is the view for that question.
|
|
if (addr >= (uint64)KERNBASE) {
|
|
kva = addr;
|
|
addr = addr - (uint64)KERNBASE;
|
|
} else {
|
|
kva = addr + (uint64)KERNBASE;
|
|
}
|
|
|
|
w.phys = addr & ~(uint64)(PGSIZE - 1);
|
|
w.off = addr & (PGSIZE - 1);
|
|
w.owners = 0;
|
|
w.free_owners = 0;
|
|
w.mappings = 0;
|
|
|
|
sb_puts(out, "phys 0x");
|
|
sb_puthex(out, w.phys, 16);
|
|
if (w.off) {
|
|
sb_puts(out, " + 0x");
|
|
sb_puthex(out, w.off, 3);
|
|
}
|
|
sb_puts(out, " kernel va 0x");
|
|
sb_puthex(out, (kva & ~(uint64)(PGSIZE - 1)), 16);
|
|
sb_putc(out, '\n');
|
|
|
|
if (w.phys >= (uint64)PHYSTOP)
|
|
sb_puts(out, " (above PHYSTOP -- this is not RAM this kernel manages)\n");
|
|
|
|
for (int i = 0; i < nclaim; i++)
|
|
claimants[i].fn(out, &w);
|
|
|
|
// The verdict. Two shapes of wrong are worth naming, because both are
|
|
// silent everywhere else and both corrupt memory some distance away.
|
|
sb_putc(out, '\n');
|
|
if (w.owners == 0 && w.mappings == 0) {
|
|
sb_puts(out, " nobody claims this page\n");
|
|
} else if (w.owners > 1) {
|
|
sb_puts(out, " *** ");
|
|
sb_putu(out, w.owners);
|
|
sb_puts(out, " owners -- this page is claimed more than once, and whichever\n"
|
|
" allocator hands it out second will be handing out memory\n"
|
|
" the first one is still using\n");
|
|
} else if (w.free_owners > 0 && w.mappings > 0) {
|
|
sb_puts(out, " *** its allocator believes this page is free, and ");
|
|
sb_putu(out, w.mappings);
|
|
sb_puts(out, " page table(s)\n"
|
|
" still map it -- it will be handed out from under them\n");
|
|
} else if (w.owners == 0) {
|
|
// Not a finding. kalloc keeps a free list and no record of what it has
|
|
// handed out, so a page in use by the kernel and a page genuinely lost
|
|
// look the same from here -- and saying "nobody owns this" about an
|
|
// ordinary allocated page would be the query crying wolf.
|
|
sb_puts(out, " no allocator has a record of this page, which is what an\n"
|
|
" allocated one looks like: kalloc tracks only what is free.\n");
|
|
if (w.mappings) {
|
|
sb_puts(out, " ");
|
|
sb_putu(out, w.mappings);
|
|
sb_puts(out, " page table(s) map it, so it is in use.\n");
|
|
}
|
|
} else {
|
|
sb_puts(out, " consistent: ");
|
|
sb_putu(out, w.owners);
|
|
sb_puts(out, " owner, ");
|
|
sb_putu(out, w.mappings);
|
|
sb_puts(out, " mapping(s)\n");
|
|
}
|
|
}
|
|
|
|
static void
|
|
do_all(struct sbuf *out)
|
|
{
|
|
for (int i = 0; i < nview; i++) {
|
|
sb_puts(out, "=== ");
|
|
sb_puts(out, views[i].name);
|
|
sb_puts(out, " ===\n");
|
|
views[i].fn(out, "");
|
|
sb_putc(out, '\n');
|
|
}
|
|
}
|
|
|
|
int
|
|
dbg_command(const char *line, struct sbuf *out)
|
|
{
|
|
char word[32], arg[64];
|
|
int n = 0;
|
|
|
|
while (*line == ' ' || *line == '\t')
|
|
line++;
|
|
while (*line && *line != '\n' && *line != ' ' && n < (int)sizeof(word) - 1)
|
|
word[n++] = *line++;
|
|
word[n] = 0;
|
|
|
|
while (*line == ' ' || *line == '\t')
|
|
line++;
|
|
n = 0;
|
|
while (*line && *line != '\n' && n < (int)sizeof(arg) - 1)
|
|
arg[n++] = *line++;
|
|
while (n > 0 && (arg[n-1] == ' ' || arg[n-1] == '\r'))
|
|
n--;
|
|
arg[n] = 0;
|
|
|
|
if (word[0] == 0 || streq(word, "help")) {
|
|
do_help(out);
|
|
return out->len;
|
|
}
|
|
if (streq(word, "all")) {
|
|
do_all(out);
|
|
return out->len;
|
|
}
|
|
if (streq(word, "whose")) {
|
|
do_whose(out, arg);
|
|
return out->len;
|
|
}
|
|
|
|
for (int i = 0; i < nview; i++) {
|
|
if (streq(word, views[i].name)) {
|
|
views[i].fn(out, arg);
|
|
return out->len;
|
|
}
|
|
}
|
|
|
|
sb_puts(out, "no view called '");
|
|
sb_puts(out, word);
|
|
sb_puts(out, "'. try: help\n");
|
|
return out->len;
|
|
}
|
|
|
|
// Fill dbgbuf, and say so if the answer did not fit.
|
|
//
|
|
// sb_* silently drops what will not fit, which is the right behaviour for a
|
|
// builder that must not fail -- but a table that stops in the middle of a row
|
|
// with no explanation reads as a kernel that is broken rather than as a buffer
|
|
// that is full. The last 64 bytes are held back to have somewhere to say it.
|
|
static int
|
|
fill(const char *cmd)
|
|
{
|
|
struct sbuf sb;
|
|
|
|
sb.p = dbgbuf; sb.len = 0; sb.cap = DBGBUF - 64;
|
|
if (cmd)
|
|
dbg_command(cmd, &sb);
|
|
else
|
|
do_all(&sb);
|
|
|
|
if (sb.len >= sb.cap - 1) {
|
|
sb.cap = DBGBUF;
|
|
sb_puts(&sb, "\n-- truncated: the answer is larger than the buffer. "
|
|
"ask for one view at a time.\n");
|
|
}
|
|
dbgbuf[sb.len] = 0;
|
|
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
|
|
dbg_dump_all(void)
|
|
{
|
|
if (!dbginit) { initlock(&dbglock, "dbg"); dbginit = 1; }
|
|
|
|
acquire(&dbglock);
|
|
dbglen = fill(0);
|
|
// Printed inside the lock so a second sysrq cannot interleave with this one.
|
|
// cprintf takes cons.lock, which is never held by anything that wants
|
|
// dbglock, so the order is fixed and there is no cycle.
|
|
cprintf("%s", dbgbuf);
|
|
release(&dbglock);
|
|
}
|
|
|
|
// --- the device -------------------------------------------------------------
|
|
|
|
// A command. The answer replaces whatever was there.
|
|
//
|
|
// **The answer is read from offset zero.** That matters because writing to a
|
|
// descriptor advances its offset, so a program that writes and then reads on
|
|
// the same descriptor starts reading as many bytes into the answer as its
|
|
// command was long -- which is exactly the bug this comment exists to stop
|
|
// being written again. `cat` is unaffected because it opens the file fresh;
|
|
// cmd/schdbg seeks back to zero. See lseek() there.
|
|
int
|
|
dbg_write(struct inode *ip, uint64 off, char *src, int n)
|
|
{
|
|
char line[128];
|
|
int i;
|
|
|
|
(void)ip; (void)off;
|
|
|
|
if (n <= 0)
|
|
return 0;
|
|
if (n > (int)sizeof(line) - 1)
|
|
n = (int)sizeof(line) - 1;
|
|
for (i = 0; i < n; i++)
|
|
line[i] = src[i];
|
|
line[n] = 0;
|
|
|
|
if (!dbginit) { initlock(&dbglock, "dbg"); dbginit = 1; }
|
|
|
|
acquire(&dbglock);
|
|
dbglen = fill(line);
|
|
release(&dbglock);
|
|
|
|
return n; // the whole line was consumed either way
|
|
}
|
|
|
|
int
|
|
dbg_read(struct inode *ip, uint64 off, char *dst, int n)
|
|
{
|
|
int i, avail;
|
|
|
|
(void)ip;
|
|
|
|
if (!dbginit) { initlock(&dbglock, "dbg"); dbginit = 1; }
|
|
|
|
acquire(&dbglock);
|
|
if (dbglen == 0) {
|
|
// Nothing asked yet. Answering with the help text rather than with EOF,
|
|
// because `cat /dev/dbg` on its own is what someone does first and an
|
|
// empty file teaches them nothing.
|
|
struct sbuf sb = { dbgbuf, 0, DBGBUF };
|
|
do_help(&sb);
|
|
dbglen = sb.len;
|
|
}
|
|
if (off >= (uint64)dbglen) {
|
|
release(&dbglock);
|
|
return 0; // EOF
|
|
}
|
|
avail = dbglen - (int)off;
|
|
if (n > avail)
|
|
n = avail;
|
|
for (i = 0; i < n; i++)
|
|
dst[i] = dbgbuf[off + i];
|
|
release(&dbglock);
|
|
return n;
|
|
}
|