The dissection toolbox: every in-OS observability builtin, the MCP bridge
that drives them from an agent, and the host harnesses that prove them.
Normative contract: CLAUDE.md ("Observability / dissection toolbox").
One-line index: docs/cheatsheet.md.
Doctrine: everything is serial-observable. State is snapshotted under the
owning lock and printed after release, so console I/O never runs with
sche_lock-class locks held. A tool that cannot observe something says so
on screen instead of printing a forged number.
| Tool | Sees | Answers |
|---|---|---|
trace |
syscalls, numeric or named | what is the system doing right now? |
strace |
syscalls, named + decoded | how does userland ask the kernel for resources? |
ltrace |
allocator-trap subset | what does malloc trap to? (no PLT on static ELFs) |
vmmap |
user window + VMA tree | where do my code, heap and mappings live? |
mem |
heap/ramdisk/minifs/procs | what is exhausted? |
schedtop |
scheduler state | who owns the CPU and why? |
irqstat |
interrupt arrivals | is the hardware talking, is the guest keeping up? |
bootlog |
boot phases with ms | where did boot time go? |
gdb |
contexts, memory, remote hookup | what is the value RIGHT NOW? |
panic |
fault vector/RIP/RSP/backtrace on screen | what killed the machine, with no serial? |
ps / jobs |
processes / shell children | what exists, what finished? |
kstack |
kernel-stack high water + canary | did a stack overflow? |
smp / sb16 / net / gfx |
subsystem status | per-domain health |
miniOS> trace verbose
syscall tracing: on (verbose)
miniOS> strace run bin/lxhello.elf
Hello from a REAL Linux ELF executable (syscall ABI)!
syscall write(1, 4202496, 54, ...) fd=1 len=54 = 54
syscall exit(1, ...) code=1 = 1
exit code: 1
strace: done (tracing restored on)
trace [on|off|verbose|quiet]. Verbose resolves names (syscall_name: Linux table, then MiniOS window, then a data table of out-of-table Linux numbers) and decodes: path strings foropen/openat/access/ unlink/readlink,code=for exit,addr=for brk,len=for mmap,fd/lenfor read/write,pid=for kill/wait4.SYS_TIME(204),SYS_KBD(205),SYS_MOUSE(219),GETC_RAW(236) are never traced: pacing loops hammer them and tracing turned interactive programs into a crawl.- Zero traced syscalls is itself a diagnosis:
strace minifetchprintsstrace: 'minifetch' is a shell builtin (ring 0): nothing to traceplus the count (done (0 syscalls, ...)). Builtins never cross into ring 3, so there is nothing to trace; usestrace run bin/lxhello.elffor a real dialogue. - Each traced line is printed atomically AFTER dispatch returns: the path
hint is snapshotted before (max 48 bytes, only after
user_str_ok, else<bad-ptr>), the whole line prints after, so program output never interleaves mid-line. A program that prints without a trailing newline still leaves its partial line before the trace line: that is the program's bytes, not a corruption.
miniOS> ltrace run bin/lxhello.elf
ltrace: no PLT on static ELFs; brk/mmap/mprotect/open/close only
...
Honest proxy, stated on every run: static ELFs carry no PLT to hook, so
function-level tracing is impossible in-guest. It runs the strace path
so the traps malloc/free actually take stay visible. True per-symbol
tracing is host-GDB work (make gdb + break malloc).
miniOS> vmmap
vmmap: pid=0 (kernel) window 0x400000..0xc000000
text 0x400000..0x403000 brk 0x403000 cap 0xb000000 mmap 0xb000000..0xbf00000
game 0xb000000 fb 0xb200000 nk 0xb600000 stack 0xbf00000..0xc000000
vma-live: (empty)
Pid 0 / the running view reads the g_brk globals; any other pid reads
its saved per-proc view. The VMA walk is bounded (64-deep explicit
stack, 128 regions, then truncated).
miniOS> schedtop
schedtop: up 4s ticks 448 cpus 1
cpu0 cur=0 dispatched=0 polls=0
pid tgid T/P ppid state nice vruntime ticks name
0 0 P -1 run 0 0 0 kernel
miniOS> irqstat
irqstat: timer=463 kbd=1 mouse=13 sb16=0 bad_gs=0
irqstat: net tx=0 rx=0 drop=0 sb16 sub=0 drop=0 gfx=0
miniOS> bootlog
bootlog: 6 phases (ms since power-on)
+ 1ms entry
+ 1ms heap
+ 3ms mm+fb
+ 12ms block+minifs
+ 570ms sched
+ 1970ms smp+audio-ready
timercountssys_ticks(100 Hz PIT + BSP IPI broadcast);kbd/mouse/sb16are counted at the top of theirisr_dispatcharms (isr_cnt_*, increment-only, lock-free single words).- Pre-
sched_initboot marks are TSC-derived, still monotonic.
miniOS> gdb regs 0
gdb: pid=0 (kernel) state=2 LIVE (sampled now)
gdb: rip=0x10f9dd rsp=0x8f1c0 rflags=0x246 cr3=0x1000 kstack=0x2c7a40
gdb: gprs not shown: live gprs are clobbered by this call
miniOS> gdb dump 0x400000 16
gdb: 0x400000: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
regs [pid]: the running pid prints LIVE-sampledrip/rsp/rflags/cr3(read without a trap viamov/pushfq/return-address); GPRs are refused, not forged, because the call path clobbers them. Preempted pids print the full parked context.regson pid 0 used to print zeros; that was a placeholder, now it samples.dump <addr> <len>: 1..256 bytes. Operands are decimal or0x-hex, strictly validated (no signs, no trailing garbage, fail-closed on overflow):gdb dump 0x400000 16andgdb dump 4194304 16agree.gdb qemuprints the remote hookup. A serial RSP stub is deliberately out of scope: the console belongs to the shell, so a stub would fight the prompt for every byte. Real breakpoints/single-step:make gdb, hosttarget remote :1234+add-symbol-file kernel.elf 0x100000(ring-3 ELF: base0x400000).
miniOS> panic
panic: vector=00 err=00000000 rip=000000000010f6ff rsp=000000000008f280 bt= 0000000000112188 00000000001001d2
- Unrecoverable faults paint the same screen from the fault handler
(vector/err/RIP/RSP plus up to five frame-pointer returns) on the
framebuffer terminal, raw VGA text, or serial-only under a graphics
mode, then halt. Recovering ring-3 faults and killed threads never
reach it.
panicdemos the screen without halting.
mcp/minios_mcp.py (stdlib only, one QEMU child on a pty, marker waits
capped by timeout_ms). The debug loop is: minios_boot once, then
minios_send / minios_expect / minios_snapshot, assert with
minios_test, minios_poweroff when done.
| Tool | Use for debugging |
|---|---|
minios_status |
{booted, pid, log_bytes, log_cap}; never fails |
minios_boot |
spawn QEMU, wait for miniOS> ; idempotent |
minios_send |
send one shell line, return output up to next prompt (exit code: N surfaces here) |
minios_expect |
wait for a marker (cursor advances past the match) |
minios_snapshot |
tail of the log since the cursor (peek) |
minios_write / minios_cat |
upload / print ramdisk files ([A-Za-z0-9._/-], printable ASCII, editor limits enforced up front) |
minios_python |
run a Python snippet inside the bridge process |
minios_test |
send commands, assert expect markers present / refute absent; returns {pass, failures, transcript} |
minios_addons / minios_install |
marketplace: build inside the OS, verify exit codes, record in var/lib/addons.txt |
minios_poweroff |
clean shutdown, releases the pid file |
Tests: python3 -m unittest -v mcp/test_minios_mcp.py (unit + QEMU BDD,
skips cleanly without QEMU/image); mcp/mutate_mcp.sh (every mutant
killed, parallel via MUTATE_JOBS).
tools/boot_run.sh "strace run bin/lxhello.elf" "vmmap" --timeout 90 --log run.log
./tools/test_bdd.sh # full serial-console suite
./tools/test_codecs.sh # lzss/lz4/aes roundtrips (pass=3)
./tools/mutate.sh # kernel/boot mutants, all must die
python3 tools/test_gui_wm.py # QMP pixel proof (Alt+Tab/tile/taskbar)
python3 tools/test_gui_fashion.py # one cursor, stable frames, ESC quit
python3 tools/test_gui_icon_cwd.py # dock launch ignores shell cwd
make gdb # QEMU -s -S gls; host attaches :1234tools/minios_gui.py is mandatory for VGA-mode work: headless boots
cannot observe desktop events. sh src/test_all.sh (79 PASS) covers the
toolbox non-interactively (trace-verbose, strace, ltrace, vmmap,
schedtop, irqstat, bootlog, gdb, gdb-regs, gdb-dump-hex).
- What does
printfdo?strace run bin/lxhello.elf: onewriteper flush,exitwithcode=. - Where is my memory?
vmmap <pid>: brk cur/cap vs mmap cur/cap; a growingvma-livelist with no matchingmunmapinstraceis a leak. - Who is slow?
schedtop(ticks/vruntime per proc) +irqstat(timer flowing?) +perf(console throughput vs ktime overhead). - Did the stack overflow?
kstack: per-proc high water;OVERFLOWnames the corpse via the canary. - Where did boot go?
bootlog:smp+audio-readydominates (AP bring-up waits);schedmarks the PIT start. - Dead graphics program?
gfx framesbefore/after +irqstatgfx line;wm listfor the window; title-bar X armswm_close_request.
make check-size (prerequisite of kernel.bin) fails the build when
_kernel_end > USER_LOAD_BASE (0x400000). That address is pinned by
the Linux-compat hard requirement, not by taste: load_exec_elf
(kernel/loader.c) maps ET_EXEC segments at their linked p_vaddr
with base = 0 and rejects dst < USER_LOAD_BASE, and static Linux
binaries link at 0x400000. Moving the window up would orphan every
ET_EXEC; the kernel cannot grow down either (boot structures, page
tables, the 0x10000 user-PT zone). So the ~3 MB [0x100000,0x400000)
image span is structural, and the gate forces the conversation at build
time instead of as a black screen in QEMU.
Measured cost of this toolbox (.o text deltas vs pre-toolbox):
shell.o +~3.4 KB (vmmap/strace-run/gdb/help), sched.o +~2.9 KB
(reports + live-regs sample), syscalls.o +~1.8 KB (name tables +
atomic trace print), kernel.o +~0.4 KB (bootlog), ramdisk +~0.3 KB
(suite markers).
Trimmed back with a data-table resolver (no case N: ladder, which also
keeps check_abi_numbers.py Rule C quiet), terse formats, and a 12-slot
bootlog. Next levers if the gate bites again: teach shell_resolve_run
a MiniFS fallback so bulk (e.g. minigcc.elf, 140 KB) can leave the
ramdisk; compress help text; split cold reports behind a build flag.