schdbgd bridges TCP to the debugger, which is the one way in that does not
need you to be sitting at the machine. It is a bridge and nothing else: a line
in becomes a line written to /dev/dbgro and the answer comes back, so `nc` is a
complete client and there is no clever client to be broken on the day you need
this.
# schdbgd &
schdbgd: debugger on port 4444, loopback only
$ nc 127.0.0.1 4444
dma
arena 0xffff80003da41000-0xffff80003ea41000 16384 KB total, ...
A kernel debugger plus a TCP socket is a remote root hole, so three things were
decided rather than discovered:
- It does not start by itself. schdbgd.job ships Disabled. SchOS has no
kernel command line, so the boot flag the plan asked for is the job
manifest, which is the same thing in this system's own idiom.
- It binds loopback; -a names an address and says so in a paragraph.
- It cannot reach a view that changes state, enforced in the kernel.
That last one is /dev/dbgro: /dev/dbg on minor 1. The minor is the one piece of
per-open state a character device already sees, so which node you opened is the
permission -- nothing to latch, nothing to remember between the write and the
read, no way across. Which views are refused comes from dbg_register_rw() on
the registration itself, because a central list of dangerous views goes stale
in the direction where a new view is quietly treated as safe. Today that is
`watch <addr>`; `watch` with no argument is still the listing and still
answered.
Also: socket() now accepts IPPROTO_TCP with SOCK_STREAM and IPPROTO_UDP with
SOCK_DGRAM. They name the default rather than departing from it, and toybox's
nc writes them out -- so TCP was "Protocol not supported" on a system whose
whole network is TCP.
docs/plan/debugger.md retires into docs/architecture/debugging.md; 16 files
repointed. Every plan in docs/plan/ that was open is now closed, so the next
thing is Gate 0 itself: the survey, with the instrument built.
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/architecture/debugging.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_rw("watch", watch_view,
|
|
"hardware watchpoints -- `watch <addr>` to arm one");
|
|
}
|