SchOS/kernel/dev/dbg.c
Nexoniarz 26bca020b6 licence: SchOS is GPL-2.0-or-later, no longer Apache-2.0
Nexoniarz is the sole author and SchOS was never published under
Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text,
every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE,
README.md, docs/licensing.md and the site say so. projects/schfs-linux
follows the rest of the tree.

The GPL gives copyleft: a fork has to ship its source. It also lifts the
old "Linux drivers, never" rule, since GPL-2.0-only code combines with
GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still
has to pick one version, so docs/licensing.md now keeps Apache-2.0 and
GPLv3 code out of the kernel to keep Linux code possible there. BSD
4-clause code goes from "avoid" to "no", because the advertising clause
is GPL-incompatible.

LDLS still ships no .ko. That boundary is now project policy rather than
a legal requirement, and ldls.md, devices.md and licensing.md section 6
say so. refmod declares license=GPL, and ldls-check passes (27 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:48:45 +02:00

497 lines
14 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// /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/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 "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;
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;
}