mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Merge pull request #22264 from hrydgard/re-module-dump
Module dumper for reverse engineering
This commit is contained in:
14 files changed
+902
-5
No files matched your search
@@ -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` /
|
||||
|
||||
@@ -1297,6 +1297,8 @@ if(HEADLESS)
|
||||
headless/Headless.cpp
|
||||
headless/Compare.cpp
|
||||
headless/Compare.h
|
||||
headless/ReverseEngineer.cpp
|
||||
headless/ReverseEngineer.h
|
||||
)
|
||||
if(SDL_LIB_TARGET)
|
||||
list(APPEND HeadlessSource
|
||||
|
||||
@@ -212,6 +212,10 @@ static const CommandLineParam g_autoParams[] = {
|
||||
{POFF(unpackUpdaterModel), CmdParamType::String, "unpack-updater-model", '\0', "PSP model to unpack for (01g..12g, default any)", CmdLineMode::Headless},
|
||||
{POFF(unpackUpdaterFilter), CmdParamType::String, "unpack-updater-filter", '\0', "Only unpack entries under this path, e.g. flash0:/font/", CmdLineMode::Headless},
|
||||
{POFF(installPkg), CmdParamType::String, "install-pkg", '\0', "Install the game update in a .pkg into DIR and exit", CmdLineMode::Headless},
|
||||
{POFF(reModule), CmdParamType::String, "re-module", '\0', "Load one PRX standalone and write a reverse-engineering report, then exit", CmdLineMode::Headless},
|
||||
{POFF(reOut), CmdParamType::String, "re-out", '\0', "Directory for --re-module output (default: re-out)", CmdLineMode::Headless},
|
||||
{POFF(reFunc), CmdParamType::String, "re-func", '\0', "Only disassemble this function of --re-module, by name or address", CmdLineMode::Headless},
|
||||
{POFF(reSyms), CmdParamType::String, "re-syms", '\0', "Apply a .ppsym file of known names before dumping --re-module", CmdLineMode::Headless},
|
||||
{POFF(odsLog), CmdParamType::Bool, "odslog", 'o', "Also log through OutputDebugString (Windows)", CmdLineMode::Headless},
|
||||
{POFF(generateInterpreterDispatch), CmdParamType::Bool, "generate-interpreter-dispatch", '\0', "Generate C++ interpreter dispatch code (ExecInstruction) to stdout and exit", CmdLineMode::Headless},
|
||||
{POFF(resolutionScale), CmdParamType::Int, "resolution-scale", '\0', "Set the resolution scale factor"},
|
||||
|
||||
@@ -88,6 +88,18 @@ struct CommandLineOptions {
|
||||
// Core/Util/PkgUnpack.h.
|
||||
std::optional<std::string> installPkg;
|
||||
|
||||
// Headless: load one PRX standalone (no game) and write a reverse-engineering report -
|
||||
// module header, exports/imports, per-function disassembly and a call graph - then exit.
|
||||
// See headless/ReverseEngineer.cpp.
|
||||
std::optional<std::string> reModule;
|
||||
// Where the report goes. Defaults to "re-out" next to the current directory.
|
||||
std::optional<std::string> reOut;
|
||||
// Only disassemble this one function, by name or "0x08801234".
|
||||
std::optional<std::string> reFunc;
|
||||
// A .ppsym file of already-known names to apply before dumping, so the disassembly comes
|
||||
// out readable. Module-relative, same format the emulator saves.
|
||||
std::optional<std::string> reSyms;
|
||||
|
||||
std::optional<int> memReadAction;
|
||||
std::optional<int> memWriteAction;
|
||||
std::optional<int> breakAction;
|
||||
|
||||
@@ -247,7 +247,20 @@ DisableHLEFlags GetEffectiveDisableHLEFlags() {
|
||||
}
|
||||
|
||||
// Note: name is the modname from prx, not the export module name!
|
||||
// See SetForceRealModuleLoads.
|
||||
static bool g_forceRealModuleLoads = false;
|
||||
|
||||
void SetForceRealModuleLoads(bool force) {
|
||||
g_forceRealModuleLoads = force;
|
||||
}
|
||||
|
||||
bool ShouldHLEModule(std::string_view modname, bool *wasDisabledManually) {
|
||||
if (g_forceRealModuleLoads) {
|
||||
if (wasDisabledManually) {
|
||||
*wasDisabledManually = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (wasDisabledManually) {
|
||||
*wasDisabledManually = false;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,13 @@ struct HLEModuleMeta {
|
||||
const HLEModuleMeta *GetHLEModuleMetaByFlag(DisableHLEFlags flag);
|
||||
const HLEModuleMeta *GetHLEModuleMeta(std::string_view modname);
|
||||
bool ShouldHLEModule(std::string_view modname, bool *wasDisabledManually = nullptr);
|
||||
// When set, ShouldHLEModule always says no, so every module genuinely loads and runs Sony's
|
||||
// code. Needed by the headless reverse-engineering dump (headless/ReverseEngineer.cpp), which
|
||||
// exists precisely to look at the real thing - including modules like sceAudiocodec_Driver that
|
||||
// have no DisableHLEFlags bit of their own and so can't be turned off the normal way.
|
||||
// Only affects loading; imports still resolve through the HLE tables, which is what gives
|
||||
// imported functions their names.
|
||||
void SetForceRealModuleLoads(bool force);
|
||||
bool ShouldHLEModuleByImportName(std::string_view importModuleName);
|
||||
|
||||
// May return nullptr
|
||||
|
||||
@@ -1695,16 +1695,29 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
|
||||
u32 scanEnd = module->textEnd;
|
||||
|
||||
if (Memory::IsValid4AlignedRange(scanStart, scanEnd - scanStart)) {
|
||||
// libent/libstub come from the module's own header, and nothing has checked that
|
||||
// they land inside the range validated just above. flash0:/kd/sysmem.prx and
|
||||
// loadcore.prx from a real firmware dump put them tens of megabytes past the end
|
||||
// of the text. So let's clamp.
|
||||
const u32 textStart = scanStart;
|
||||
auto clampToText = [textStart, scanEnd](u32 addr) {
|
||||
return std::min(std::max(addr, textStart), scanEnd);
|
||||
};
|
||||
auto scanRange = [&insertSymbols](u32 from, u32 to) {
|
||||
if (from < to) {
|
||||
insertSymbols = MIPSAnalyst::ScanForFunctions(from, to, insertSymbols);
|
||||
}
|
||||
};
|
||||
// Skip the exports and imports sections, they're not code.
|
||||
if (scanEnd >= std::min(modinfo->libent, modinfo->libstub)) {
|
||||
insertSymbols = MIPSAnalyst::ScanForFunctions(scanStart, std::min(modinfo->libent, modinfo->libstub), insertSymbols);
|
||||
scanStart = std::min(modinfo->libentend, modinfo->libstubend);
|
||||
scanRange(scanStart, clampToText(std::min(modinfo->libent, modinfo->libstub)));
|
||||
scanStart = clampToText(std::min(modinfo->libentend, modinfo->libstubend));
|
||||
}
|
||||
if (scanEnd >= std::max(modinfo->libent, modinfo->libstub)) {
|
||||
insertSymbols = MIPSAnalyst::ScanForFunctions(scanStart, std::max(modinfo->libent, modinfo->libstub), insertSymbols);
|
||||
scanStart = std::max(modinfo->libentend, modinfo->libstubend);
|
||||
scanRange(scanStart, clampToText(std::max(modinfo->libent, modinfo->libstub)));
|
||||
scanStart = clampToText(std::max(modinfo->libentend, modinfo->libstubend));
|
||||
}
|
||||
insertSymbols = MIPSAnalyst::ScanForFunctions(scanStart, scanEnd, insertSymbols);
|
||||
scanRange(scanStart, scanEnd);
|
||||
} else {
|
||||
ERROR_LOG(Log::Loader, "Bad text scan range %08x-%08x", scanStart, scanEnd);
|
||||
}
|
||||
|
||||
@@ -1017,6 +1017,7 @@ ifeq ($(HEADLESS),1)
|
||||
LOCAL_MODULE := ppsspp_headless
|
||||
LOCAL_SRC_FILES := \
|
||||
$(SRC)/headless/Headless.cpp \
|
||||
$(SRC)/headless/ReverseEngineer.cpp \
|
||||
$(SRC)/headless/Compare.cpp
|
||||
|
||||
include $(BUILD_EXECUTABLE)
|
||||
|
||||
@@ -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
|
||||
|
||||
- `<module>.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`.
|
||||
- `<module>/<addr>_<name>.asm` - one file per function.
|
||||
- `<module>.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 <sampleRateTable>`). 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_<address>`, 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/<name>_<crc>.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.
|
||||
@@ -59,6 +59,7 @@
|
||||
#include "Core/System.h"
|
||||
#include "Core/Util/PSARUnpack.h"
|
||||
#include "Core/Util/PkgUnpack.h"
|
||||
#include "headless/ReverseEngineer.h"
|
||||
#include "Core/WebServer.h"
|
||||
#include "Core/HLE/sceUtility.h"
|
||||
#include "Core/SaveState.h"
|
||||
@@ -860,6 +861,18 @@ int main(int argc, const char* argv[]) {
|
||||
}
|
||||
g_Config.nandRootDirectory = GetSysDirectory(DIRECTORY_NAND);
|
||||
coreParameter.nandRoot = g_Config.nandRootDirectory;
|
||||
// Placed here rather than with the other early-exit subcommands above, because resolving a
|
||||
// "flash0:/kd/foo.prx" module path needs nandRootDirectory, which is only settled just above.
|
||||
if (cmdLineOptions.reModule.has_value()) {
|
||||
ReverseEngineerOptions reOptions;
|
||||
reOptions.modulePath = cmdLineOptions.reModule.value();
|
||||
reOptions.outDir = cmdLineOptions.reOut.value_or("re-out");
|
||||
reOptions.funcFilter = cmdLineOptions.reFunc.value_or("");
|
||||
reOptions.symsFile = cmdLineOptions.reSyms.value_or("");
|
||||
reOptions.verbose = testOptions.verbose;
|
||||
return RunReverseEngineer(reOptions);
|
||||
}
|
||||
|
||||
|
||||
// Try to find the assets flash0 directory. Often this is from a subdirectory.
|
||||
// This is needed for our fallback fonts.
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Windows\W32Util\Misc.cpp" />
|
||||
<ClCompile Include="Compare.cpp" />
|
||||
<ClCompile Include="ReverseEngineer.cpp" />
|
||||
<ClCompile Include="Headless.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
@@ -416,6 +417,7 @@
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Compare.h" />
|
||||
<ClInclude Include="ReverseEngineer.h" />
|
||||
<ClInclude Include="SDLHeadlessGLGraphicsContext.h">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Headless.cpp" />
|
||||
<ClCompile Include="Compare.cpp" />
|
||||
<ClCompile Include="ReverseEngineer.cpp" />
|
||||
<ClCompile Include="..\ext\glew\glew.c" />
|
||||
<ClCompile Include="..\Windows\GPU\WindowsGLContext.cpp">
|
||||
<Filter>Windows</Filter>
|
||||
@@ -35,6 +36,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Compare.h" />
|
||||
<ClInclude Include="ReverseEngineer.h" />
|
||||
<ClInclude Include="WindowsHeadlessHost.h">
|
||||
<Filter>Windows</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -0,0 +1,688 @@
|
||||
// Copyright (c) 2026- PPSSPP Project.
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
// Static reverse-engineering dump for a single PRX, with no game running.
|
||||
//
|
||||
// Brings up just enough of the emulator to run the real module loader (decrypt, decompress,
|
||||
// relocate, resolve imports/exports, scan for functions), then writes a report: an index of
|
||||
// the module's exports/imports/functions, one annotated disassembly file per function, and a
|
||||
// call graph.
|
||||
//
|
||||
// Deliberately reuses the emulator's own loader rather than parsing PRXes a second time - a
|
||||
// separate parser would drift from the one that actually runs.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "Common/File/Path.h"
|
||||
#include "Common/StringUtils.h"
|
||||
|
||||
#include "Core/Config.h"
|
||||
#include "Core/CoreTiming.h"
|
||||
#include "Core/Debugger/DebugInterface.h"
|
||||
#include "Core/Debugger/DisassemblyManager.h"
|
||||
#include "Core/Debugger/SymbolMap.h"
|
||||
#include "Core/FileSystems/DirectoryFileSystem.h"
|
||||
#include "Core/FileSystems/MetaFileSystem.h"
|
||||
#include "Core/HLE/HLE.h"
|
||||
#include "Core/HLE/sceKernel.h"
|
||||
#include "Core/HLE/sceKernelMemory.h"
|
||||
#include "Core/HLE/sceKernelModule.h"
|
||||
#include "Core/MIPS/MIPS.h"
|
||||
#include "Core/MIPS/MIPSDebugInterface.h"
|
||||
#include "Core/MIPS/MIPSAnalyst.h"
|
||||
#include "Core/MIPS/MIPSTables.h"
|
||||
#include "Core/MemMap.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
#include "headless/ReverseEngineer.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// MIPS register names, in encoding order.
|
||||
const char *const kRegNames[32] = {
|
||||
"zero", "at", "v0", "v1", "a0", "a1", "a2", "a3",
|
||||
"t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7",
|
||||
"s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7",
|
||||
"t8", "t9", "k0", "k1", "gp", "sp", "fp", "ra",
|
||||
};
|
||||
|
||||
enum {
|
||||
REG_V0 = 2,
|
||||
REG_A0 = 4,
|
||||
REG_A3 = 7,
|
||||
REG_SP = 29,
|
||||
REG_RA = 31,
|
||||
};
|
||||
|
||||
// What one function does with the registers it was handed. We report evidence rather than a
|
||||
// signature: MIPS callers routinely leave an argument in place for a callee to pick up, so a
|
||||
// register this function never touches can still be a parameter it is passing on. Deciding the
|
||||
// real arity means looking at the whole call chain, which is a judgement call for a human, not
|
||||
// something to guess here.
|
||||
struct RegEvidence {
|
||||
bool readBeforeWritten[32] = {};
|
||||
bool written[32] = {};
|
||||
// Call sites where an argument register still held whatever the caller left in it.
|
||||
std::map<int, std::vector<u32>> forwardedAt;
|
||||
// Call sites where this function set the argument register itself.
|
||||
std::map<int, std::vector<u32>> setLocallyAt;
|
||||
bool writesV0 = false;
|
||||
bool hasCalls = false;
|
||||
bool hasIndirectCalls = false;
|
||||
};
|
||||
|
||||
struct FuncInfo {
|
||||
u32 start = 0;
|
||||
u32 size = 0;
|
||||
std::string name;
|
||||
std::set<u32> callees;
|
||||
std::set<u32> callers;
|
||||
bool indirectCallees = false;
|
||||
RegEvidence regs;
|
||||
};
|
||||
|
||||
bool IsJal(u32 op) {
|
||||
return (op >> 26) == 3;
|
||||
}
|
||||
|
||||
bool IsJalr(u32 op) {
|
||||
return (op >> 26) == 0 && (op & 0x3f) == 9;
|
||||
}
|
||||
|
||||
u32 JumpTarget(u32 addr, u32 op) {
|
||||
return (addr & 0xF0000000) | ((op & 0x03FFFFFF) << 2);
|
||||
}
|
||||
|
||||
// Applies one instruction's register reads and writes to the evidence being accumulated.
|
||||
void ApplyRegEffects(u32 op, RegEvidence *ev) {
|
||||
const MIPSInfo info = MIPSGetInfo(MIPSOpcode(op));
|
||||
const int rs = (op >> 21) & 0x1f;
|
||||
const int rt = (op >> 16) & 0x1f;
|
||||
const int rd = (op >> 11) & 0x1f;
|
||||
|
||||
if (info & IN_RS) {
|
||||
if (!ev->written[rs]) {
|
||||
ev->readBeforeWritten[rs] = true;
|
||||
}
|
||||
}
|
||||
if (info & IN_RT) {
|
||||
if (!ev->written[rt]) {
|
||||
ev->readBeforeWritten[rt] = true;
|
||||
}
|
||||
}
|
||||
if (info & OUT_RT) {
|
||||
ev->written[rt] = true;
|
||||
}
|
||||
if (info & OUT_RD) {
|
||||
ev->written[rd] = true;
|
||||
}
|
||||
if (info & OUT_RA) {
|
||||
ev->written[REG_RA] = true;
|
||||
}
|
||||
if (((info & OUT_RT) && rt == REG_V0) || ((info & OUT_RD) && rd == REG_V0)) {
|
||||
ev->writesV0 = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Walks a function once, collecting the call graph and the register evidence.
|
||||
//
|
||||
// Delay slots matter here: the instruction after a jal executes *before* the call, so a
|
||||
// "move a0, s0" sitting in the delay slot is setting up that call's argument, not the next
|
||||
// one's. Getting this backwards would report an argument as forwarded when it was set locally.
|
||||
void AnalyzeFunction(FuncInfo *func) {
|
||||
RegEvidence &ev = func->regs;
|
||||
const u32 end = func->start + func->size;
|
||||
|
||||
for (u32 addr = func->start; addr < end; addr += 4) {
|
||||
if (!Memory::IsValidAddress(addr)) {
|
||||
break;
|
||||
}
|
||||
const u32 op = Memory::Read_Instruction(addr).encoding;
|
||||
const bool jal = IsJal(op);
|
||||
const bool jalr = IsJalr(op);
|
||||
|
||||
if (!jal && !jalr) {
|
||||
ApplyRegEffects(op, &ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A call. The delay slot runs first.
|
||||
const u32 delayAddr = addr + 4;
|
||||
if (delayAddr < end && Memory::IsValidAddress(delayAddr)) {
|
||||
ApplyRegEffects(Memory::Read_Instruction(delayAddr).encoding, &ev);
|
||||
}
|
||||
|
||||
ev.hasCalls = true;
|
||||
for (int reg = REG_A0; reg <= REG_A3; reg++) {
|
||||
if (ev.written[reg]) {
|
||||
ev.setLocallyAt[reg].push_back(addr);
|
||||
} else {
|
||||
ev.forwardedAt[reg].push_back(addr);
|
||||
}
|
||||
}
|
||||
|
||||
if (jal) {
|
||||
func->callees.insert(JumpTarget(addr, op));
|
||||
} else {
|
||||
ev.hasIndirectCalls = true;
|
||||
func->indirectCallees = true;
|
||||
}
|
||||
ApplyRegEffects(op, &ev);
|
||||
// v0 is clobbered by the call, and a0-a3 are caller-saved - but for our purposes the
|
||||
// interesting question is only what the caller left in place, so nothing to reset.
|
||||
addr += 4; // Skip the delay slot; already accounted for.
|
||||
}
|
||||
}
|
||||
|
||||
// Names an address formed by a lui/addiu pair, if anything is known about it.
|
||||
std::string DescribeAddr(u32 addr) {
|
||||
const std::string label = g_symbolMap->GetLabelString(addr);
|
||||
if (!label.empty()) {
|
||||
return " <" + label + ">";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string SanitizeForFilename(std::string_view name) {
|
||||
std::string out;
|
||||
out.reserve(name.size());
|
||||
for (char c : name) {
|
||||
if (isalnum((unsigned char)c) || c == '_' || c == '.' || c == '-') {
|
||||
out.push_back(c);
|
||||
} else {
|
||||
out.push_back('_');
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Brings up the minimum needed to load a module: memory map, timing, HLE tables (so imports
|
||||
// resolve to named syscalls), the kernel memory allocators and the object pool. Notably this
|
||||
// does *not* start threads, the GPU or any of the HLE subsystems - nothing is going to run.
|
||||
bool InitMinimalPSP() {
|
||||
Memory::g_MemorySize = Memory::RAM_DOUBLE_SIZE;
|
||||
Memory::g_PSPModel = PSP_MODEL_SLIM;
|
||||
|
||||
if (g_symbolMap) {
|
||||
delete g_symbolMap;
|
||||
}
|
||||
g_symbolMap = new SymbolMap();
|
||||
MIPSAnalyst::Reset();
|
||||
|
||||
if (!Memory::Init(Memory::MemMapSetupFlags::Default)) {
|
||||
fprintf(stderr, "re: memory init failed\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
mipsr4k.Reset();
|
||||
CoreTiming::Init(&mipsr4k);
|
||||
HLEInit();
|
||||
kernelObjects.Clear();
|
||||
__KernelMemoryInit();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShutdownMinimalPSP() {
|
||||
__KernelMemoryShutdown();
|
||||
kernelObjects.Clear();
|
||||
HLEShutdown();
|
||||
CoreTiming::Shutdown();
|
||||
Memory::Shutdown();
|
||||
delete g_symbolMap;
|
||||
g_symbolMap = nullptr;
|
||||
}
|
||||
|
||||
// Accepts either a host path or a PSP-style "flash0:/kd/foo.prx", which is resolved against the
|
||||
// configured NAND directory so firmware modules can be named the way they are on the PSP.
|
||||
Path ResolveModulePath(const std::string &input) {
|
||||
if (startsWithNoCase(input, "flash0:/") || startsWithNoCase(input, "flash0:")) {
|
||||
std::string rest = input.substr(input.find(':') + 1);
|
||||
while (!rest.empty() && (rest[0] == '/' || rest[0] == '\\')) {
|
||||
rest = rest.substr(1);
|
||||
}
|
||||
return g_Config.nandRootDirectory / "flash0" / rest;
|
||||
}
|
||||
return Path(input);
|
||||
}
|
||||
|
||||
void WriteRegEvidence(FILE *f, const FuncInfo &func) {
|
||||
const RegEvidence &ev = func.regs;
|
||||
fprintf(f, "; Register evidence (NOT a signature - see below):\n");
|
||||
for (int reg = REG_A0; reg <= REG_A3; reg++) {
|
||||
const char *name = kRegNames[reg];
|
||||
std::string verdict;
|
||||
if (ev.readBeforeWritten[reg]) {
|
||||
verdict = "READ before written -> used as a parameter here";
|
||||
} else if (!ev.forwardedAt.count(reg) && !ev.written[reg]) {
|
||||
verdict = "never touched";
|
||||
} else if (!ev.readBeforeWritten[reg] && ev.forwardedAt.count(reg)) {
|
||||
verdict = "never read, but live across a call -> FORWARDED from our caller";
|
||||
} else if (ev.written[reg]) {
|
||||
verdict = "written before any read -> set up locally";
|
||||
} else {
|
||||
verdict = "unclear";
|
||||
}
|
||||
fprintf(f, "; %-4s %s\n", name, verdict.c_str());
|
||||
|
||||
auto fwd = ev.forwardedAt.find(reg);
|
||||
if (fwd != ev.forwardedAt.end() && !fwd->second.empty()) {
|
||||
fprintf(f, "; forwarded at:");
|
||||
for (size_t i = 0; i < fwd->second.size() && i < 8; i++) {
|
||||
fprintf(f, " %08x", fwd->second[i]);
|
||||
}
|
||||
if (fwd->second.size() > 8) {
|
||||
fprintf(f, " (+%d more)", (int)(fwd->second.size() - 8));
|
||||
}
|
||||
fprintf(f, "\n");
|
||||
}
|
||||
}
|
||||
fprintf(f, "; %-4s %s\n", "v0", ev.writesV0 ? "written -> returns a value" : "never written -> returns nothing");
|
||||
if (ev.hasIndirectCalls) {
|
||||
fprintf(f, "; note: has indirect calls (jalr) - callee list is incomplete\n");
|
||||
}
|
||||
fprintf(f, ";\n");
|
||||
fprintf(f, "; A register this function never reads can still be a parameter: MIPS code often\n");
|
||||
fprintf(f, "; leaves an argument untouched for a callee to pick up. Treat 'FORWARDED' as\n");
|
||||
fprintf(f, "; evidence the real arity is larger than what is read here, and settle it by\n");
|
||||
fprintf(f, "; looking at what the callees do with it.\n");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int RunReverseEngineer(const ReverseEngineerOptions &opts) {
|
||||
const Path modulePath = ResolveModulePath(opts.modulePath);
|
||||
if (!File::Exists(modulePath)) {
|
||||
fprintf(stderr, "re: no such file: %s\n", modulePath.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Path outDir(opts.outDir);
|
||||
if (!File::CreateFullPath(outDir)) {
|
||||
fprintf(stderr, "re: couldn't create output directory: %s\n", outDir.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Load every module for real, whatever HLE implementations we may have for it - the whole
|
||||
// point is to look at Sony's code, not to have it quietly replaced by ours.
|
||||
g_Config.iDisableHLE = -1;
|
||||
g_Config.iForceEnableHLE = 0;
|
||||
SetForceRealModuleLoads(true);
|
||||
g_Config.bAutoSaveLoadSymbols = false;
|
||||
|
||||
if (!InitMinimalPSP()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Mount the containing directory so the normal file-backed loader path can be used.
|
||||
const std::string dir = modulePath.GetDirectory();
|
||||
const std::string filename = modulePath.GetFilename();
|
||||
auto hostFs = std::make_shared<DirectoryFileSystem>(&pspFileSystem, Path(dir), FileSystemFlags::FLASH);
|
||||
pspFileSystem.Mount("host0:", hostFs);
|
||||
|
||||
std::string error;
|
||||
const SceUID uid = KernelLoadModule("host0:/" + filename, &error);
|
||||
if (uid < 0) {
|
||||
fprintf(stderr, "re: failed to load %s: %s\n", modulePath.c_str(), error.c_str());
|
||||
ShutdownMinimalPSP();
|
||||
return 1;
|
||||
}
|
||||
|
||||
u32 kerr = 0;
|
||||
PSPModule *module = kernelObjects.Get<PSPModule>(uid, kerr);
|
||||
if (!module) {
|
||||
fprintf(stderr, "re: loaded module vanished (uid %d)\n", uid);
|
||||
ShutdownMinimalPSP();
|
||||
return 1;
|
||||
}
|
||||
if (module->isFake) {
|
||||
fprintf(stderr, "re: module was fake-loaded (HLE stub) rather than really loaded - can't analyze\n");
|
||||
ShutdownMinimalPSP();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string moduleName = module->nm.name;
|
||||
const u32 base = module->memoryBlockAddr;
|
||||
const u32 blockSize = module->memoryBlockSize;
|
||||
|
||||
// The loader's function scan names everything z_un_<addr>. We know better for two whole
|
||||
// categories: exported functions have a NID the HLE tables can often name, and every import
|
||||
// stub is a known library function. Naming them here means every call site in the
|
||||
// disassembly below reads as a name instead of a bare address.
|
||||
const int moduleIdx = g_symbolMap->GetModuleIndexByName(moduleName);
|
||||
int namedExports = 0, namedImports = 0;
|
||||
for (const FuncSymbolExport &exp : module->exportedFuncs) {
|
||||
const char *known = GetHLEFuncName(exp.moduleName, exp.nid);
|
||||
const std::string name = known ? known : StringFromFormat("%s_%08x", exp.moduleName, exp.nid);
|
||||
u32 size = g_symbolMap->GetFunctionSize(exp.symAddr);
|
||||
if (size == SymbolMap::INVALID_ADDRESS) {
|
||||
size = 4;
|
||||
}
|
||||
g_symbolMap->AddFunction(name.c_str(), exp.symAddr, size, moduleIdx, true);
|
||||
namedExports++;
|
||||
}
|
||||
for (const FuncSymbolImport &imp : module->importedFuncs) {
|
||||
const char *known = GetHLEFuncName(imp.moduleName, imp.nid);
|
||||
const std::string name = known ? known : StringFromFormat("%s_%08x", imp.moduleName, imp.nid);
|
||||
g_symbolMap->AddFunction(name.c_str(), imp.stubAddr, 8, moduleIdx, true);
|
||||
namedImports++;
|
||||
}
|
||||
|
||||
// Optional pre-existing names, so the disassembly comes out readable instead of a wall of
|
||||
// z_un_08801234. Uses the same .ppsym format the emulator saves, module-relative. Applied
|
||||
// after the automatic naming above so a hand-written name always wins.
|
||||
if (!opts.symsFile.empty()) {
|
||||
if (moduleIdx < 0) {
|
||||
fprintf(stderr, "re: warning: module '%s' not in the symbol map, can't apply %s\n",
|
||||
moduleName.c_str(), opts.symsFile.c_str());
|
||||
} else if (!g_symbolMap->LoadModuleSymbols(moduleIdx, Path(opts.symsFile))) {
|
||||
fprintf(stderr, "re: warning: couldn't load symbols from %s\n", opts.symsFile.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// A few modules - sysmem.prx and loadcore.prx among them - carry modinfo pointers that are
|
||||
// file offsets rather than addresses, so the loader's own scan is left with nothing to look
|
||||
// at and finds no functions. For reverse engineering we would still like the disassembly, and
|
||||
// we know exactly which range is code, so scan it ourselves.
|
||||
{
|
||||
bool anyInModule = false;
|
||||
for (const SymbolEntry &sym : g_symbolMap->GetAllActiveSymbols(ST_FUNCTION)) {
|
||||
if (sym.address >= base && sym.address < base + blockSize) {
|
||||
anyInModule = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyInModule && blockSize >= 8) {
|
||||
const u32 textStart = module->nm.text_addr ? (u32)module->nm.text_addr : base;
|
||||
u32 textEnd = textStart + (u32)module->nm.text_size;
|
||||
if (textEnd <= textStart || textEnd > base + blockSize) {
|
||||
textEnd = base + blockSize;
|
||||
}
|
||||
if (Memory::IsValid4AlignedRange(textStart, textEnd - textStart)) {
|
||||
printf("re: loader found no functions, scanning %08x-%08x directly\n", textStart, textEnd);
|
||||
MIPSAnalyst::ScanForFunctions(textStart, textEnd - 4, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect the functions the loader's scan found, restricted to this module.
|
||||
std::vector<FuncInfo> funcs;
|
||||
std::map<u32, size_t> funcByAddr;
|
||||
for (const SymbolEntry &sym : g_symbolMap->GetAllActiveSymbols(ST_FUNCTION)) {
|
||||
if (sym.address < base || sym.address >= base + blockSize) {
|
||||
continue;
|
||||
}
|
||||
// A module with an odd segment layout can leave a symbol at an address that isn't
|
||||
// really code; reading an instruction there trips a debug assert deep in MemMap.
|
||||
if (!Memory::IsValid4AlignedAddress(sym.address) || sym.size == 0) {
|
||||
continue;
|
||||
}
|
||||
FuncInfo f;
|
||||
f.start = sym.address;
|
||||
f.size = sym.size;
|
||||
f.name = sym.name;
|
||||
funcByAddr[f.start] = funcs.size();
|
||||
funcs.push_back(f);
|
||||
}
|
||||
std::sort(funcs.begin(), funcs.end(), [](const FuncInfo &a, const FuncInfo &b) {
|
||||
return a.start < b.start;
|
||||
});
|
||||
funcByAddr.clear();
|
||||
for (size_t i = 0; i < funcs.size(); i++) {
|
||||
funcByAddr[funcs[i].start] = i;
|
||||
}
|
||||
|
||||
for (FuncInfo &f : funcs) {
|
||||
AnalyzeFunction(&f);
|
||||
}
|
||||
// Second pass: invert the call graph.
|
||||
for (const FuncInfo &f : funcs) {
|
||||
for (u32 callee : f.callees) {
|
||||
auto it = funcByAddr.find(callee);
|
||||
if (it != funcByAddr.end()) {
|
||||
funcs[it->second].callers.insert(f.start);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto nameOf = [&](u32 addr) -> std::string {
|
||||
auto it = funcByAddr.find(addr);
|
||||
if (it != funcByAddr.end()) {
|
||||
return funcs[it->second].name;
|
||||
}
|
||||
const std::string label = g_symbolMap->GetLabelString(addr);
|
||||
return label.empty() ? StringFromFormat("%08x", addr) : label;
|
||||
};
|
||||
|
||||
// ---- index ----
|
||||
const Path indexPath = outDir / (SanitizeForFilename(moduleName) + ".index.md");
|
||||
FILE *f = File::OpenCFile(indexPath, "w");
|
||||
if (!f) {
|
||||
fprintf(stderr, "re: couldn't write %s\n", indexPath.c_str());
|
||||
ShutdownMinimalPSP();
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(f, "# %s\n\n", moduleName.c_str());
|
||||
fprintf(f, "- file: `%s`\n", modulePath.GetFilename().c_str());
|
||||
fprintf(f, "- crc32: `%08x` (matches `PSP/SYSTEM/SYMBOLS/%s_%08x.ppsym`)\n", module->crc, moduleName.c_str(), module->crc);
|
||||
fprintf(f, "- attribute: `%04x`%s\n", (u32)module->nm.attribute,
|
||||
(module->nm.attribute & PSP_MODULE_KERNEL_MODE) ? " (kernel mode)" : "");
|
||||
fprintf(f, "- version: %d.%d\n", module->nm.version[1], module->nm.version[0]);
|
||||
fprintf(f, "- loaded at: `%08x`, size `%08x`\n", base, blockSize);
|
||||
fprintf(f, "- text: `%08x`..`%08x` data: `%x` bss: `%x` gp: `%08x`\n",
|
||||
(u32)module->nm.text_addr, (u32)module->nm.text_addr + (u32)module->nm.text_size,
|
||||
(u32)module->nm.data_size, (u32)module->nm.bss_size, (u32)module->nm.gp_value);
|
||||
fprintf(f, "- entry: `%08x` module_start: `%08x` module_stop: `%08x`\n\n",
|
||||
(u32)module->nm.entry_addr, (u32)module->nm.module_start_func, (u32)module->nm.module_stop_func);
|
||||
|
||||
fprintf(f, "## Segments\n\n| # | address | size |\n|---|---|---|\n");
|
||||
for (u32 i = 0; i < module->nm.nsegment && i < 4; i++) {
|
||||
fprintf(f, "| %d | `%08x` | `%x` |\n", i, (u32)module->nm.segmentaddr[i], (u32)module->nm.segmentsize[i]);
|
||||
}
|
||||
|
||||
fprintf(f, "\n## Exports (%d functions, %d variables)\n\n",
|
||||
(int)module->exportedFuncs.size(), (int)module->exportedVars.size());
|
||||
fprintf(f, "| library | NID | address | name |\n|---|---|---|---|\n");
|
||||
for (const FuncSymbolExport &exp : module->exportedFuncs) {
|
||||
const char *known = GetHLEFuncName(exp.moduleName, exp.nid);
|
||||
fprintf(f, "| `%s` | `%08x` | `%08x` | %s |\n", exp.moduleName, exp.nid, exp.symAddr,
|
||||
known ? known : nameOf(exp.symAddr).c_str());
|
||||
}
|
||||
for (const VarSymbolExport &exp : module->exportedVars) {
|
||||
fprintf(f, "| `%s` | `%08x` | `%08x` | *(variable)* |\n", exp.moduleName, exp.nid, exp.symAddr);
|
||||
}
|
||||
|
||||
fprintf(f, "\n## Imports (%d functions, %d variables)\n\n",
|
||||
(int)module->importedFuncs.size(), (int)module->importedVars.size());
|
||||
fprintf(f, "| library | NID | stub | name |\n|---|---|---|---|\n");
|
||||
for (const FuncSymbolImport &imp : module->importedFuncs) {
|
||||
const char *known = GetHLEFuncName(imp.moduleName, imp.nid);
|
||||
fprintf(f, "| `%s` | `%08x` | `%08x` | %s |\n", imp.moduleName, imp.nid, imp.stubAddr,
|
||||
known ? known : "*(unknown NID)*");
|
||||
}
|
||||
for (const VarSymbolImport &imp : module->importedVars) {
|
||||
fprintf(f, "| `%s` | `%08x` | `%08x` | *(variable)* |\n", imp.moduleName, imp.nid, imp.stubAddr);
|
||||
}
|
||||
|
||||
fprintf(f, "\n## Functions (%d)\n\n", (int)funcs.size());
|
||||
fprintf(f, "| address | +offset | size | callers | callees | v0 | name |\n|---|---|---|---|---|---|---|\n");
|
||||
for (const FuncInfo &fn : funcs) {
|
||||
fprintf(f, "| `%08x` | `+%05x` | %d | %d | %d | %s | %s |\n",
|
||||
fn.start, fn.start - base, fn.size, (int)fn.callers.size(), (int)fn.callees.size(),
|
||||
fn.regs.writesV0 ? "y" : "-", fn.name.c_str());
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
// ---- call graph ----
|
||||
const Path xrefPath = outDir / (SanitizeForFilename(moduleName) + ".xref.json");
|
||||
f = File::OpenCFile(xrefPath, "w");
|
||||
if (f) {
|
||||
fprintf(f, "{\n \"module\": \"%s\",\n \"base\": %u,\n \"functions\": [\n", moduleName.c_str(), base);
|
||||
for (size_t i = 0; i < funcs.size(); i++) {
|
||||
const FuncInfo &fn = funcs[i];
|
||||
fprintf(f, " {\"addr\": \"%08x\", \"name\": \"%s\", \"size\": %d, \"callers\": [",
|
||||
fn.start, fn.name.c_str(), fn.size);
|
||||
bool first = true;
|
||||
for (u32 c : fn.callers) {
|
||||
fprintf(f, "%s\"%08x\"", first ? "" : ", ", c);
|
||||
first = false;
|
||||
}
|
||||
fprintf(f, "], \"callees\": [");
|
||||
first = true;
|
||||
for (u32 c : fn.callees) {
|
||||
fprintf(f, "%s\"%08x\"", first ? "" : ", ", c);
|
||||
first = false;
|
||||
}
|
||||
fprintf(f, "]}%s\n", i + 1 < funcs.size() ? "," : "");
|
||||
}
|
||||
fprintf(f, " ]\n}\n");
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// ---- per-function disassembly ----
|
||||
// Deliberately not DisassemblyManager: its analyze() refuses to do anything unless a game is
|
||||
// fully booted, and nothing is booted here. DisAsm() is the same formatter its opcode entries
|
||||
// use, minus the gate.
|
||||
|
||||
const Path funcDir = outDir / SanitizeForFilename(moduleName);
|
||||
if (!File::CreateFullPath(funcDir)) {
|
||||
fprintf(stderr, "re: couldn't create %s\n", funcDir.c_str());
|
||||
ShutdownMinimalPSP();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int written = 0;
|
||||
for (const FuncInfo &fn : funcs) {
|
||||
if (!opts.funcFilter.empty()) {
|
||||
const bool byName = fn.name == opts.funcFilter;
|
||||
const bool byAddr = StringFromFormat("%08x", fn.start) == opts.funcFilter ||
|
||||
StringFromFormat("0x%08x", fn.start) == opts.funcFilter;
|
||||
if (!byName && !byAddr) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const Path path = funcDir / StringFromFormat("%08x_%s.asm", fn.start, SanitizeForFilename(fn.name).c_str());
|
||||
FILE *out = File::OpenCFile(path, "w");
|
||||
if (!out) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(out, "; %s :: %s\n", moduleName.c_str(), fn.name.c_str());
|
||||
fprintf(out, "; %08x - %08x (module +%05x, %d bytes)\n",
|
||||
fn.start, fn.start + fn.size, fn.start - base, fn.size);
|
||||
fprintf(out, ";\n");
|
||||
|
||||
fprintf(out, "; Callers (%d):", (int)fn.callers.size());
|
||||
if (fn.callers.empty()) {
|
||||
fprintf(out, " none found (exported, or only called indirectly)");
|
||||
}
|
||||
for (u32 c : fn.callers) {
|
||||
fprintf(out, " %s", nameOf(c).c_str());
|
||||
}
|
||||
fprintf(out, "\n; Callees (%d):", (int)fn.callees.size());
|
||||
for (u32 c : fn.callees) {
|
||||
fprintf(out, " %s", nameOf(c).c_str());
|
||||
}
|
||||
if (fn.indirectCallees) {
|
||||
fprintf(out, " + indirect");
|
||||
}
|
||||
fprintf(out, "\n;\n");
|
||||
|
||||
WriteRegEvidence(out, fn);
|
||||
fprintf(out, "\n");
|
||||
|
||||
// Tracks lui-loaded upper halves so an "lui/addiu" or "lui/lw" pair can be reported as
|
||||
// the address it actually forms. Those pairs are how every global and constant table is
|
||||
// reached, so without this the interesting operands all read as bare halves.
|
||||
u32 luiVal[32] = {};
|
||||
bool luiSet[32] = {};
|
||||
|
||||
const u32 end = fn.start + fn.size;
|
||||
for (u32 addr = fn.start; addr < end; addr += 4) {
|
||||
if (!Memory::IsValidAddress(addr)) {
|
||||
break;
|
||||
}
|
||||
const std::string label = g_symbolMap->GetLabelString(addr);
|
||||
if (!label.empty() && addr != fn.start) {
|
||||
fprintf(out, "\n%s:\n", label.c_str());
|
||||
}
|
||||
|
||||
char text[512];
|
||||
DisAsm(addr, text, sizeof(text));
|
||||
// DisAsm separates mnemonic from operands with a tab.
|
||||
char *tab = strchr(text, '\t');
|
||||
std::string mnemonic = text;
|
||||
std::string operands;
|
||||
if (tab) {
|
||||
*tab = '\0';
|
||||
mnemonic = text;
|
||||
operands = tab + 1;
|
||||
}
|
||||
fprintf(out, "%08x %-10s %-30s", addr, mnemonic.c_str(), operands.c_str());
|
||||
|
||||
const u32 op = Memory::Read_Instruction(addr).encoding;
|
||||
const u32 opcode = op >> 26;
|
||||
const int rs = (op >> 21) & 0x1f;
|
||||
const int rt = (op >> 16) & 0x1f;
|
||||
const s32 imm = (s16)(op & 0xffff);
|
||||
|
||||
if (IsJal(op)) {
|
||||
fprintf(out, " ; -> %s", nameOf(JumpTarget(addr, op)).c_str());
|
||||
} else if (opcode == 0x0f) { // lui
|
||||
luiVal[rt] = (u32)(op & 0xffff) << 16;
|
||||
luiSet[rt] = true;
|
||||
} else if (opcode == 0x09 && luiSet[rs]) { // addiu
|
||||
const u32 target = luiVal[rs] + imm;
|
||||
fprintf(out, " ; = %08x%s", target, DescribeAddr(target).c_str());
|
||||
luiVal[rt] = target;
|
||||
luiSet[rt] = true;
|
||||
} else if (opcode >= 0x20 && opcode <= 0x2e && luiSet[rs]) { // load/store
|
||||
const u32 target = luiVal[rs] + imm;
|
||||
fprintf(out, " ; @ %08x%s", target, DescribeAddr(target).c_str());
|
||||
} else {
|
||||
// Anything else that writes a register invalidates what we thought it held.
|
||||
const MIPSInfo info = MIPSGetInfo(MIPSOpcode(op));
|
||||
if (info & OUT_RT) {
|
||||
luiSet[rt] = false;
|
||||
}
|
||||
if (info & OUT_RD) {
|
||||
luiSet[(op >> 11) & 0x1f] = false;
|
||||
}
|
||||
}
|
||||
fprintf(out, "\n");
|
||||
}
|
||||
fclose(out);
|
||||
written++;
|
||||
}
|
||||
|
||||
printf("re: %s (crc %08x) at %08x, %d bytes\n", moduleName.c_str(), module->crc, base, blockSize);
|
||||
printf("re: %d exports (%d named), %d imports (%d named), %d functions; wrote %d disassembly file(s)\n",
|
||||
(int)module->exportedFuncs.size(), namedExports, (int)module->importedFuncs.size(), namedImports,
|
||||
(int)funcs.size(), written);
|
||||
printf("re: index at %s\n", indexPath.c_str());
|
||||
|
||||
ShutdownMinimalPSP();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2026- PPSSPP Project.
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
struct ReverseEngineerOptions {
|
||||
// PRX/ELF to load. A host path, or a PSP path like "flash0:/kd/libmp3.prx" (resolved
|
||||
// against the configured NAND directory).
|
||||
std::string modulePath;
|
||||
// Where to write the report. Created if missing.
|
||||
std::string outDir;
|
||||
// If set, only this function is disassembled (by name, or "0x08801234").
|
||||
std::string funcFilter;
|
||||
// Optional .syms file applied before dumping, so names show up in every caller.
|
||||
std::string symsFile;
|
||||
bool verbose = false;
|
||||
};
|
||||
|
||||
// Loads a module standalone (no game), analyzes it, and writes a report. Returns a process
|
||||
// exit code.
|
||||
int RunReverseEngineer(const ReverseEngineerOptions &opts);
|
||||
Reference in new issue
Block a user