SchOS/kernel/dev/dbg.c
Nexoniarz 415df0fd4d 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
2026-09-14 01:29:30 +02:00

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;
}