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
416 lines
12 KiB
C
416 lines
12 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// Hardware watchpoints. docs/plan/debugger.md milestone D4.
|
|
//
|
|
// The question this answers is the one no amount of printing gets at:
|
|
// *something wrote to this address -- who?* Adding a print to every writer
|
|
// only works when you already know who the writers are, and the bug is
|
|
// invariably a writer you have not thought of. The processor will simply tell
|
|
// you, through four debug registers that have been sitting unused in this
|
|
// kernel since it was written.
|
|
//
|
|
// Two decisions worth reading:
|
|
//
|
|
// **It reports and continues.** A data watchpoint on x86 is a *trap*, not a
|
|
// fault -- it is delivered after the storing instruction has completed -- so
|
|
// returning from the handler carries on with the write already done. That is
|
|
// what makes this usable on a running system, which docs/architecture/
|
|
// debugging.md argues is the whole point: the bugs here are timing bugs, and a
|
|
// debugger that stops the world cannot see them. The cost is that you cannot
|
|
// prevent the write, only learn about it.
|
|
//
|
|
// **Every CPU watches, without an IPI.** Debug registers are per-CPU, so
|
|
// arming DR0 on the processor that ran the command would watch one quarter of
|
|
// a four-way machine and silently miss the rest. There is no general IPI in
|
|
// this kernel, so instead the table carries a generation number and each CPU
|
|
// reloads its registers from the timer interrupt when it notices the number
|
|
// has moved. Worst case a watchpoint takes one tick to cover a CPU, and the
|
|
// cost when nothing has changed is one comparison per tick.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "x86.h"
|
|
#include "proc.h"
|
|
#include "traps.h"
|
|
#include "sbuf.h"
|
|
#include "dbg.h"
|
|
|
|
#define NWATCH 4 // DR0..DR3, and there are no more
|
|
|
|
// DR7. The Intel manual's names, because nothing is gained by inventing new
|
|
// ones for a register that has had these since 1985.
|
|
#define DR7_L(n) (1u << ((n) * 2)) // local enable
|
|
#define DR7_G(n) (1u << ((n) * 2 + 1)) // global enable
|
|
#define DR7_LE (1u << 8)
|
|
#define DR7_GE (1u << 9)
|
|
#define DR7_RW_SHIFT(n) (16 + (n) * 4)
|
|
#define DR7_LEN_SHIFT(n) (18 + (n) * 4)
|
|
|
|
#define RW_WRITE 0x1 // break on data write
|
|
#define RW_RW 0x3 // break on data read or write
|
|
|
|
struct watchpoint {
|
|
uint64 addr;
|
|
int len; // 1, 2, 4 or 8
|
|
int rw; // RW_WRITE or RW_RW
|
|
int used;
|
|
uint64 hits;
|
|
};
|
|
|
|
static struct watchpoint wp[NWATCH];
|
|
static volatile uint wp_generation = 1;
|
|
static uint wp_seen[NCPU];
|
|
|
|
// Recursion guard. The handler prints, and printing touches memory; a
|
|
// watchpoint armed on something cprintf writes to would otherwise re-enter
|
|
// here for ever. Per-CPU because two processors can be in the handler at once
|
|
// and one must not silence the other.
|
|
static int wp_busy[NCPU];
|
|
|
|
// --- the registers ----------------------------------------------------------
|
|
|
|
static inline uint64
|
|
read_dr6(void)
|
|
{
|
|
uint64 v;
|
|
asm volatile("mov %%dr6, %0" : "=r"(v));
|
|
return v;
|
|
}
|
|
|
|
static inline void
|
|
write_dr6(uint64 v)
|
|
{
|
|
asm volatile("mov %0, %%dr6" : : "r"(v));
|
|
}
|
|
|
|
static inline void
|
|
write_dr7(uint64 v)
|
|
{
|
|
asm volatile("mov %0, %%dr7" : : "r"(v));
|
|
}
|
|
|
|
static void
|
|
write_dr(int n, uint64 v)
|
|
{
|
|
switch (n) {
|
|
case 0: asm volatile("mov %0, %%dr0" : : "r"(v)); break;
|
|
case 1: asm volatile("mov %0, %%dr1" : : "r"(v)); break;
|
|
case 2: asm volatile("mov %0, %%dr2" : : "r"(v)); break;
|
|
case 3: asm volatile("mov %0, %%dr3" : : "r"(v)); break;
|
|
}
|
|
}
|
|
|
|
// The LEN field is not a byte count. 1 byte is 00, 2 is 01, 8 is 10 and 4 is
|
|
// 11 -- the 8 and 4 encodings are the way round nobody expects, and getting
|
|
// them backwards produces a watchpoint that silently covers the wrong span.
|
|
static uint64
|
|
len_bits(int len)
|
|
{
|
|
switch (len) {
|
|
case 1: return 0x0;
|
|
case 2: return 0x1;
|
|
case 8: return 0x2;
|
|
default: return 0x3; // 4
|
|
}
|
|
}
|
|
|
|
// Load this CPU's debug registers from the table.
|
|
static void
|
|
wp_load(void)
|
|
{
|
|
uint64 dr7 = DR7_LE | DR7_GE;
|
|
|
|
for (int i = 0; i < NWATCH; i++) {
|
|
if (!wp[i].used) {
|
|
write_dr(i, 0);
|
|
continue;
|
|
}
|
|
write_dr(i, wp[i].addr);
|
|
// Global, not local. The local bits are cleared by a hardware task switch,
|
|
// which this kernel never does -- but "watch this everywhere" is what is
|
|
// meant, and G is the bit that says so.
|
|
dr7 |= DR7_G(i);
|
|
dr7 |= (uint64)wp[i].rw << DR7_RW_SHIFT(i);
|
|
dr7 |= len_bits(wp[i].len) << DR7_LEN_SHIFT(i);
|
|
}
|
|
|
|
write_dr6(0);
|
|
write_dr7(dr7);
|
|
}
|
|
|
|
// Called from the timer interrupt. Cheap when nothing has changed, which is
|
|
// every tick except the handful after somebody arms or clears a watchpoint.
|
|
void
|
|
watch_sync(void)
|
|
{
|
|
int c = cpunum();
|
|
|
|
if (c < 0 || c >= NCPU)
|
|
return;
|
|
if (wp_seen[c] == wp_generation)
|
|
return;
|
|
wp_seen[c] = wp_generation;
|
|
wp_load();
|
|
}
|
|
|
|
// --- the handler ------------------------------------------------------------
|
|
|
|
// #DB. Say which watchpoint fired, where the write came from, and carry on.
|
|
//
|
|
// Returns 1 if this was ours, so trap() can tell a watchpoint apart from a
|
|
// single-step or any other source of #DB and not swallow the latter.
|
|
int
|
|
watch_trap(struct trapframe *tf)
|
|
{
|
|
uint64 dr6 = read_dr6();
|
|
int c = cpunum();
|
|
int hit = 0;
|
|
|
|
if (!(dr6 & 0xF))
|
|
return 0; // a #DB, but not one of ours
|
|
|
|
write_dr6(0); // DR6 bits are sticky; nothing else clears them
|
|
|
|
if (c >= 0 && c < NCPU) {
|
|
if (wp_busy[c])
|
|
return 1; // already reporting; see the note on recursion
|
|
wp_busy[c] = 1;
|
|
}
|
|
|
|
for (int i = 0; i < NWATCH; i++) {
|
|
addr_t pcs[10];
|
|
char where[72];
|
|
|
|
if (!(dr6 & (1u << i)))
|
|
continue;
|
|
hit = 1;
|
|
wp[i].hits++;
|
|
|
|
ksym_fmt(where, sizeof(where), tf->rip);
|
|
|
|
cprintf("watch %d: 0x%p written, %d byte(s), by %s (rip 0x%p) on cpu %d",
|
|
i, wp[i].addr, wp[i].len, where, tf->rip, c);
|
|
if (myproc())
|
|
cprintf(", pid %d %s", myproc()->pid, myproc()->name);
|
|
cprintf("\n");
|
|
|
|
// The chain matters more than the rip. "memset wrote to it" is not an
|
|
// answer; "memset, called from dma_alloc, called from do_dma" is.
|
|
getstackpcs((addr_t *)tf->rbp, pcs);
|
|
for (int j = 0; j < 6 && pcs[j]; j++) {
|
|
ksym_fmt(where, sizeof(where), pcs[j]);
|
|
cprintf(" #%d %s\n", j, where);
|
|
}
|
|
}
|
|
|
|
if (c >= 0 && c < NCPU)
|
|
wp_busy[c] = 0;
|
|
return hit;
|
|
}
|
|
|
|
// --- arming and clearing ----------------------------------------------------
|
|
|
|
static int
|
|
watch_set(uint64 addr, int len, int rw)
|
|
{
|
|
int i;
|
|
|
|
if (len != 1 && len != 2 && len != 4 && len != 8)
|
|
return -1;
|
|
// The address must be naturally aligned for the length. The processor
|
|
// ignores the low bits otherwise and watches a span you did not ask for,
|
|
// which is worse than refusing.
|
|
if (addr & (uint64)(len - 1))
|
|
return -2;
|
|
|
|
for (i = 0; i < NWATCH; i++)
|
|
if (!wp[i].used)
|
|
break;
|
|
if (i == NWATCH)
|
|
return -3;
|
|
|
|
wp[i].addr = addr;
|
|
wp[i].len = len;
|
|
wp[i].rw = rw;
|
|
wp[i].hits = 0;
|
|
wp[i].used = 1;
|
|
|
|
wp_generation++;
|
|
wp_load(); // this CPU now; the rest at their next tick
|
|
return i;
|
|
}
|
|
|
|
static int
|
|
watch_clear(int i)
|
|
{
|
|
if (i < 0 || i >= NWATCH || !wp[i].used)
|
|
return -1;
|
|
wp[i].used = 0;
|
|
wp_generation++;
|
|
wp_load();
|
|
return 0;
|
|
}
|
|
|
|
// --- the command ------------------------------------------------------------
|
|
|
|
static int
|
|
parse_num(const char **sp, uint64 *out)
|
|
{
|
|
const char *s = *sp;
|
|
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; }
|
|
else {
|
|
const char *p = s;
|
|
while (*p && *p != ' ') {
|
|
if ((*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F')) { hex = 1; break; }
|
|
p++;
|
|
}
|
|
}
|
|
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++;
|
|
}
|
|
*sp = s;
|
|
*out = v;
|
|
return any;
|
|
}
|
|
|
|
static void
|
|
watch_list(struct sbuf *out)
|
|
{
|
|
int any = 0;
|
|
|
|
sb_puts(out, " # address len on hits\n");
|
|
for (int i = 0; i < NWATCH; i++) {
|
|
if (!wp[i].used)
|
|
continue;
|
|
any = 1;
|
|
sb_putu_pad(out, i, 3);
|
|
sb_puts(out, " 0x");
|
|
sb_puthex(out, wp[i].addr, 16);
|
|
sb_putu_pad(out, wp[i].len, 5);
|
|
sb_puts(out, wp[i].rw == RW_RW ? " rw " : " w ");
|
|
sb_putu_pad(out, wp[i].hits, 6);
|
|
sb_putc(out, '\n');
|
|
}
|
|
if (!any)
|
|
sb_puts(out, " (none armed)\n");
|
|
|
|
// Which processors have actually loaded the table.
|
|
//
|
|
// Reported rather than assumed, because the answer is the difference
|
|
// between "this watchpoint is watching" and "this watchpoint is watching
|
|
// one quarter of the machine". A CPU that is behind catches up at its next
|
|
// timer tick; one that stays behind is a bug and this is where it shows.
|
|
sb_puts(out, "\n table generation ");
|
|
sb_putu(out, wp_generation);
|
|
sb_puts(out, ", loaded by:");
|
|
for (int c = 0; c < ncpu && c < NCPU; c++) {
|
|
sb_puts(out, " cpu");
|
|
sb_putu(out, c);
|
|
sb_putc(out, '=');
|
|
sb_putu(out, wp_seen[c]);
|
|
if (wp_seen[c] != wp_generation)
|
|
sb_puts(out, "(behind)");
|
|
}
|
|
sb_putc(out, '\n');
|
|
|
|
sb_puts(out, "\n watch <addr> [len] [w|rw] arm one; len is 1, 2, 4 or 8\n"
|
|
" watch clear <n> disarm one\n"
|
|
" watch clear all disarm every one\n"
|
|
"\n Addresses are kernel virtual. `whose` prints the kernel va of a\n"
|
|
" physical page, which is how you get from one to the other.\n"
|
|
" Hits are reported to the console, not here -- they happen while\n"
|
|
" you are not looking, which is the point.\n");
|
|
}
|
|
|
|
static void
|
|
watch_view(struct sbuf *out, const char *arg)
|
|
{
|
|
const char *p = arg;
|
|
uint64 addr, len = 8;
|
|
int rw = RW_WRITE, r;
|
|
|
|
while (*p == ' ') p++;
|
|
|
|
if (*p == 0) {
|
|
watch_list(out);
|
|
return;
|
|
}
|
|
|
|
if (p[0] == 'c' && p[1] == 'l') { // clear
|
|
while (*p && *p != ' ') p++;
|
|
while (*p == ' ') p++;
|
|
if (p[0] == 'a') { // all
|
|
for (int i = 0; i < NWATCH; i++)
|
|
if (wp[i].used)
|
|
watch_clear(i);
|
|
sb_puts(out, "all watchpoints disarmed\n");
|
|
return;
|
|
}
|
|
if (!parse_num(&p, &addr)) {
|
|
sb_puts(out, "watch clear <n>, or watch clear all\n");
|
|
return;
|
|
}
|
|
if (watch_clear((int)addr) < 0)
|
|
sb_puts(out, "no such watchpoint\n");
|
|
else {
|
|
sb_puts(out, "watch ");
|
|
sb_putu(out, addr);
|
|
sb_puts(out, " disarmed\n");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!parse_num(&p, &addr)) {
|
|
watch_list(out);
|
|
return;
|
|
}
|
|
parse_num(&p, &len); // optional
|
|
while (*p == ' ') p++;
|
|
if (p[0] == 'r' && p[1] == 'w')
|
|
rw = RW_RW;
|
|
|
|
// A watchpoint on a user address would fire in whichever address space
|
|
// happens to have that linear address mapped, which is every process. Say so
|
|
// rather than arming something that reports nonsense.
|
|
if (addr < (uint64)KERNBASE)
|
|
sb_puts(out, "note: that is below KERNBASE, so it is a *user* linear address\n"
|
|
" and will fire in whatever process is current. Kernel\n"
|
|
" addresses are what this is for.\n");
|
|
|
|
r = watch_set(addr, (int)len, rw);
|
|
if (r == -1) sb_puts(out, "length must be 1, 2, 4 or 8\n");
|
|
else if (r == -2) sb_puts(out, "address must be aligned to its length\n");
|
|
else if (r == -3) sb_puts(out, "all four debug registers are in use\n");
|
|
else {
|
|
sb_puts(out, "watch ");
|
|
sb_putu(out, r);
|
|
sb_puts(out, " armed on 0x");
|
|
sb_puthex(out, addr, 16);
|
|
sb_puts(out, ", ");
|
|
sb_putu(out, len);
|
|
sb_puts(out, rw == RW_RW ? " byte(s), read or write\n" : " byte(s), on write\n");
|
|
sb_puts(out, "hits are reported to the console\n");
|
|
}
|
|
}
|
|
|
|
void
|
|
watch_init(void)
|
|
{
|
|
dbg_register("watch", watch_view,
|
|
"hardware watchpoints -- `watch <addr>` to arm one");
|
|
}
|