diff --git a/Makefile b/Makefile index 8a9d20f..9045f89 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ MAKEFLAGS += -j$(NPROC) # Kernel object files — organized by subsystem KERN_ARCH = $(addprefix $(BUILD)/kernel/arch/x86_64/, \ acpi.o e820.o entry.o trapasm.o swtch.o \ - trap.o vm.o lapic.o ioapic.o mp.o) + trap.o vm.o lapic.o ioapic.o mp.o watch.o) KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \ main.o kalloc.o dma.o spinlock.o sleeplock.o string.o console.o \ diff --git a/docs/architecture/debugging.md b/docs/architecture/debugging.md index bc99e8e..6bac47f 100644 --- a/docs/architecture/debugging.md +++ b/docs/architecture/debugging.md @@ -13,7 +13,10 @@ asked anything. > **The kernel had no way to answer a question it was not already compiled to > answer.** -Three things close that, and none of them is a debugger in the stepping sense. +Five things close that, and none of them is a debugger in the stepping sense: +names for addresses, a log that survives, a way to ask each subsystem what it +knows, a way to ask all of them about one page, and the processor's own +watchpoints. --- @@ -179,6 +182,7 @@ years it holds three self-tests and no state at all. | `claims` | devices, DMA blocks and interrupts held by userspace drivers | | `pci` | the bus, with each device's command register decoded | | `sock` | the socket table, with buffered bytes and descriptors in flight | +| `watch` | the hardware watchpoints, and which CPUs have loaded them | `all` prints every view; `help` lists them; a view may take an argument, so `vma 4` narrows to one process. @@ -200,21 +204,144 @@ fine. | `/dev/dbg` | there is a scheduler and a filesystem | | `schdbg` | there is a shell | +A watchpoint's *hits* go to the console rather than to any of these, because +they happen while you are not looking, which is the point of having them. + Views run in ordinary process context and may take locks. **They must not be called from `panic()`.** --- +## Who owns this page + +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`. That it is on *both* is a fact +no single subsystem holds, and it is the bug that cost this project a day. + +`whose` asks every subsystem about one page and counts the answers: + +``` +# 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) +``` + +That is the complete life of one page in three lines: the arena gave it out, +`/dev/pci` handed it to a driver, and the driver has it mapped under a VMA +marked `no_free` so `exit()` will not free it twice. + +The claimants are registered exactly as views are, for the same reason — adding +one is a line in the file that owns the data: + +| | | +|---|---| +| `kernel` | inside the kernel image, below `end` | +| `klog` | inside the log region | +| `dma` | the arena owns it, and whether the arena thinks it is free | +| `mem` | on `kalloc`'s free list, and where | +| `pcidma` | inside a block handed to a userspace driver, and which | +| `mapped` | every page table that maps it, at what address, under which VMA | + +`mapped` is the one no allocator can answer. There is no reverse map in this +kernel, so it walks every mapped page of every live process — a few thousand +iterations, which is nothing for a command somebody typed and would be +unacceptable anywhere else. + +### The two verdicts that matter + +**More than one owner** means whichever allocator hands the page out second +will be handing out memory the first one is still using. That is the H2 bug, +and it would have printed as three claimant lines instead of taking a day. + +**Free, but still mapped** means an allocator believes a page is spare while +page tables still point at it — it will be given away from under them. + +Anything else is reported as fact, not verdict. In particular *no owner* is +normal: `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 "nobody owns +this" about one would be the query crying wolf. + +An address at or above `KERNBASE` is taken as a kernel virtual address and +translated; anything below is taken as physical. A *user* virtual address is +refused rather than guessed at — it means nothing without saying which address +space, and answering for whichever one happened to be current would be wrong in +a way that looks right. + +--- + +## Watching an address + +The question no amount of printing gets at: *something wrote here — 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. Four debug registers had been sitting unused +in this kernel since it was written. + +``` +# schdbg watch 0xffff80003e239000 8 w +watch 0 armed on 0xffff80003e239000, 8 byte(s), on write + +watch 0: 0xffff80003e239000 written, 8 byte(s), by stosq+0x1f on cpu 0, 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: "`memset` wrote to it" is not useful; +"`memset`, from `dma_alloc`, from `do_dma`" is. + +**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 program none the wiser. That is what makes +it usable on a machine that is still running, which is the whole stance of this +document. The cost is that you cannot prevent the write, only learn about it. + +**Every CPU watches, and there is no 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. The table carries a generation +number instead, and each CPU reloads from its timer interrupt when it notices +the number has moved: one comparison per tick, and at worst one tick of latency. +`watch` reports which CPUs have caught up, because "this watchpoint is watching" +and "this watchpoint is watching one CPU" must not look the same: + +``` + table generation 2, loaded by: cpu0=2 cpu1=2 +``` + +Addresses must be naturally aligned for their length — the processor quietly +ignores the low bits otherwise and watches a span you did not ask for, so it is +refused instead. A watchpoint below `KERNBASE` is a *user* linear address and +will fire in whatever process is current; `watch` says so rather than arming +something that reports nonsense. + +--- + ## What it does not do - **It does not step, break, or halt.** If a bug genuinely needs a stepper, this is the wrong tool, and that is a deliberate trade rather than an omission. -- **No cross-subsystem queries.** *"Who owns physical page `0x3eceb000`?"* — the - question that would have ended the investigation that prompted all this — still - cannot be asked. Each view answers for its own subsystem and nothing - correlates them. That is `whose`, and it is not built. -- **No watchpoints.** DR0–DR3 are untouched and `T_DEBUG` maps to `SIGTRAP`, so - "something zeroed my page table, tell me who" has no answer. +- **`whose` cannot take a user virtual address**, by refusal rather than + oversight. `vma ` is the view for that question. +- **`whose` is a snapshot with no lock held across it.** Each claimant takes its + own subsystem's lock and releases it, so a page that changes hands while the + query runs can be reported inconsistently. Taking every allocator's lock at + once to fix that would deadlock against the allocators themselves. +- **Four watchpoints, and no more.** That is the hardware. There is no software + watchpoint fallback and there should not be one. +- **Watchpoints cannot stop a write**, only report it — see above. And a + watchpoint on something `cprintf` itself writes is silently dropped rather + than looping; the handler guards against re-entering itself per CPU. +- **A watchpoint takes up to one timer tick** to reach the other CPUs. - **No network transport.** The plan puts a TCP bridge in a userspace daemon, deliberately outside the kernel; it does not exist, so the debugger is local or serial only. @@ -234,4 +361,5 @@ called from `panic()`.** happened in the two hundred events before this" is not a question this can answer. -What remains is in [../plan/debugger.md](../plan/debugger.md). +What remains — a network transport, kept deliberately outside the kernel — is +in [../plan/debugger.md](../plan/debugger.md). diff --git a/docs/plan/1.0.md b/docs/plan/1.0.md index a711c2d..77ae9b8 100644 --- a/docs/plan/1.0.md +++ b/docs/plan/1.0.md @@ -96,7 +96,7 @@ The galling ones. The work exists, is tested, and nothing uses it. | **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** | | **`hybrid.md`** | All four mechanisms built. H1 and H2 done — the driver can be killed and comes back, which is the milestone the plan calls the one that proves the whole idea. H3 (a modeset API) and H4 (USB) open. | | **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. | -| **`debugger.md`** | D0, D1 and D2 built and described in [../architecture/debugging.md](../architecture/debugging.md). D3 (`whose `, the cross-subsystem ownership query) and D4 (hardware watchpoints) open, and the network transport with them. | +| **`debugger.md`** | D0 through D4 built and described in [../architecture/debugging.md](../architecture/debugging.md). Only the network transport is open — a userspace daemon bridging TCP to `/dev/dbg`, which is deliberately last. | **The row the debugger spent.** Adding one requires closing one, and *A released PCI device* closed when H2 landed. It is worth saying plainly that the debugger diff --git a/docs/plan/README.md b/docs/plan/README.md index cfe5323..1c18b32 100644 --- a/docs/plan/README.md +++ b/docs/plan/README.md @@ -43,7 +43,7 @@ And one that is a tool rather than a capability: | | | |---|---| -| **[debugger.md](debugger.md)** | A debugger for this kernel — one that observes rather than halts, and answers in this kernel's own vocabulary. **Mostly built**: symbols, a log that outlives the kernel, and `/dev/dbg` are done and described in [../architecture/debugging.md](../architecture/debugging.md). | +| **[debugger.md](debugger.md)** | A debugger for this kernel — one that observes rather than halts, and answers in this kernel's own vocabulary. **Built**, except the network transport: symbols, a log that outlives the kernel, `/dev/dbg`, the `whose` ownership query and hardware watchpoints are all described in [../architecture/debugging.md](../architecture/debugging.md). | Two longer-horizon documents sit behind these: [schapi.md](schapi.md), the API layering and the compatibility promise, and [roadmap.md](roadmap.md), which holds diff --git a/docs/plan/debugger.md b/docs/plan/debugger.md index 983e554..1f9b3b9 100644 --- a/docs/plan/debugger.md +++ b/docs/plan/debugger.md @@ -1,9 +1,12 @@ # A debugger for this kernel -> **D0, D1 and D2 are built.** Symbols, a log that outlives the kernel, and the -> introspection registry behind `/dev/dbg` are described in -> [../architecture/debugging.md](../architecture/debugging.md). What is left -> here is D3, D4, and the network transport. +> **Every milestone on this plan is built.** Symbols, a log that outlives the +> kernel, the introspection registry behind `/dev/dbg`, the `whose` ownership +> query and hardware watchpoints are all described in +> [../architecture/debugging.md](../architecture/debugging.md). +> +> What is left here is **level 2, the network transport** — a userspace daemon +> bridging TCP to `/dev/dbg`, deliberately outside the kernel. Nothing else. Every bug in this project so far has been found the same way: add a `cprintf`, rebuild, boot, read the serial log, guess again. That loop is about ninety @@ -151,8 +154,9 @@ happens the kernel does not change. | **D0** | **Symbols.** A compact sorted table emitted from `nm` at link time and linked into the image; `ksym_lookup(addr, &off)`. | **done** | | **D1** | **A log that survives.** A ring behind `cprintf`, timestamped, in a region the allocator never sees — so a panic's last words are readable on the next boot. `/proc/kmsg`. | **done** | | **D2** | **The introspection registry.** `dbg_register("dma", dma_dump)`. Seven views, `/dev/dbg`, `schdbg`, and `sysrq g`. | **done** | -| **D3** | **`whose `** — the cross-subsystem ownership query above. | | -| **D4** | **Watchpoints**, DR0–DR3, break-on-write, reporting rip and call chain and **continuing** rather than stopping. | | +| **D3** | **`whose `** — the cross-subsystem ownership query above. | **done** | +| **D4** | **Watchpoints**, DR0–DR3, break-on-write, reporting rip and call chain and **continuing** rather than stopping. | **done** | +| **D5** | **The network transport**, level 2 above: a userspace daemon bridging TCP to `/dev/dbg`. Off unless a boot flag turns it on, loopback by default. | | ### Why D0 and D1 come before the interesting parts diff --git a/include/dbg.h b/include/dbg.h index 29143d8..bfcf767 100644 --- a/include/dbg.h +++ b/include/dbg.h @@ -37,6 +37,33 @@ typedef void (*dbg_view_fn)(struct sbuf *out, const char *arg); // and needs no initialisation of its own. void dbg_register(const char *name, dbg_view_fn fn, const char *what); +// --- whose ----------------------------------------------------------- +// +// The query the views cannot answer between them: *who owns this page?* +// +// Each view speaks 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. That it is on both is a fact no +// single subsystem holds, and it is the bug that cost this project a day. +// +// So `whose` asks every subsystem about one page and counts the answers. The +// contributors are registered the same way views are, for the same reason: +// adding one is a line in the file that owns the data. + +struct whose { + uint64 phys; // the page being asked about, page-aligned + uint64 off; // how far into it the original address was + int owners; // allocators and reserved regions claiming it + int free_owners; // of those, how many believe it is unused + int mappings; // user page tables mapping it +}; + +typedef void (*dbg_whose_fn)(struct sbuf *out, struct whose *w); + +// Publish a claimant. `name` is the column it prints under. +void dbg_register_whose(const char *name, dbg_whose_fn fn); + // Run one command line and render the answer. Returns the number of bytes // written into `out`. // @@ -46,6 +73,7 @@ void dbg_register(const char *name, dbg_view_fn fn, const char *what); // help what this kernel can be asked // all every view, in registration order // [arg] that view, optionally narrowed +// whose every subsystem's claim on one page int dbg_command(const char *line, struct sbuf *out); // Every view, straight to the console. This is what sysrq 'g' runs, and it is diff --git a/include/defs.h b/include/defs.h index 89eb890..9459fde 100644 --- a/include/defs.h +++ b/include/defs.h @@ -365,6 +365,12 @@ void ioapicinit(void); // dma.c — see include/dma.h for the full contract void dma_init(void*, void*); +// watch.c -- hardware watchpoints on DR0-DR3. See docs/plan/debugger.md, D4. +struct trapframe; +void watch_init(void); +void watch_sync(void); +int watch_trap(struct trapframe *tf); + // dbg.c -- /dev/dbg, the introspection registry. See include/dbg.h. int dbg_read(struct inode*, uint64, char*, int); int dbg_write(struct inode*, uint64, char*, int); @@ -667,6 +673,7 @@ void vm_unmap_range(pml4e_t*, uint64, uint64); // Unmap without freeing: for pages this process does not own — device memory // and shared segments. vm_unmap_range() would hand them to kfree. void vm_unmap_norelease(pml4e_t*, uint64, uint64); +void vm_walk_user(pml4e_t*, void (*)(void*, uint64, uint64, uint64), void*); // Shared memory (sys_mem.c). shm_ref/shm_detach maintain the attachment // count that decides when a segment's pages go back. void shm_ref(int id); diff --git a/kernel/arch/x86_64/trap.c b/kernel/arch/x86_64/trap.c index d070a28..0b2f91f 100644 --- a/kernel/arch/x86_64/trap.c +++ b/kernel/arch/x86_64/trap.c @@ -90,8 +90,24 @@ trap(struct trapframe *tf) wakeup(&ticks); release(&tickslock); } + // Debug registers are per-CPU, and there is no general IPI here, so this + // is how a watchpoint armed on one processor reaches the others: each + // notices the table's generation has moved and reloads. One comparison + // per tick when nothing has changed. See kernel/arch/x86_64/watch.c. + watch_sync(); lapiceoi(); break; + + case T_DEBUG: + // A hardware watchpoint fired. It is a *trap*, not a fault -- the store + // has already happened -- so reporting and returning carries on with the + // program none the wiser, which is what lets this be used on a machine + // that is still running. + if(watch_trap(tf)) + break; + // Not ours: a single-step, or a #DB from somewhere else. Falls through to + // the unexpected-trap path rather than being swallowed. + goto unexpected; case T_IRQ0 + IRQ_SPURIOUS: cprintf("cpu%d: spurious interrupt at %p:%p\n", cpunum(), tf->cs, tf->rip); @@ -110,6 +126,7 @@ trap(struct trapframe *tf) break; } + unexpected: if(myproc() == 0 || (tf->cs&3) == 0){ // The rip is named where it can be. This is a kernel address, so // ksym_lookup() has something to say about it -- and the whole reason diff --git a/kernel/arch/x86_64/vm.c b/kernel/arch/x86_64/vm.c index 06057e6..5e637e6 100644 --- a/kernel/arch/x86_64/vm.c +++ b/kernel/arch/x86_64/vm.c @@ -547,6 +547,56 @@ deallocuvm(pml4e_t *pgdir, uint64 oldsz, uint64 newsz) return newsz; } +// Walk every page a process has mapped in the low half, calling back for each. +// +// Written for `whose` (docs/architecture/debugging.md): answering "which +// process has this physical page mapped" means a reverse lookup, and there is +// no reverse map in this kernel -- so the only way is forward, over every +// entry. That is O(pages mapped), a few thousand iterations for a real +// process, which is nothing for a command somebody typed and would be +// unacceptable anywhere else. +// +// The low half only. The kernel half is the direct map and is identical in +// every address space, so reporting it per process would be noise: every +// physical page is mapped there, always, and saying so once is the useful +// form of that fact. +void +vm_walk_user(pml4e_t *pml4, void (*fn)(void *ctx, uint64 va, uint64 pa, uint64 pte), + void *ctx) +{ + uint i, j, k, l; + pdpe_t *pdp; + pde_t *pd; + pte_t *pt; + + if (pml4 == 0) + return; + + for (i = 0; i < (512/2); i++) { + if (!(pml4[i] & PTE_P)) + continue; + pdp = (pdpe_t*)P2V(PTE_ADDR(pml4[i])); + for (j = 0; j < 512; j++) { + if (!(pdp[j] & PTE_P)) + continue; + pd = (pde_t*)P2V(PTE_ADDR(pdp[j])); + for (k = 0; k < 512; k++) { + if (!(pd[k] & PTE_P)) + continue; + pt = (pte_t*)P2V(PTE_ADDR(pd[k])); + for (l = 0; l < 512; l++) { + if (!(pt[l] & PTE_P)) + continue; + fn(ctx, + ((uint64)i << 39) | ((uint64)j << 30) | ((uint64)k << 21) | + ((uint64)l << 12), + PTE_ADDR(pt[l]), pt[l]); + } + } + } + } +} + void freevm(pml4e_t *pml4) { diff --git a/kernel/arch/x86_64/watch.c b/kernel/arch/x86_64/watch.c new file mode 100644 index 0000000..54fabd4 --- /dev/null +++ b/kernel/arch/x86_64/watch.c @@ -0,0 +1,416 @@ +// 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 [len] [w|rw] arm one; len is 1, 2, 4 or 8\n" + " watch clear 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 , 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 ` to arm one"); +} diff --git a/kernel/core/dma.c b/kernel/core/dma.c index bbbd7e2..82a457c 100644 --- a/kernel/core/dma.c +++ b/kernel/core/dma.c @@ -85,6 +85,46 @@ dma_dump(struct sbuf *out, const char *arg) release(&arena.lock); } +// Does the arena own this page, and does it think it is free? +// +// The "free" half is what matters. A page the arena has handed out and a page +// the arena is holding look identical from outside, and the difference is +// whether someone else may be given it next. +static void +dma_whose(struct sbuf *out, struct whose *w) +{ + addr_t kva = (addr_t)P2V(w->phys); + int i, freed = 0; + + if (kva < arena.lo || kva >= arena.hi) + return; + + acquire(&arena.lock); + for (i = 0; i < arena.n; i++) { + if (kva >= arena.free[i].base && + kva < arena.free[i].base + arena.free[i].len) { + freed = 1; + break; + } + } + release(&arena.lock); + + w->owners++; + sb_puts(out, " dma arena"); + if (freed) { + w->free_owners++; + sb_puts(out, ", FREE -- in extent "); + sb_putu(out, i); + sb_puts(out, " (0x"); + sb_puthex(out, arena.free[i].base, 16); + sb_puts(out, ", "); + sb_putu(out, arena.free[i].len); + sb_puts(out, " bytes)\n"); + } else { + sb_puts(out, ", handed out\n"); + } +} + void dma_init(void *vstart, void *vend) { @@ -99,6 +139,7 @@ dma_init(void *vstart, void *vend) arena.total = arena.free[0].len; dbg_register("dma", dma_dump, "the DMA arena's free extents"); + dbg_register_whose("dma", dma_whose); } // Remove free[i], shifting the tail down. diff --git a/kernel/core/kalloc.c b/kernel/core/kalloc.c index f9b7d98..d4807c3 100644 --- a/kernel/core/kalloc.c +++ b/kernel/core/kalloc.c @@ -116,11 +116,59 @@ mem_dump(struct sbuf *out, const char *arg) sb_puts(out, "\n the free list did not end -- it has a CYCLE in it\n"); } +// The page allocator, and the kernel's own image. +// +// Both live here because both are answered from the same two facts -- where +// the kernel ends and what is on the free list -- and because a page inside +// the kernel image being on the free list is precisely the corruption this +// query exists to catch. +static void +mem_whose(struct sbuf *out, struct whose *w) +{ + addr_t kva = (addr_t)P2V(w->phys); + struct run *r; + uint64 n = 0; + int found = 0; + + if (w->phys >= (addr_t)PHYSTOP) + return; + + if (kva < (addr_t)end) { + // Below `end` is the kernel image itself: text, rodata, data and bss, plus + // everything the loader put under it. It is owned, permanently, by nothing + // that can be asked -- so it is reported here. + w->owners++; + sb_puts(out, " kernel image (below `end` at 0x"); + sb_puthex(out, (addr_t)end, 16); + sb_puts(out, ")\n"); + return; + } + + if (kmem.use_lock) + acquire(&kmem.lock); + for (r = kmem.freelist; r; r = r->next) { + if (++n > (PHYSTOP / PGSIZE) + 16) + break; // a cycle; mem_dump says so + if ((addr_t)r == kva) { found = 1; break; } + } + if (kmem.use_lock) + release(&kmem.lock); + + if (found) { + w->owners++; + w->free_owners++; + sb_puts(out, " kalloc FREE -- position "); + sb_putu(out, n); + sb_puts(out, " on the free list\n"); + } +} + void kinit2() { kmem.use_lock = 1; dbg_register("mem", mem_dump, "the page allocator's free list"); + dbg_register_whose("mem", mem_whose); } void diff --git a/kernel/core/klog.c b/kernel/core/klog.c index 1bde706..5147fdb 100644 --- a/kernel/core/klog.c +++ b/kernel/core/klog.c @@ -24,6 +24,8 @@ #include "defs.h" #include "memlayout.h" #include "klog.h" +#include "sbuf.h" +#include "dbg.h" // consputc()'s cursor-movement escape, from kernel/core/console.c. #define BACKSPACE 0x100 @@ -153,6 +155,33 @@ klog_putc(int c) at_line_start = (ch == '\n'); } +// The log's own region. +// +// It is not on any allocator's list and never will be -- kinit1 is started +// above it -- so the only way to discover that something has been handed a +// page of it is to ask here. A page of the log turning up on kalloc's free +// list is the failure mode this whole region is arranged to avoid, and this +// is what would say so. +static void +klog_whose(struct sbuf *out, struct whose *w) +{ + addr_t base, kva; + + if (!klog) + return; + base = (addr_t)klog; + kva = (addr_t)P2V(w->phys); + if (kva < base || kva >= base + KLOG_BYTES) + return; + + w->owners++; + sb_puts(out, " klog the kernel log, "); + sb_putu(out, (kva - base) / 1024); + sb_puts(out, " KB into a "); + sb_putu(out, KLOG_BYTES / 1024); + sb_puts(out, " KB region\n"); +} + void klog_init(void *base, uint64 bytes) { @@ -215,6 +244,8 @@ klog_init(void *base, uint64 bytes) h->sum = checksum(h); + dbg_register_whose("klog", klog_whose); + // Said out loud on every boot, because "the log survived" and "the log was // silently started over" look identical in a file that begins with a boot // divider either way. diff --git a/kernel/core/main.c b/kernel/core/main.c index 3c0a631..212c6f3 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -118,6 +118,9 @@ kmain(uint32 fb_addr) lapicinit(); if (boot_verbose) cprintf("verbose boot: lapicinit done\n"); tvinit(); + // Once, on the boot processor. mpmain() runs on every CPU, and registering + // from there published the command as many times as there are processors. + watch_init(); if (boot_verbose) cprintf("verbose boot: tvinit done\n"); seginit(); if (boot_verbose) cprintf("verbose boot: seginit done\n"); diff --git a/kernel/dev/dbg.c b/kernel/dev/dbg.c index ee29c13..d926129 100644 --- a/kernel/dev/dbg.c +++ b/kernel/dev/dbg.c @@ -26,6 +26,8 @@ #include "file.h" #include "errno.h" #include "sbuf.h" +#include "memlayout.h" +#include "mmu.h" #include "dbg.h" #define NVIEW 24 @@ -40,6 +42,15 @@ struct view { 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 @@ -54,12 +65,31 @@ dbg_register(const char *name, dbg_view_fn fn, const char *what) { 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 : ""; nview++; } +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) { @@ -77,8 +107,140 @@ do_help(struct sbuf *out) sb_puts(out, views[i].what); sb_putc(out, '\n'); } - sb_puts(out, "\n all every view above\n"); - sb_puts(out, " help this\n"); + sb_puts(out, "\n whose 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 -- 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 ` 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 @@ -122,6 +284,10 @@ dbg_command(const char *line, struct sbuf *out) 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)) { diff --git a/kernel/dev/pciuser.c b/kernel/dev/pciuser.c index 9ec4bd5..777279e 100644 --- a/kernel/dev/pciuser.c +++ b/kernel/dev/pciuser.c @@ -107,6 +107,37 @@ pciuser_owner_pid(int idx) return pid; } +// A DMA block handed to a userspace driver. +// +// Informational rather than an ownership claim: the arena is the owner and +// says so separately. What this adds is *which driver was given it*, which is +// the question you actually have when a page turns up somewhere unexpected. +static void +pciuser_whose(struct sbuf *out, struct whose *w) +{ + acquire(&pciuser_lock); + for (int i = 0; i < PCIUSER_MAXDMA; i++) { + addr_t base; + if (!dmablk[i].proc) + continue; + base = dma_phys(dmablk[i].kva); + if (w->phys < base || w->phys >= base + dmablk[i].size) + continue; + sb_puts(out, " pcidma slot "); + sb_putu(out, i); + sb_puts(out, ", "); + sb_putu(out, (w->phys - base) / 1024); + sb_puts(out, " KB into a "); + sb_putu(out, dmablk[i].size / 1024); + sb_puts(out, " KB block given to pid "); + sb_putu(out, dmablk[i].proc->pid); + sb_puts(out, " ("); + sb_puts(out, dmablk[i].proc->name); + sb_puts(out, ")\n"); + } + release(&pciuser_lock); +} + // Who is holding what, and what memory they were given for it. // // The three things that go wrong with a userspace driver are all here: a @@ -207,6 +238,7 @@ pciuser_init(void) initlock(&pciuser_lock, "pciuser"); dbg_register("claims", pciuser_dump, "devices, DMA and interrupts held by userspace drivers"); + dbg_register_whose("pcidma", pciuser_whose); } // Does `phys` fall inside a memory BAR of a device this process has claimed? diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index af039f1..f0f7cae 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -161,12 +161,89 @@ vma_dump(struct sbuf *out, const char *arg) : " (no process is holding a mapping)\n"); } +// Every page table that maps this physical page, and which VMA it falls in. +// +// This is the half of `whose` that no allocator can answer. An allocator knows +// whether it has handed a page out; it has no idea whether anyone still has it +// mapped, and "free, but three processes are still writing to it" is the exact +// shape of the bug that prompted all of this. +// +// The walk is forward -- there is no reverse map -- so it costs one pass over +// every mapped page of every live process. See vm_walk_user(). + +struct maphit { + struct sbuf *out; + struct whose *w; + struct proc *p; +}; + +static void +maphit(void *ctx, uint64 va, uint64 pa, uint64 pte) +{ + struct maphit *m = (struct maphit *)ctx; + + if (pa != m->w->phys) + return; + + m->w->mappings++; + sb_puts(m->out, " mapped pid "); + sb_putu(m->out, m->p->pid); + sb_puts(m->out, " ("); + sb_puts(m->out, m->p->name); + sb_puts(m->out, ") at 0x"); + sb_puthex(m->out, va, 16); + sb_puts(m->out, (pte & PTE_W) ? " rw" : " r-"); + sb_puts(m->out, (pte & PTE_U) ? "u" : "-"); + + // And whether the process admits to holding it. A mapping with no VMA is + // how the DMA blocks used to look, which is why exit() freed them twice. + { + int i, found = 0; + for (i = 0; i < NVMA; i++) { + if (!m->p->vma[i].used) + continue; + if (va < m->p->vma[i].addr || + va >= m->p->vma[i].addr + m->p->vma[i].length) + continue; + found = 1; + sb_puts(m->out, " vma["); + sb_putu(m->out, i); + sb_puts(m->out, m->p->vma[i].no_free ? "] no_free" : "]"); + break; + } + if (!found) { + if (va < m->p->sz) + sb_puts(m->out, " heap"); + else + sb_puts(m->out, " NO VMA -- exit() will free this page"); + } + } + sb_putc(m->out, '\n'); +} + +static void +proc_whose(struct sbuf *out, struct whose *w) +{ + struct proc *p; + + acquire(&ptable.lock); + for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) { + struct maphit m; + if (p->state == UNUSED || p->pgdir == 0) + continue; + m.out = out; m.w = w; m.p = p; + vm_walk_user(p->pgdir, maphit, &m); + } + release(&ptable.lock); +} + void pinit(void) { initlock(&ptable.lock, "ptable"); dbg_register("proc", proc_dump, "every process, with its state and size"); dbg_register("vma", vma_dump, "mappings a process holds but does not own"); + dbg_register_whose("mapped", proc_whose); } static struct proc*