-
Notifications
You must be signed in to change notification settings - Fork 0
Debugging The emulator shell
This page owns: the debugging surface every recipe on this wiki is built from.
Granny Smith's headless build exposes a typed object-model shell over TCP. Every subsystem is a
path — machine.cpu.pc, machine.memory.peek.l, debug.breakpoints.add — and that shell is what
this project's tools drive.
The daemon listens on port 6820 by default. → Getting Granny Smith §5
python3 tools/gsh.py 'echo "pc=${machine.cpu.pc}"'gsh.py is a thin client: it sends a command string and prints what comes back. GS_PORT,
GS_TIMEOUT and GS_IDLE override the defaults — the last two matter, because a boot run takes
far longer than any sensible default.
One daemon holds one machine, and it keeps state between commands. After a run finishes the machine is still there, which is routinely more useful than adding instrumentation to the next run.
machine.cpu.pc # bare path: read and print
machine.cpu.r3 = 0 # write
debug.breakpoints.add 0x300 # method call, argument form
machine.memory.peek.l(0x1000) # call form, usable in any expression
let base = 0x80705000 # a binding; read it back as $base
$base = $base + 0x100 # mutate (error if undeclared)
echo "pc=${machine.cpu.pc:08x}" # ${...} interpolation, inside strings only
if machine.cpu.pc == 0x300 { ... } # blocks
while $i < 100 { ... }
def where() { return "pc=${machine.cpu.pc}" }
Three things that will trip you:
-
Scripts print nothing implicitly. A bare
machine.cpu.pcline is silent in a script. Wrap it inecho. -
$on every read of a binding.let x = 1then$x. -
The first failed statement aborts the script. Probe expected failures with
try(EXPR, none) == none.
machine.cpu.pc msr lr ctr cr xer
machine.cpu.r0 … r31
machine.cpu.srr0 srr1 dar dsisr ← exception state
machine.cpu.sr0 … sr15 bat0u … dbat3l ← segment and BAT registers
srr0, srr1, dar and dsisr are the ones that matter when something faults: the faulting
instruction's address, the machine state at the fault, the address referenced, and why it failed.
→ Decoding a bugcheck
dar is reported with the little-endian address munge applied. Un-munge before doing
arithmetic with it. → When your instrumentation lies
machine.memory.peek.b(addr) peek.w peek.l
machine.memory.poke.b addr v poke.w poke.l
machine.memory.dump addr n
machine.cpu.mmu.translate(ea) ← virtual -> physical
machine.cpu.mmu.peek(ea, size)
find.str "text" start end find.bytes find.long find.word
Two transformations you must apply yourself, and forgetting either produces confident nonsense:
| Physical, not virtual |
peek/poke do not go through the MMU. A KSEG0 pointer needs - 0x80000000; anything else needs mmu.translate first |
| Unmunged | a guest word at A is at physical A ^ 4; a byte at A is at A ^ 7
|
So reading the guest's word at 0x54748:
machine.memory.peek.l(0x5474c)
Inspection is side-effect-free and crash-proof: peeking an unmapped address returns all-ones rather than faulting the guest or killing the daemon.
debug.breakpoints.add 0x80679eac
debug.breakpoints.add 0x300 "machine.cpu.dar == 0xEE315C9C" "logical"
debug.breakpoints.clear
The second form — a condition — is the single most valuable feature here. A boot takes thousands of page faults; a conditional breakpoint on the data-fault vector fires only for the one address you care about. That is how wall 49 was found.
Two behaviours to know:
- Breakpoints report after the instruction executes. A breakpoint on a faulting load never fires for that fault — the exception is taken first.
- A condition can match transiently. A register may briefly hold your value before the instruction you meant. Confirm with a second field.
Watchers that do not stop the machine:
debug.logpoints.add addr=0xE6E84324 width=l mode=write level=1 \
message="wrote ${$value} from pc=${machine.cpu.pc}"
debug.log "memory" 1
mode is pc (default), read, write or rw. The message is a fire-time template —
evaluated on every hit, with $value, $addr and $size bound.
Two caveats worth stating, both learned here: memory logpoints hook the CPU access path, so they do not see DMA; and their output goes to the log stream, which for a daemon means its own stdout, not necessarily your client.
scheduler.run # until a breakpoint or a stop
scheduler.run 20000000 # bounded, in instructions
scheduler.stop
Long runs emit a heartbeat per second. A run inside a shell loop is one statement to the daemon,
so a second connection cannot interrupt it normally — send stop on a fresh connection instead.
Execution is deterministic. Same checkpoint, same commands, same result — bit-identical, out to hundreds of millions of instructions. That is worth more than it sounds: you can add instrumentation to a run that already failed and be confident you are looking at the same failure. If you ever see apparent non-determinism, suspect the harness — a torn image, a killed daemon — before the core.
machine.screen.save "shot.png"
machine.screen.checksum
screen.save fails outright if no framebuffer is up yet, and a failed statement aborts the
script. In a boot script, gate it on something that proves the console exists:
if contains($out, "54M30 console") { machine.screen.save "shot.png" }
That one cost a whole run.
The model is self-describing. Rather than guessing at a path:
objects
attributes "machine.cpu"
methods "debug.breakpoints"
help machine.screen.save
Every claim on this page was checked that way rather than remembered.
- Debugging recipes — these primitives, assembled into procedures.
- When your instrumentation lies — before you trust any of it.
Corrections welcome — this wiki is edited directly, so nothing here has had a review. Repository · STORY.md · GPL-2.0-only
Start here
Theory
- Why NT on a Power Mac is hard
- Open Firmware
- ARC
- The veneer
- The NT boot chain
- The HAL contract
- The NT PowerPC ABI
- Little-endian PowerPC
- How Setup chooses a HAL
- The NT video stack
Machines
Emulator
- Getting Granny Smith
- Media you must supply
- Building the HAL
- Making an OEM CD
- Preparing disks
- Running text-mode Setup
- Capturing the installed image
- Booting the installed system
- Iterating on the HAL
- Checkpoints and deltas
Real hardware
Debugging
- The emulator shell
- Reading NT binaries
- Decoding a bugcheck
- When your instrumentation lies
- Debugging recipes
Reference
- HAL exports
- ARC environment variables
- The veneer's VrDebug bitmask
- Veneer patch catalogue
- Address and interrupt map
- Error codes seen
Project