From fa577eaa83a1e41118046a1cb5335da240f002fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 7 Sep 2026 13:28:17 -0600 Subject: [PATCH] Document --re-module in AGENTS.md and docs/reverse-engineering.md A tool nobody knows about is a tool nobody uses. AGENTS.md gets a short section pointing at it, plus the two things most likely to be got wrong when reading the output: that a function's arity can't be inferred from the registers it reads, since MIPS code passes arguments through untouched, and that a finding is worth much more when the comment says which module it came from. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 22 ++++++++++ docs/reverse-engineering.md | 81 +++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 docs/reverse-engineering.md diff --git a/AGENTS.md b/AGENTS.md index 597bf8fcd7..1be91b998c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,7 @@ for it: | [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md) | Writing a new pspautotest, and running it on a real PSP over PSPLink to record its `.expected` | | [docs/frametest.md](docs/frametest.md) | Framedump rendering tests | | [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) | WebSocket debugger protocol reference | +| [docs/reverse-engineering.md](docs/reverse-engineering.md) | Disassembling a firmware PRX with `--re-module`, to find out what the hardware actually does | ## General instructions @@ -136,6 +137,27 @@ UWP/PPSSPP_UWPMain.cpp android/jni/app-android.cpp libretro/libretro.cpp +## Reverse-engineering the firmware + +When a question about hardware behaviour can't be settled from the docs or from JPCSP - what a +field in a codec context means, what a library actually returns when a buffer runs dry - the +firmware itself can be read. `PPSSPPHeadless --re-module flash0:/kd/libmp3.prx --re-out DIR` +loads one PRX standalone and writes an annotated disassembly, the export/import tables with NIDs +resolved, and a call graph. It needs a firmware dump (`--memstick` pointing at one; PPSSPP can +unpack an updater itself with `--unpack-updater`). + +Full usage, and how to accumulate names in a `.ppsym` file so the disassembly stays readable: +[docs/reverse-engineering.md](docs/reverse-engineering.md). + +Two things to know before trusting what you read there: + +- **Don't infer a function's arity from the registers it reads.** MIPS code routinely leaves an + argument untouched for a callee to pick up, so a function that reads only `a0` may well take + three. The per-function register evidence block flags this as `FORWARDED`; follow the callees. +- **Record how you know.** A comment saying which module and function a fact came from is worth + more than the fact alone, since the next person can re-derive it. Behavioural findings belong + in the tree; bulk transcriptions of Sony's code do not. + ## Command-line parsing All command-line parsing for both the main app and headless builds belongs in `Core/CmdLine.cpp` / diff --git a/docs/reverse-engineering.md b/docs/reverse-engineering.md new file mode 100644 index 0000000000..001d54b237 --- /dev/null +++ b/docs/reverse-engineering.md @@ -0,0 +1,81 @@ +# Reverse-engineering a PSP firmware module + +`PPSSPPHeadless --re-module` loads a single PRX on its own - no game, no boot - and writes a report +about it: the module header, its exports and imports with NIDs resolved to names, one annotated +disassembly file per function, and the call graph. + +This is a developer tool for understanding the PSP, not something a user ever runs. + +## Running it + +```bash +./build/PPSSPPHeadless --memstick ~/.config/ppsspp \ + --re-module flash0:/kd/libmp3.prx \ + --re-out /tmp/re +``` + +`--re-module` takes either a host path or a PSP-style `flash0:/kd/foo.prx`, which is resolved +against the configured NAND directory - so `--memstick` (or `--nand`) has to point at a dump. +PPSSPP can produce one itself from a firmware updater with `--unpack-updater`; see +[PsarFileFormat.md](PsarFileFormat.md). + +| Option | | +|---|---| +| `--re-module PATH` | The module to load. Host path, or `flash0:/kd/foo.prx`. | +| `--re-out DIR` | Where the report goes. Created if missing; defaults to `re-out`. | +| `--re-func NAME` | Only disassemble this one function, by name or `0x08801234`. | +| `--re-syms FILE` | A `.ppsym` file of names to apply first, so the disassembly reads properly. | + +## What comes out + +- `.index.md` - header, segments, export table (library, NID, address, name), import table, + and every function with its size, caller/callee counts and whether it writes `v0`. +- `/_.asm` - one file per function. +- `.xref.json` - the call graph, for asking "who calls this" without grepping. + +Two things in the disassembly are worth knowing about: + +**`lui`/`addiu` pairs are folded** and reported as the address they form, with a symbol name where +one is known (`; = 08004a10 `). Every global and constant table is reached through +such a pair, so this is usually how you find the data a function works on. + +**Each function gets a register evidence block rather than a guessed signature.** It reports, for +`a0`-`a3`, whether each was read before being written, written before being read, never touched, or +- the interesting case - *never read but still live across a call*: + +``` +; a0 READ before written -> used as a parameter here +; a1 never read, but live across a call -> FORWARDED from our caller +``` + +That last one matters because MIPS code routinely takes an argument it never touches and leaves it +in place for a callee to pick up. A tool that inferred "this function takes one argument" from the +reads alone would be wrong, and so would you. Treat `FORWARDED` as evidence that the real arity is +larger than what is read here, and settle it by looking at what the callees do with the register. + +## Naming things + +The loader's scan names every function it finds `z_un_
`, which makes for unreadable +disassembly. The tool names what it can automatically - exported functions get their name from the +NID via PPSSPP's own HLE tables, and every import stub is named after the function it resolves to - +but the rest is up to you. + +Accumulated names go in a `.ppsym` file, the same module-relative format the emulator saves from +`hle.module.saveSymbols` and the ImDebugger, and are applied with `--re-syms`. Names compound: once +a function is named, every call site that reaches it reads as that name, so later functions get +progressively cheaper to work out. + +Those files are keyed by module name and crc32 (`PSP/SYSTEM/SYMBOLS/_.ppsym`), so a set +of names only ever attaches to the exact build of the module it was written against. The report's +header line prints the crc to match. + +## Notes and caveats + +- Modules load at a fixed base (`0x08000000` for kernel modules), and the index prints a `+offset` + column, so addresses can be compared against another tool's view of the same module. +- Some modules - `sysmem.prx` and `loadcore.prx` among them - have `modinfo` pointers that are file + offsets rather than addresses, so the loader's own function scan finds nothing in them. The tool + falls back to scanning the module's text range directly. Those modules are hand-written assembly + without standard prologues, so expect few, large "functions". +- Nothing is executed. The tool brings up the memory map, timing, the HLE tables and the kernel + allocators, then runs the real module loader and stops.