Skip to content

Debugging The emulator shell

pappadf edited this page Sep 14, 2026 · 1 revision

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.


1. Connecting

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.


2. The grammar, in ten lines

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.pc line is silent in a script. Wrap it in echo.
  • $ on every read of a binding. let x = 1 then $x.
  • The first failed statement aborts the script. Probe expected failures with try(EXPR, none) == none.

3. Registers

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


4. Memory

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.


5. Breakpoints

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.

6. Logpoints

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.


7. Running

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.


8. Screen

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.


9. Introspection

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.


Next

Clone this wiki locally