SchOS/kernel/arch/x86_64/watch.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

416 lines
12 KiB
C

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