mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
3207d52491
- 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
168 lines
8.2 KiB
Java
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);
|
|
}
|
|
}
|