Skip to content

The Ghidra project

The Ghidra project is where findings live. A discovery that only ever makes it into a report is one the next session will do again from scratch.

Where things are

Project firmware/iPodClassic.gpr, program osos-dec.dfu
Scripts firmware/ghidra_scripts_project/
ASM dump firmware/claudedump/asm_current/

One file per function, asm_current/220cfc74.s, with an INDEX.txt and a functions.txt mapping address to name. The dump annotates string arguments (PARAM -> s_Foo) and branch targets (-> FUN_xxxx), so grep-by-string and grep-by-callee both work on the disassembly. Ghidra ships prebuilt decompiler natives for Apple silicon, so nothing needs building.

The bulk C dump is gone

Deleted on 2026-08-16, deliberately. The decompilation reads like an answer without being one, and what it hid here was substantial: nineteen separate *Hilited handlers inlined into a single switch with the jump table swallowed, tail calls rendered as ninety-line bodies, adjustor thunks that resolve to "no function" until the branch is decoded, a divisor dropped out of a scale, and a set of colours that exist only as stack byte stores and never appear in the C at all.

What survives is one function's C, on demand, which is worth it for the shape the ARM makes expensive to read by hand - a forty-case property getter, say:

cd firmware && <ghidra>/support/analyzeHeadless . iPodClassic -process osos-dec.dfu -noanalysis \
    -scriptPath ghidra_scripts_project -postScript DumpOneFunctionC 0x221ddf74

Anything it says about a number, a call target or a register-derived value still gets checked in the .s before it reaches code.

Refresh the dump first

Not at the end of the session. First.

cd firmware && <ghidra>/support/analyzeHeadless . iPodClassic \
    -process osos-dec.dfu -noanalysis -scriptPath ghidra_scripts_project \
    -postScript DumpAllFunctionsAsm claudedump/asm_current

The dump is a snapshot, and it has sat at 192 named functions while the project held 457. Script output arrives on stderr, prefixed INFO <Script>.java>.

Read the ARM whenever a call's target matters

The decompiler renders an indirect call as a plain one, which has caught this project out more than once:

  • a display function's panel write is ldr ip,[r0,#0xc] ; blx ip through a descriptor slot;
  • a gate dispatches through two vtable slots;
  • a frame count comes from TABLE + 4 rather than header + 4, because r1 is reloaded midway.

In each case the C read as a settled answer while hiding the question.

Findings go back in through a script

Apply what was learned with an Apply*Findings.java script, then re-dump so the next read sees the new names. Do it when a finding is established rather than at the end of a thread, because the end of a thread is where findings get forgotten.

Before disassembling anything that looks like infrastructure

Grep the project's symbols and the reports first. At least one address has been "discovered" twice, having been named and written up the first time.

Two things worth knowing about this particular image

A method reached only through a vtable may have no function at all. It is missing from the dump and reads as unreachable code. Dump the vtable first - the object's vptr, then the slot - and if the target has no function, create it with an Apply*Findings script and re-dump. Creating one can land a zero-length body when the bytes were never disassembled; the order that works is remove, clear, disassemble, create. Hand-disassembling with objdump is the signal that this step was skipped.

A name can be attached to the wrong function. getFunctionContaining will happily return an unrelated function when the address you want has no function defined at it. Check getFunctionAt first, or you leave a wrong name in the shared project, which is worse than no name.

Semihosting is instrumented and then thrown away. RetailOS instruments roughly 131 call sites with SVC 0x123456 (ARM semihosting) and the retail handler is a stub (mov r0,#0 ; bx lr), which discards all of it. Replacement firmware that implements that handler inherits the convention, and OpenOCD semihosting, on day one.