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>
416 lines
12 KiB
C
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");
|
|
}
|