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
243 lines
6.4 KiB
C
243 lines
6.4 KiB
C
// Physical memory allocator, intended to allocate
|
|
// memory for user processes, kernel stacks, page table pages,
|
|
// and pipe buffers. Allocates 4096-byte pages.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "spinlock.h"
|
|
#include "sbuf.h"
|
|
#include "dbg.h"
|
|
|
|
void freerange(void *vstart, void *vend);
|
|
extern char end[]; // first address after kernel loaded from ELF file
|
|
|
|
|
|
struct run {
|
|
struct run *next;
|
|
};
|
|
|
|
struct {
|
|
struct spinlock lock;
|
|
int use_lock;
|
|
struct run *freelist;
|
|
} kmem;
|
|
|
|
// Catch kmem.freelist corruption at the point it's touched, rather than
|
|
// letting a bad pointer be silently returned by kalloc() and crash whatever
|
|
// code dereferences it minutes later on an unrelated call stack.
|
|
static void
|
|
check_run(struct run *r, char *where)
|
|
{
|
|
if(r == 0)
|
|
return;
|
|
if((addr_t)r % PGSIZE || (char*)r < end || V2P(r) >= PHYSTOP){
|
|
cprintf("kalloc: corrupt freelist entry %p at %s\n", r, where);
|
|
panic("kalloc: corrupt freelist");
|
|
}
|
|
}
|
|
|
|
// vstart..vend is the window the kernel is willing to give away: from the end
|
|
// of its own image to the bottom of the DMA arena. Inside that window the
|
|
// firmware's map decides what is real -- free each usable region clipped to the
|
|
// window, rather than walking the whole span and handing out the holes.
|
|
//
|
|
// The old code walked it as one range, which is what a BIOS map looks like.
|
|
// Under UEFI it is not: OVMF puts an ACPI NVS region at 8 MB, so the run
|
|
// containing the kernel stopped there and everything above it was lost. The
|
|
// window still bounds things at both ends, so nothing below the kernel image
|
|
// and nothing inside the DMA arena is ever freed, whatever the map says.
|
|
void
|
|
kinit1(void *vstart, void *vend)
|
|
{
|
|
initlock(&kmem.lock, "kmem");
|
|
kmem.use_lock = 0;
|
|
kmem.freelist = 0; // empty
|
|
|
|
// No map at all means e820_init() could not parse one; it leaves a single
|
|
// synthetic region behind for exactly this reason, so this is belt and
|
|
// braces rather than a path anything takes.
|
|
if(nmem_usable == 0){
|
|
freerange(vstart, vend);
|
|
return;
|
|
}
|
|
|
|
addr_t lo_limit = V2P((addr_t)vstart);
|
|
addr_t hi_limit = V2P((addr_t)vend);
|
|
|
|
for(int i = 0; i < nmem_usable; i++){
|
|
addr_t lo = mem_usable[i].base;
|
|
addr_t hi = mem_usable[i].end;
|
|
|
|
if(lo < lo_limit)
|
|
lo = lo_limit;
|
|
if(hi > hi_limit)
|
|
hi = hi_limit;
|
|
if(lo >= hi)
|
|
continue;
|
|
|
|
freerange(P2V(lo), P2V(hi));
|
|
}
|
|
}
|
|
|
|
// The page allocator, from the outside.
|
|
//
|
|
// Counting the free list is O(the free list), which on a gigabyte is a quarter
|
|
// of a million links. That is fine for a command someone typed and would not
|
|
// be fine anywhere else, so it lives here and nowhere near a hot path -- and
|
|
// the walk is bounded, because the one time you most want to run this is when
|
|
// you suspect the list is a loop.
|
|
static void
|
|
mem_dump(struct sbuf *out, const char *arg)
|
|
{
|
|
(void)arg;
|
|
struct run *r;
|
|
uint64 n = 0;
|
|
int looped = 0;
|
|
|
|
if (kmem.use_lock)
|
|
acquire(&kmem.lock);
|
|
for (r = kmem.freelist; r; r = r->next) {
|
|
if (++n > (PHYSTOP / PGSIZE) + 16) { looped = 1; break; }
|
|
}
|
|
if (kmem.use_lock)
|
|
release(&kmem.lock);
|
|
|
|
sb_puts(out, "window 0x");
|
|
sb_puthex(out, (addr_t)end, 16);
|
|
sb_puts(out, " upward; PHYSTOP is 0x");
|
|
sb_puthex(out, (addr_t)PHYSTOP, 16);
|
|
sb_putc(out, '\n');
|
|
sb_kv(out, "free ", n, "pages");
|
|
sb_kv(out, " ", n * PGSIZE / 1024, "KB");
|
|
if (looped)
|
|
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
|
|
freerange(void *vstart, void *vend)
|
|
{
|
|
char *p;
|
|
p = (char*)PGROUNDUP((addr_t)vstart);
|
|
for(; p + PGSIZE <= (char*)vend; p += PGSIZE)
|
|
kfree(p);
|
|
}
|
|
|
|
// Free the page of physical memory pointed at by v,
|
|
// which normally should have been returned by a
|
|
// call to kalloc(). (The exception is when
|
|
// initializing the allocator; see kinit above.)
|
|
void
|
|
kfree(char *v)
|
|
{
|
|
struct run *r;
|
|
|
|
if((addr_t)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
|
|
panic("kfree");
|
|
|
|
// Fill with junk to catch dangling refs.
|
|
memset(v, 1, PGSIZE);
|
|
|
|
if(kmem.use_lock)
|
|
acquire(&kmem.lock);
|
|
check_run(kmem.freelist, "kfree entry");
|
|
r = (struct run*)v;
|
|
r->next = kmem.freelist;
|
|
kmem.freelist = r;
|
|
if(kmem.use_lock)
|
|
release(&kmem.lock);
|
|
}
|
|
|
|
// Allocate one 4096-byte page of physical memory.
|
|
// Returns a pointer that the kernel can use.
|
|
// Returns 0 if the memory cannot be allocated.
|
|
char*
|
|
kalloc(void)
|
|
{
|
|
struct run *r;
|
|
|
|
if(kmem.use_lock)
|
|
acquire(&kmem.lock);
|
|
check_run(kmem.freelist, "kalloc entry");
|
|
r = kmem.freelist;
|
|
if(r)
|
|
kmem.freelist = r->next;
|
|
|
|
if(kmem.use_lock)
|
|
release(&kmem.lock);
|
|
return (char*)r;
|
|
}
|
|
|
|
// Total free physical memory, in bytes. Walks the free list; only /proc
|
|
// readers call it, so the O(n) scan is not on any hot path.
|
|
uint64
|
|
kalloc_free_bytes(void)
|
|
{
|
|
struct run *r;
|
|
uint64 n = 0;
|
|
|
|
if(kmem.use_lock)
|
|
acquire(&kmem.lock);
|
|
for(r = kmem.freelist; r; r = r->next)
|
|
n += PGSIZE;
|
|
if(kmem.use_lock)
|
|
release(&kmem.lock);
|
|
return n;
|
|
}
|