debugger: who owns this page, and who just wrote to it
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
This commit is contained in:
parent
1dd4ba0186
commit
113595f80b
17 changed files with 1067 additions and 19 deletions
2
Makefile
2
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 \
|
||||
|
|
|
|||
|
|
@ -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 <pid>` 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).
|
||||
|
|
|
|||
|
|
@ -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 <addr>`, 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <addr>`** — 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 <addr>`** — 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <addr> -----------------------------------------------------------
|
||||
//
|
||||
// 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
|
||||
// <name> [arg] that view, optionally narrowed
|
||||
// whose <addr> 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
416
kernel/arch/x86_64/watch.c
Normal file
416
kernel/arch/x86_64/watch.c
Normal file
|
|
@ -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 <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");
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
168
kernel/dev/dbg.c
168
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,10 +107,142 @@ 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, "\n whose <addr> 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 <addr> -- 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 <pid>` 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
|
||||
do_all(struct sbuf *out)
|
||||
{
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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*
|
||||
|
|
|
|||
Loading…
Reference in a new issue