Files
BFM-decomp/tools/ghidra_scripts/DumpFunctionSignatures.java
T
Drew T 3207d52491 feat(phase-3.5): prototype research spike — go/no-go (protos don't accelerate Gen1 matching)
- extract_proto_exe.py: subdir-aware proto main-EXE extraction (reuses iso9660); extracted
  sep8 SLUS_007.26 (413,696 B) + aug31 MUSASHI/USA_DEMO.EXE (415,744 B, base 0x80018000)
- ghidra_import.sh: reusable headless import; both protos imported into the bfm project
  (sep8 1726 funcs, aug31 1737 funcs, both PsyQ 4.0.0), R9-verified
- DumpFunctionSignatures.java + match_protos.py: 3-tier (exact/norm/seq) cross-binary
  correspondence; MIPS normalizer validated by anchors
- FINDINGS: (T2) no debug symbols in either proto — Hidden Palace "no symbols" VERIFIED;
  recon "Sep-8 less-stripped" REFUTED by per-file byte compare. (T3) Sep-8 99.6% byte-identical
  to retail (only 3 funcs differ: DebugMenuHandler, CdReadSectorReadyCB, SaveLoadRoutine);
  Aug-31 66% norm-identical, 862 1:1 correspondences. (T4) demo shares retail's 18-entry
  game-mode dispatch architecture but the handler code diverged — Q#10 resolved
- VERDICT (docs/proto-correspondence.md): NO-GO as a Gen1 label/symbol accelerator; GO to retain
  Sep-8 (Phase-6 compiler corroborant) + both (Gen2 assets); proto-side labels in
  config/symbols.proto-{sep8,demo}.txt (R13-tagged, never feed symbols.us.txt)
- reorg extracted/ into per-ROM subfolders (extracted/retail/, extracted/proto/); nested
  gitignore allowlist; extractor defaults updated; manifest --verify PASS
- memory-map.md: Phase-3.5 block + demo dispatch anchors; Q#10 RESOLVED; §5 proto notes VERIFIED
- ghidra_mcp_start.sh: PROG arg to serve a prototype
- rules R13 (proto-provenance/confidence tagging), R14 (verify recon counts vs bytes)
- bumps project version 1.3.0 -> 1.3.1
2026-06-14 03:19:23 -06:00

168 lines
8.2 KiB
Java

// DumpFunctionSignatures.java — headless, READ-ONLY: dump a per-function fingerprint
// for cross-binary correspondence (Phase 3.5 proto<->retail diff). Three tiers so the
// match count is bracketed and trustworthy even if the middle tier is imperfect:
//
// h_exact : SHA1 of the raw instruction bytes (strict; same bytes, usually same addr).
// h_norm : SHA1 of a MIPS-normalized instruction stream that neutralizes ONLY the
// address/relocation-sensitive fields (j/jal absolute targets, lui high
// halves, %lo / gp-relative address immediates detected via Ghidra's
// analyzed references) and rewrites branch targets as PC-relative offsets,
// while KEEPING registers (regalloc matters) and true constants. This is the
// "same source compiled to a different address" workhorse.
// h_seq : SHA1 of the mnemonic-only sequence (loose; same ops, any regalloc/consts).
//
// Also emits nins, nbytes, ncalls and the ORDERED list of direct-call target addresses
// (for the T4 anchor-bootstrap call-graph BFS). The PSX loader emits no relocations, so
// normalization is derived from the analyzed disassembly, not a reloc table.
//
// analyzeHeadless ~/bfm-decomp/ghidra bfm -process <PROG> -noanalysis -readOnly \
// -scriptPath ~/bfm-decomp/tools/ghidra_scripts -postScript DumpFunctionSignatures.java
//
// Output: ~/bfm-decomp/.run/sig.<PROG>.jsonl (gitignored; R12 — never /tmp).
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.lang.Register;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.listing.Listing;
import ghidra.program.model.scalar.Scalar;
import ghidra.program.model.symbol.RefType;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.SourceType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.ArrayList;
public class DumpFunctionSignatures extends GhidraScript {
private static String hex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte x : b) sb.append(Character.forDigit((x >> 4) & 0xF, 16)).append(Character.forDigit(x & 0xF, 16));
return sb.toString();
}
private static String jsonEsc(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '"' || c == '\\') sb.append('\\').append(c);
else if (c < 0x20) sb.append(String.format("\\u%04x", (int) c));
else sb.append(c);
}
return sb.toString();
}
// Normalized token for one instruction: mnemonic + per-operand canonical form.
private String normToken(Instruction ins) {
StringBuilder sb = new StringBuilder();
String m = ins.getMnemonicString();
sb.append(m);
String ml = m.toLowerCase();
boolean isAbsJump = ml.equals("j") || ml.equals("jal"); // J-type absolute target
int nops = ins.getNumOperands();
for (int i = 0; i < nops; i++) {
sb.append('|');
Reference[] refs = ins.getOperandReferences(i);
boolean flowRef = false, dataRef = false;
Address flowTgt = null;
for (Reference r : refs) {
RefType rt = r.getReferenceType();
if (rt.isFlow()) { flowRef = true; flowTgt = r.getToAddress(); }
if (rt.isData() || rt.isRead() || rt.isWrite()) dataRef = true;
}
Object[] objs = ins.getOpObjects(i);
boolean hasGp = false;
for (Object o : objs)
if (o instanceof Register && ((Register) o).getName().equals("gp")) hasGp = true;
// Direct flow target operand.
if (flowRef && flowTgt != null) {
if (isAbsJump) sb.append("@J"); // absolute j/jal -> neutralize
else sb.append("rel").append(flowTgt.subtract(ins.getAddress())); // PC-relative branch -> keep offset
continue;
}
// lui: the immediate is the high half of an address (PSX convention) -> neutralize.
if (ml.equals("lui")) {
for (Object o : objs) {
if (o instanceof Register) sb.append(((Register) o).getName()).append(',');
else if (o instanceof Scalar) sb.append("@HI,");
else sb.append("?,");
}
continue;
}
// Generic operand: keep registers + true constants; neutralize address immediates.
for (Object o : objs) {
if (o instanceof Register) {
sb.append(((Register) o).getName()).append(',');
} else if (o instanceof Scalar) {
if (dataRef || hasGp) sb.append("@LO,"); // %lo / gp-relative address
else sb.append(Long.toString(((Scalar) o).getValue())).append(','); // true constant
} else if (o instanceof Address) {
sb.append("@A,"); // resolved absolute address operand
} else {
sb.append(o.toString()).append(',');
}
}
}
return sb.toString();
}
@Override
public void run() throws Exception {
Listing listing = currentProgram.getListing();
String prog = currentProgram.getName();
Path out = Paths.get(System.getProperty("user.home"), "bfm-decomp", ".run", "sig." + prog + ".jsonl");
Files.createDirectories(out.getParent());
StringBuilder file = new StringBuilder();
FunctionIterator fns = currentProgram.getFunctionManager().getFunctions(true);
int nfuncs = 0;
while (fns.hasNext()) {
Function f = fns.next();
if (f.isExternal() || f.isThunk()) continue;
MessageDigest mdE = MessageDigest.getInstance("SHA-1");
MessageDigest mdN = MessageDigest.getInstance("SHA-1");
MessageDigest mdS = MessageDigest.getInstance("SHA-1");
int nins = 0, nbytes = 0;
ArrayList<String> calls = new ArrayList<>();
for (Instruction ins : listing.getInstructions(f.getBody(), true)) {
byte[] raw;
try { raw = ins.getBytes(); } catch (Exception e) { raw = new byte[0]; }
mdE.update(raw);
mdN.update(normToken(ins).getBytes("UTF-8"));
mdN.update((byte) '\n');
mdS.update(ins.getMnemonicString().getBytes("UTF-8"));
mdS.update((byte) ' ');
nins++;
nbytes += ins.getLength();
if (ins.getFlowType() != null && ins.getFlowType().isCall()) {
for (Reference r : ins.getReferencesFrom())
if (r.getReferenceType().isCall()) calls.add(r.getToAddress().toString());
}
}
String src = (f.getSymbol() != null) ? f.getSymbol().getSource().toString() : "UNKNOWN";
StringBuilder cj = new StringBuilder();
for (int i = 0; i < calls.size(); i++) {
if (i > 0) cj.append(',');
cj.append('"').append(calls.get(i)).append('"');
}
file.append("{\"addr\":\"0x").append(f.getEntryPoint().toString())
.append("\",\"name\":\"").append(jsonEsc(f.getName()))
.append("\",\"src\":\"").append(src)
.append("\",\"nins\":").append(nins)
.append(",\"nbytes\":").append(nbytes)
.append(",\"ncalls\":").append(calls.size())
.append(",\"h_exact\":\"").append(hex(mdE.digest()))
.append("\",\"h_norm\":\"").append(hex(mdN.digest()))
.append("\",\"h_seq\":\"").append(hex(mdS.digest()))
.append("\",\"calls\":[").append(cj).append("]}\n");
nfuncs++;
}
Files.write(out, file.toString().getBytes("UTF-8"));
println("BFMSIG wrote " + nfuncs + " function signatures -> " + out);
}
}