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
289 lines
7.1 KiB
C
289 lines
7.1 KiB
C
// Physically contiguous memory for devices. See include/dma.h.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "spinlock.h"
|
|
#include "dma.h"
|
|
#include "sbuf.h"
|
|
#include "dbg.h"
|
|
|
|
#define DMA_GRAIN 64 // one cache line; also xHCI's ring alignment
|
|
#define NEXTENT 64 // free extents we can track
|
|
|
|
struct extent {
|
|
addr_t base; // kernel virtual address
|
|
uint64 len;
|
|
};
|
|
|
|
static struct {
|
|
struct spinlock lock;
|
|
struct extent free[NEXTENT];
|
|
int n; // free extents, kept sorted by base
|
|
addr_t lo, hi;
|
|
uint64 total;
|
|
} arena;
|
|
|
|
static uint64
|
|
roundup(uint64 v, uint64 to)
|
|
{
|
|
return (v + to - 1) & ~(to - 1);
|
|
}
|
|
|
|
// What the arena is holding, extent by extent.
|
|
//
|
|
// This is the view that would have ended the H2 investigation in one line. The
|
|
// bug was that a 4 MB block was on this free list *and* on kalloc's at the same
|
|
// time, and the only way to see it at the time was to read two allocators and
|
|
// reason about PHYSTOP. Printing the list is not clever; not being able to
|
|
// print it was the problem.
|
|
static void
|
|
dma_dump(struct sbuf *out, const char *arg)
|
|
{
|
|
(void)arg;
|
|
acquire(&arena.lock);
|
|
|
|
sb_puts(out, "arena 0x");
|
|
sb_puthex(out, arena.lo, 16);
|
|
sb_puts(out, "-0x");
|
|
sb_puthex(out, arena.hi, 16);
|
|
sb_puts(out, " (virtual; physical is this minus KERNBASE)\n");
|
|
|
|
sb_puts(out, " ");
|
|
sb_putu(out, arena.total / 1024);
|
|
sb_puts(out, " KB total, ");
|
|
{
|
|
uint64 free = 0;
|
|
for (int i = 0; i < arena.n; i++)
|
|
free += arena.free[i].len;
|
|
sb_putu(out, free / 1024);
|
|
sb_puts(out, " KB free in ");
|
|
sb_putu(out, arena.n);
|
|
sb_puts(out, " extent(s), ");
|
|
sb_putu(out, (arena.total - free) / 1024);
|
|
sb_puts(out, " KB out\n");
|
|
}
|
|
|
|
sb_puts(out, "\n # base phys bytes\n");
|
|
for (int i = 0; i < arena.n; i++) {
|
|
sb_puts(out, " ");
|
|
sb_putu_pad(out, i, 2);
|
|
sb_puts(out, " 0x");
|
|
sb_puthex(out, arena.free[i].base, 16);
|
|
sb_puts(out, " 0x");
|
|
sb_puthex(out, arena.free[i].base - (addr_t)KERNBASE, 16);
|
|
sb_puts(out, " ");
|
|
sb_putu_pad(out, arena.free[i].len, 10);
|
|
sb_putc(out, '\n');
|
|
}
|
|
if (arena.n == NEXTENT)
|
|
sb_puts(out, "\n the extent table is FULL -- a free here leaks rather "
|
|
"than corrupting the list\n");
|
|
|
|
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)
|
|
{
|
|
initlock(&arena.lock, "dma");
|
|
arena.lo = roundup((addr_t)vstart, PGSIZE);
|
|
arena.hi = (addr_t)vend & ~(addr_t)(PGSIZE - 1);
|
|
if(arena.hi <= arena.lo)
|
|
panic("dma_init: empty arena");
|
|
arena.free[0].base = arena.lo;
|
|
arena.free[0].len = arena.hi - arena.lo;
|
|
arena.n = 1;
|
|
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.
|
|
static void
|
|
drop(int i)
|
|
{
|
|
int j;
|
|
for(j = i; j + 1 < arena.n; j++)
|
|
arena.free[j] = arena.free[j+1];
|
|
arena.n--;
|
|
}
|
|
|
|
// Insert a free extent at index i, shifting the tail up. Returns 0 if the
|
|
// table is full, in which case the memory is leaked rather than corrupting
|
|
// the list -- a leak is recoverable by rebooting, a corrupt free list hands
|
|
// two drivers the same ring.
|
|
static int
|
|
insert(int i, addr_t base, uint64 len)
|
|
{
|
|
int j;
|
|
|
|
if(arena.n >= NEXTENT)
|
|
return 0;
|
|
for(j = arena.n; j > i; j--)
|
|
arena.free[j] = arena.free[j-1];
|
|
arena.free[i].base = base;
|
|
arena.free[i].len = len;
|
|
arena.n++;
|
|
return 1;
|
|
}
|
|
|
|
void *
|
|
dma_alloc(uint64 size, uint64 align)
|
|
{
|
|
int i;
|
|
addr_t start;
|
|
|
|
if(size == 0)
|
|
return 0;
|
|
if(align < DMA_GRAIN)
|
|
align = DMA_GRAIN;
|
|
if(align & (align - 1))
|
|
panic("dma_alloc: alignment not a power of two");
|
|
size = roundup(size, DMA_GRAIN);
|
|
|
|
acquire(&arena.lock);
|
|
|
|
for(i = 0; i < arena.n; i++){
|
|
addr_t base = arena.free[i].base;
|
|
uint64 len = arena.free[i].len;
|
|
uint64 lead;
|
|
|
|
start = roundup(base, align);
|
|
lead = start - base;
|
|
if(lead + size > len)
|
|
continue;
|
|
|
|
// Three cases: the request consumes the extent, sits at its head, sits
|
|
// at its tail, or sits in the middle and splits it in two.
|
|
if(lead == 0 && size == len){
|
|
drop(i);
|
|
} else if(lead == 0){
|
|
arena.free[i].base += size;
|
|
arena.free[i].len -= size;
|
|
} else if(lead + size == len){
|
|
arena.free[i].len = lead;
|
|
} else {
|
|
uint64 tail_len = len - lead - size;
|
|
arena.free[i].len = lead;
|
|
if(!insert(i + 1, start + size, tail_len)){
|
|
// No room to record the tail. Give the whole extent to the caller
|
|
// rather than lose track of where the tail was.
|
|
arena.free[i].len = len;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
release(&arena.lock);
|
|
memset((void*)start, 0, size);
|
|
return (void*)start;
|
|
}
|
|
|
|
release(&arena.lock);
|
|
cprintf("dma_alloc: out of DMA memory (wanted %d bytes, %d free)\n",
|
|
(int)size, (int)dma_free_bytes());
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
dma_free(void *p, uint64 size)
|
|
{
|
|
addr_t base = (addr_t)p;
|
|
int i;
|
|
|
|
if(p == 0)
|
|
return;
|
|
size = roundup(size, DMA_GRAIN);
|
|
if(base < arena.lo || base + size > arena.hi)
|
|
panic("dma_free: not DMA memory");
|
|
|
|
acquire(&arena.lock);
|
|
|
|
// Find the first extent that starts after this block, so the list stays
|
|
// sorted and neighbours end up adjacent for coalescing.
|
|
for(i = 0; i < arena.n && arena.free[i].base < base; i++)
|
|
;
|
|
|
|
if(i > 0 && arena.free[i-1].base + arena.free[i-1].len == base){
|
|
arena.free[i-1].len += size; // merge with the block before
|
|
i--;
|
|
} else if(!insert(i, base, size)){
|
|
release(&arena.lock);
|
|
cprintf("dma_free: extent table full, leaking %d bytes\n", (int)size);
|
|
return;
|
|
}
|
|
|
|
if(i + 1 < arena.n &&
|
|
arena.free[i].base + arena.free[i].len == arena.free[i+1].base){
|
|
arena.free[i].len += arena.free[i+1].len; // and with the block after
|
|
drop(i + 1);
|
|
}
|
|
|
|
release(&arena.lock);
|
|
}
|
|
|
|
addr_t
|
|
dma_phys(const void *p)
|
|
{
|
|
return V2P((addr_t)p);
|
|
}
|
|
|
|
uint64
|
|
dma_free_bytes(void)
|
|
{
|
|
uint64 n = 0;
|
|
int i;
|
|
|
|
for(i = 0; i < arena.n; i++)
|
|
n += arena.free[i].len;
|
|
return n;
|
|
}
|
|
|
|
uint64
|
|
dma_total_bytes(void)
|
|
{
|
|
return arena.total;
|
|
}
|