docs/plan/debugger.md D3 and D4, which finish the plan apart from the
network transport.
D3 -- `whose <addr>`. The views each speak for one subsystem, which is what
keeps them cheap and current, and it is exactly why none of them can see
the bug that matters: a page on the DMA arena's free list looks fine from
dma.c, the same page on kalloc's free list looks fine from kalloc.c, and
that it is on both is a fact no single subsystem holds.
# schdbg whose 0x3e239000
phys 0x000000003e239000 kernel va 0xffff80003e239000
dma arena, handed out
pcidma slot 2, 0 KB into a 4000 KB block given to pid 4 (vgpud)
mapped pid 4 (vgpud) at 0x000000004000d000 rwu vma[5] no_free
consistent: 1 owner, 1 mapping(s)
Claimants register the way views do -- one line in the file that owns the
data -- so the kernel image, the log region, the arena, the page allocator,
the pciuser DMA blocks and every page table each answer for themselves.
`mapped` is the one no allocator can answer: there is no reverse map, so
vm_walk_user() goes forward over every mapped page of every live process.
Two verdicts are shouted and everything else is reported as fact. More than
one owner means the second allocator to hand the page out is handing out
memory the first is still using -- the H2 bug, which would have printed as
three lines. Free-but-still-mapped means a page will be given away from
under the page tables that point at it. Deliberately *not* a finding: no
owner at all, because kalloc keeps a free list and no record of what it has
handed out, so an ordinary allocated page has nobody to speak for it and
saying otherwise would be the query crying wolf.
D4 -- watchpoints, on the four debug registers that had been sitting unused
since this kernel was written. Armed against the same framebuffer page and
then killing the driver:
watch 0: 0xffff80003e239000 written, 8 byte(s), by stosq+0x1f, pid 12 vgpud
#0 memset+0xba
#1 dma_alloc+0x2e2
#2 do_dma+0x22a
#3 pciuser_ioctl+0x13b
#4 sys_ioctl+0x3c7
#5 syscall+0xf6
The chain is the answer, not the rip. It reports and continues -- a data
watchpoint is a trap, delivered after the store completes, so returning
carries on with the program none the wiser, which is what lets this be used
on a machine that is still running.
Debug registers are per-CPU and there is no general IPI here, so the table
carries a generation number and each CPU reloads from its timer interrupt
when it notices the number has moved: one comparison per tick, one tick of
latency at worst. `watch` prints which CPUs have caught up, because "this
watchpoint is watching" and "this watchpoint is watching one CPU" must not
look the same.
Also fixed while testing: watch_init() was called from mpmain(), which runs
on every processor, so the command appeared in the list once per CPU.
dbg_register() now refuses a duplicate name outright, so the next init that
runs per-CPU fails loudly in the list rather than quietly doubling it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
409 lines
11 KiB
C
409 lines
11 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;
|
|
}
|
|
|
|
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;
|
|
}
|