Sixty-five headers sat side by side in include/, so `#include "irq.h"` said nothing about where irq.h was or what it belonged to, and errno.h, stat.h and fcntl.h at the top shadowed the system's for any host tool built with -I./include. They are in folders now -- arch/ core/ fs/ dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and every include names the folder, as Linux's do: "core/proc.h". Done mechanically and checked the hard way: the kernel built from this tree disassembles identically to the one built from the tree before it. The full image boots, and userland is the same set of programs. Paths in docs and comments follow; docs/guides/layout.md has a table of what is where, and tools/fix-includes.py rewrites old-style includes from the folders in the tree, for code written before this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
497 lines
14 KiB
C
497 lines
14 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
//
|
|
// /dev/dbg -- asking the kernel a question. See include/core/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/architecture/debugging.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 "core/types.h"
|
|
#include "core/defs.h"
|
|
#include "core/param.h"
|
|
#include "core/spinlock.h"
|
|
#include "core/sleeplock.h"
|
|
#include "fs/fs.h"
|
|
#include "fs/file.h"
|
|
#include "uapi/errno.h"
|
|
#include "core/sbuf.h"
|
|
#include "arch/memlayout.h"
|
|
#include "arch/mmu.h"
|
|
#include "core/dbg.h"
|
|
|
|
#define NVIEW 24
|
|
#define DBGBUF (16 * 1024)
|
|
|
|
struct view {
|
|
const char *name;
|
|
dbg_view_fn fn;
|
|
const char *what;
|
|
int rw; // changes something; see dbg_register_rw
|
|
};
|
|
|
|
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;
|
|
|
|
static void
|
|
dbg_register_at(const char *name, dbg_view_fn fn, const char *what, int rw)
|
|
{
|
|
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 : "";
|
|
views[nview].rw = rw;
|
|
nview++;
|
|
}
|
|
|
|
void
|
|
dbg_register(const char *name, dbg_view_fn fn, const char *what)
|
|
{
|
|
dbg_register_at(name, fn, what, 0);
|
|
}
|
|
|
|
void
|
|
dbg_register_rw(const char *name, dbg_view_fn fn, const char *what)
|
|
{
|
|
dbg_register_at(name, fn, what, 1);
|
|
}
|
|
|
|
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);
|
|
if (views[i].rw)
|
|
sb_puts(out, " [changes state]");
|
|
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)
|
|
{
|
|
return dbg_command_ro(line, out, 0);
|
|
}
|
|
|
|
// `ro` refuses any view that declared itself as changing state.
|
|
//
|
|
// The refusal is driven by the registration rather than by a list here, which
|
|
// is the whole reason dbg_register_rw() exists: a list of dangerous views in
|
|
// this file would go stale in the direction where a new one is treated as
|
|
// safe.
|
|
int
|
|
dbg_command_ro(const char *line, struct sbuf *out, int ro)
|
|
{
|
|
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)) {
|
|
// A view that changes state, asked for through /dev/dbgro. Reading it
|
|
// with no argument is still allowed, because that is the listing --
|
|
// `watch` on its own prints what is armed and arms nothing.
|
|
if (ro && views[i].rw && arg[0] != 0) {
|
|
sb_puts(out, "'");
|
|
sb_puts(out, word);
|
|
sb_puts(out, "' changes state, and this is /dev/dbgro.\n"
|
|
"use /dev/dbg from the console for that.\n");
|
|
return out->len;
|
|
}
|
|
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, int ro)
|
|
{
|
|
struct sbuf sb;
|
|
|
|
sb.p = dbgbuf; sb.len = 0; sb.cap = DBGBUF - 64;
|
|
if (cmd)
|
|
dbg_command_ro(cmd, &sb, ro);
|
|
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/architecture/debugging.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, 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)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, ip && ip->minor == 1);
|
|
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;
|
|
}
|