wip(phase-33): B5 Ghidra regenerability — ExportAnnotations.java + ghidra_export_annotations.sh PROVEN (all 129 programs exported to byte-stable JSONL in 18.5 s), ghidra_annotations_delta.py (live − baseline), ImportAnnotations.java + ghidra_rebuild.sh WRITTEN but unproven (the resident rebuild hit an OSGi script-bundle load error — a compile error in the new importer breaks every script in the dir; the known bad LocalVariableImpl ctor is named in CURRENT_PHASE); Makefile print-% + GHIDRA_PROJ repo-relative; the six ghidra_*.sh repo-relative (BFM_GHIDRA_PROJ); DefineFunctions arg path; ImportPsyqGdt install-dir default; ExportSymbols R15 fix; scratch project under build/ (Ghidra refuses '.'-prefixed path components); SETUP P33 B5 section; checkpoint refreshed for the next session (S86 paused at 87% context)

This commit is contained in:
Drew T
2026-09-06 19:31:34 -06:00
parent 707ec491e4
commit cabab00c0e
17 changed files with 1036 additions and 31 deletions
+7 -1
View File
@@ -168,7 +168,7 @@ help:
# Phase 7 reports: deterministic, committable docs/ digests. progress/difficulty/dup_report
# are Ghidra-free; sig-refresh regenerates dup_report's input from the saved Ghidra DB.
GHIDRA := $(or $(GHIDRA_INSTALL_DIR),$(HOME)/ghidra_12.1_PUBLIC)
GHIDRA_PROJ := $(HOME)/bfm-decomp/ghidra
GHIDRA_PROJ := $(or $(BFM_GHIDRA_PROJ),$(CURDIR)/ghidra) # P33 B5: repo-relative (override: BFM_GHIDRA_PROJ)
# The corpus oracle (Phase 26-A, R32/R33). A SECOND, INDEPENDENT oracle: it cross-checks splat's
# function boundaries against sig_image's, which are derived from the ORIGINAL bytes without splat.
@@ -519,6 +519,12 @@ sig-modules:
echo "sig-modules: signed $$n modules (of $(words $(MODULE_BINARIES)) onboarded)"
if [ "$$bad" -ne 0 ]; then echo "[FAIL] sig-modules: $$bad module(s) failed to sign"; exit 1; fi
# print-<VAR> (P33 B5): echo one Makefile variable so tools derive per-binary facts from the ONE registry
# (R33) instead of re-parsing overlays.mk/modules.mk — e.g. `make -s print-EXE BINARY=ov_SC01_077`,
# `make -s print-VRAM_BASE BINARY=resident`, `make -s print-BINARIES`.
print-%:
@echo '$($*)'
# bootstrap (P33 B3): the fresh-clone setup — apt presence (printed, never run), the venv from
# requirements-python.txt, the submodules, the two cc1 tarballs verified + extracted, then check-env.
bootstrap:
+20
View File
@@ -1069,6 +1069,26 @@ fills fast). Nothing is leaking — but the host does not get the memory back on
* `gate_main` **REFUSES a draft containing its own `INCLUDE_ASM`** (substituting it restores the stub,
so the build passes for free and the function counts as banked), and counts banks from the SOURCE.
### P33 B5 (S86, 2026-09-06) — Ghidra regenerability (IN PROGRESS at the S86 pause; see `phase-ends/CURRENT_PHASE.md`)
- **Model:** the Ghidra database embeds the program bytes, so it leaves git; what it holds beyond the splat symbol files is
exported to text and the project is rebuilt from the disc + that text. `tools/ghidra_scripts/ExportAnnotations.java`
(read-only; byte-stable JSONL: container facts, LOCAL types, every function signature, defined data, the 5 comment kinds,
bookmarks, equates, labels the symbol files lack) via `tools/ghidra_export_annotations.sh [PROG…]` (no PROG = every
program; **129 programs in 18.5 s** → `.run/ghidra_export/`). The committed file `config/ghidra/<prog>.jsonl` is the
DELTA (`tools/ghidra_annotations_delta.py live baseline out`) between the live export and a FRESH rebuild's export, so
analysis-origin rows subtract themselves out. `tools/ghidra_rebuild.sh <prog> [--proof] [--keep]`: import (PSX loader for
the EXEs, BinaryLoader at `make -s print-VRAM_BASE BINARY=<alias>` for the rest) → `DefineFunctions.java` from the built
ELF → `ApplySymbols.java` → baseline export → `ImportAnnotations.java config/ghidra/<prog>.jsonl` → export → delta → `cmp`.
Scratch project under `build/ghidra_rebuild/proj` (**Ghidra refuses a project path with a component starting with '.'**,
so never `.run/`). `make print-<VAR> BINARY=<alias>` echoes any per-binary Makefile variable (R33).
- **Gotcha (S86):** Ghidra compiles a `-scriptPath` directory as ONE OSGi bundle — a single script that fails to compile
makes EVERY script in the directory fail with `Failed to get OSGi bundle containing script`. Diagnose by invoking the
suspect script directly and reading the javac lines.
- Also in this change: `ExportSymbols.java` R15 fix (output path arg, refuses to overwrite, refuses `config/`);
`DefineFunctions.java` list path = arg 1; `ImportPsyqGdt.java` default gdt from the Ghidra install dir; the six
`tools/ghidra_*.sh` are repo-relative (`BFM_GHIDRA_PROJ` overrides the project dir; `ghidra_mcp_verify.sh <addr> <name>
[PROG]`); Makefile `GHIDRA_PROJ := $(or $(BFM_GHIDRA_PROJ),$(CURDIR)/ghidra)`.
### P33 B4 (S86, 2026-09-06) — `tools/fetch_psyq.sh`: the OPTIONAL PsyQ SDK objects, user-supplied and verified
- **What it is for.** Byte-identity never needs Sony's libraries (without them main links its INCLUDE_ASM tiles — the
fresh-clone proof in B3 built all 218 that way). `tools/fetch_psyq.sh [--disc <RTL-4.0 Track 1 .bin>] [--from <dir>]
+61 -10
View File
@@ -170,24 +170,45 @@ Mid-phase rules check after every 4 completed tasks (P6). Commit banked artifact
with one corrupted LIB → `sha256 MISMATCH for tools/psyq/lib40/LIBTAP.LIB`, rc 1, nothing built. **The run:** with
`tools/psyq/lib40/` moved aside, `fetch_psyq.sh --disc "<our Track 1 .bin>"` extracted the 20 LIBs from the disc
(`diff -rq` against the tracked copies: identical), verified them, rebuilt `.run/obj40/*`, `.run/obj42/*`,
`tools/psyq/lib40_elf/`, and `sdk-dual: OK` — **36.9 s wall**, EXIT=0 (`.run/P33/b4_fetch.log`). SETUP: P33 B4 (R21).
`tools/psyq/lib40_elf/`, and `sdk-dual: OK` — **36.9 s wall**, EXIT=0 (`.run/P33/b4_fetch.log`). SETUP: P33 B4 (R21). Commit
`commit:4019`.
- **2026-09-06 (S86) — B5 IN PROGRESS (checkpointed at 87% context; WIP commit).** Done and proven: MCP stopped cleanly;
`tools/ghidra_scripts/ExportAnnotations.java` + `tools/ghidra_export_annotations.sh` — byte-stable JSONL export of a
program's container facts, LOCAL types, every function signature (params/locals/storage/sources), defined data, the 5
comment kinds, bookmarks, equates, and labels the symbol files do not carry; **all 129 programs exported in 18.5 s** →
`.run/ghidra_export/<prog>.jsonl` (139 MB; resident 2,573 rows in 5.7 s). `tools/ghidra_annotations_delta.py` (live −
baseline; container rows always kept; `--census`). Makefile `print-%` helper + `GHIDRA_PROJ` repo-relative; the six
`tools/ghidra_*.sh` wrappers repo-relative (`BFM_GHIDRA_PROJ` override; `ghidra_mcp_verify.sh` takes the program as arg 3
/ `BFM_GHIDRA_PROG`); `DefineFunctions.java` takes the list path as arg 1; `ImportPsyqGdt.java` finds the gdt under
`Application.getInstallationDirectory()`; `ExportSymbols.java` R15 fix (output path arg, refuses to overwrite, refuses
`config/`). Written, NOT yet proven: `tools/ghidra_scripts/ImportAnnotations.java`, `tools/ghidra_rebuild.sh` (its import
step works: the scratch project must live under `build/ghidra_rebuild/proj` — Ghidra refuses a path component starting
with '.'; `resident_funcs.txt` from the ELF = 1,304 entries). **Blocker found:** in the resident rebuild the runs after the
import failed with `Failed to get OSGi bundle containing script: …/tools/ghidra_scripts/ApplySymbols.java` (same for
ExportAnnotations) — Ghidra compiles the script DIRECTORY as one bundle, so ONE file that does not compile breaks every
script in it; `ExportAnnotations` worked on 129 programs BEFORE `ImportAnnotations.java` existed ⇒ the new file almost
certainly has a compile error (one known: `new LocalVariableImpl(name, first, dt, stackOffset, program)` — no such ctor;
the stack form is `(String, DataType, int, Program)` without first-use, or `(String, int, DataType, VariableStorage/Address/
Register, Program)`). Census of the live exports (heuristic, before baselines): `types=0` in EVERY program (no hand-authored
structs in the DB at all); the 154/402 "USER_DEFINED" variables per program are the loader's GTEMAC functions
(analysis-origin, will subtract); real user labels beyond the symbol files: aug31 382, sep8 15, SLUS_007.26 1, overlays 1–2.
## 🛑 SESSION CHECKPOINT — A1–A4 ✓, B1–B4 ✓ (8 tasks; P6 rules check done after B4); NEXT = B5 Ghidra regenerability (Max, MCP stopped) (2026-09-06, written by session bd19e14a "S86"; SUPERSEDES the earlier blocks)
## 🛑 SESSION CHECKPOINT — A1–A4 ✓, B1–B4 ✓, B5 IN PROGRESS (exporter proven; importer + rebuild proof pending on one compile error); NEXT = finish B5 (2026-09-06 ~19:35 MDT, written by session bd19e14a "S86" at Drew's 87%-context pause; SUPERSEDES the earlier blocks)
### 0. How to use this block
You are a FRESH SESSION that has read `PROJECT_CONTEXT.md`, `phase-ends/DIGEST.md`, `PhaseEnd_Phase30/31/32.md` and this file,
and nothing else (R64). Replay this block verbatim, state phase / done / NEXT / effort, list the rules from the digest
(R1–R73), then WAIT for Drew. **NEXT = B5** (Max; the Ghidra text export + rebuild proof — the plan's Block B5 and §3 step 1
below; the headless MCP server holds the project lock: `tools/ghidra_mcp_stop.sh` FIRST, then every headless step; the
SessionStart hook restarts the server next session and R29 applies). If `git log -1 --format=%s` does not start with
`feat(phase-33): B4`, B4's commit did not land: `tools/fetch_psyq.sh --no-build` must print `verified …` for every piece and
exit 0 before committing (`.run/P33/b4_fetch.log` is the full run).
(R1–R73), then WAIT for Drew. **NEXT = finish B5** (Max). The WIP commit `wip(phase-33): B5 …` holds every B5 file (see
the Log's B5 entry for what is proven and what is not). The SessionStart hook will have RESTARTED the headless MCP server
(it serves `SLUS_007.26` and holds the project lock): run `tools/ghidra_mcp_stop.sh` before ANY headless step (R23), and
prompt Drew for `/mcp` only if you actually need the MCP tools (R29) — B5 needs none. §3 step 1 is the exact resume path.
### 1. Where we are
**Phase 33 — 100% verification + the public flip + Gen2 exit.** Gate 1 approved 2026-09-06 (plan mode, Max). R65–R73 ratified at
gate 1 (DIGEST §3). **Done: A1 (`commit:4012`), A2 (`commit:4013` — the corrected main denominator 45,150 is now the published
number; `.run/sig.main.jsonl` is the sig main is weighed by; `tools/main_seed_ends.py` is new), A3 (the `NO_SDK` knob +
`make sdk-dual`, proven 28 s: both legs `143dbb89…`).** The approved plan is VERBATIM
number; `.run/sig.main.jsonl` is the sig main is weighed by; `tools/main_seed_ends.py` is new), A3 (`commit:4014`, the
`NO_SDK` knob + `make sdk-dual`, proven 28 s: both legs `143dbb89…`), A4 (`commit:4015`), B1 (`commit:4016`), B2 (`commit:4017`),
B3 (`commit:4018`), B4 (`commit:4019`); B5 in progress (the WIP commit).** The approved plan is VERBATIM
at the end of this file — read its Blocks A–G for every task's files, commands and verification; the "Execution order and
why" section is the sequence; the "Honest scale" table is the budget (≈17–21 sessions; the flip precedes the writing so later
items can be cut). Harness tasks: #1 A1 done, #2 A2 done, #3 A3 next … #41 G2.
@@ -250,7 +271,31 @@ items can be cut). Harness tasks: #1 A1 done, #2 A2 done, #3 A3 next … #41 G2.
### 3. NEXT — in order
0. **Preflight:** `git status --short | grep -v ghidra/` (empty) · `git log -1 --format='%h %s'` · `df -h ~` (≈13 GB free) ·
`.venv/bin/python -c 'import splat'` · `ls build/us/SLUS_007.26.map` (main is built; `make check BINARY=main` if not).
1. **B5** (Max) — facts gathered (S86): the existing scripts and their conventions are in `tools/ghidra_scripts/`
1. **B5 — RESUME HERE** (Max). (a) `tools/ghidra_mcp_stop.sh`. (b) Find the compile error: `mv tools/ghidra_scripts/
ImportAnnotations.java /tmp`-style moves are FORBIDDEN (R12) — instead run `~/ghidra_12.1_PUBLIC/support/analyzeHeadless
build/ghidra_rebuild/proj bfm -process resident -noanalysis -scriptPath tools/ghidra_scripts -postScript
ImportAnnotations.java /dev/null 2>&1 | grep -iE 'error|cannot find|symbol' | head` (the scratch project from the last
run still exists) and read the javac messages; fix `ImportAnnotations.java` (known: the stack-local ctor — use
`new LocalVariableImpl(name, dt, off, currentProgram)` (no first-use) or `new LocalVariableImpl(name, first, dt, new
VariableStorage(currentProgram, off, size), currentProgram)` if that ctor exists — check with `javap -cp ~/ghidra_12.1_PUBLIC/
Ghidra/Framework/SoftwareModeling/lib/SoftwareModeling.jar ghidra.program.model.listing.LocalVariableImpl`); re-run until
the bundle loads (a bundle failure names EVERY script in the dir — one bad file breaks all). (c) `tools/ghidra_rebuild.sh
resident --keep` → candidate delta `.run/ghidra_rebuild/resident.candidate.jsonl` (census printed): inspect what remains
after the baseline subtraction (expect: few rows — the DB holds no hand-authored types; check the comment/bookmark/label
residue is real RE, not analysis noise; if analysis noise remains, it means auto-analysis is NOT deterministic between the
live import (Phase 10) and today's — then filter those row kinds explicitly and record why). (d) `mkdir -p config/ghidra;
cp .run/ghidra_rebuild/resident.candidate.jsonl config/ghidra/resident.jsonl; tools/ghidra_rebuild.sh resident --proof` →
must print `PROOF PASS`. (e) Negative control (R39): copy the config, change one field name in the copy, point a proof at it
(temporarily swap the file) → must FAIL; restore. (f) The same for `SLUS_007.26` (main; import + analysis ≈ 3 min); then
`ov_SC01_077`, `ov_SC06_018`, `sep8_SLUS_007.26`, `aug31_USA_DEMO.EXE` (the protos need `extracted/proto/*`, present).
(g) `config/ghidra/ROSTER.md` (generated by a tiny script or by hand: program, kind, payload, vram, rows kept, proof
date) — the roster = the 6 formerly-tracked programs; the other 123 regenerate on demand via `prefetch_fleet.py`.
(h) `.claude/settings.json` hooks → `"command": "bash \"$CLAUDE_PROJECT_DIR\"/tools/ghidra_mcp_start.sh"` (and stop);
`tools/ghidra_mcp_start.sh` exits 0 silently when `$GHIDRA/support/analyzeHeadless` is absent (a contributor's session
must not try to launch Ghidra). (i) `docs/SETUP.md` P33 B5 section (R21): every new tool + the OSGi-bundle gotcha + the
'.'-path gotcha + the delta model; `docs/second-oracle.md`/§2.8 pointers as needed. (j) Log entry, tick B5, refresh this
block, commit `feat(phase-33): B5 …`. Then **B6**.
Facts gathered (S86): the existing scripts and their conventions are in `tools/ghidra_scripts/`
(`ExportSymbols.java` symbols-only, hardcoded output + blind overwrite — the R15 violation to fix; `ApplySymbols.java` the
proven text→DB mirror with `BFMAPPLY` counts; `DumpFunctionSignatures.java` has the `jsonEsc` helper + JSONL style to reuse;
`DefineFunctions.java` reads `~/bfm-decomp/.run/<prog>_funcs.txt` (hardcoded `user.home`); `ImportPsyqGdt.java` hardcodes
@@ -279,6 +324,12 @@ section), regenerated `docs/progress.fleet.md`, `docs/duplicates.md`, `docs/dupl
`.run/backlog.jsonl`. A3: `Makefile` (`NO_SDK`, `sdk-dual`, tools-health wiring, `.PHONY`), `docs/SETUP.md` (P33 A3 section).
A4: `tools/family_hseq.py` (`binaries` + `open_instances` in the json; `load()` returns a pair), `tools/audit_binaries.py`
(CHECK 4 reads the scanned set), `docs/family-hseq.md` (regenerated, 0 families), `docs/SETUP.md` (P33 A4 section).
B1–B4: see their Log entries. B5 (WIP): `tools/ghidra_scripts/{ExportAnnotations,ImportAnnotations}.java` (new),
`tools/ghidra_export_annotations.sh`, `tools/ghidra_annotations_delta.py`, `tools/ghidra_rebuild.sh` (new),
`tools/ghidra_scripts/{DefineFunctions,ImportPsyqGdt,ExportSymbols}.java`, the six `tools/ghidra_*.sh`, `Makefile`
(`print-%`, `GHIDRA_PROJ`). Scratch: `.run/ghidra_export/` (129 exports), `.run/ghidra_rebuild/`, `build/ghidra_rebuild/`.
Environment at the pause: the MCP server is STOPPED (saved cleanly, "Save succeeded"); disk ≈ 13 GB free; the SDK object dirs
are rebuilt and the tree's main build is in the WITH-SDK state; `extracted/proto/` regenerated.
Scratch: `.run/public_audit/` (the history inventory), `.run/P33/a2_report.log`, `.run/P33/a3_sdk_dual.log`,
`.run/P33/verify/main_{with_sdk,no_sdk}.map` (to be allowlisted by A5).
The plan file: `~/.claude/plans/max-effort-set-plan-twinkling-moonbeam.md` (copied below).
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""ghidra_annotations_delta.py — the HAND-AUTHORED part of a Ghidra program, as a byte-stable file (P33 B5).
ghidra_annotations_delta.py <live.jsonl> <baseline.jsonl> <out.jsonl> [--census]
`live.jsonl` is ExportAnnotations.java's dump of the program as it is; `baseline.jsonl` is the same
dump of a FRESH rebuild of that program (import + auto-analysis + DefineFunctions + ApplySymbols, no
annotations imported). Every row auto-analysis produces on its own appears in both and subtracts
itself out; what remains — types, retyped/renamed signatures and locals, typed data, comments,
bookmarks, equates, labels that no symbol file carries — is the RE work worth committing:
config/ghidra/<program>.jsonl. The container rows (`program`, `block`, `archive`) are always kept
from `live` so an image-base or memory-map drift shows as a diff rather than a silent skip.
The proof (tools/ghidra_rebuild.sh --proof) rebuilds, imports config/ghidra/<program>.jsonl, exports,
takes the delta against the SAME rebuild's pre-import baseline, and `cmp`s it against the committed file.
`--census` prints per-kind counts of the delta (the roster decision: a program whose delta holds no
hand-authored rows need not be kept regenerable — it regenerates on demand).
"""
import collections
import json
import sys
KEEP_ALWAYS = ("program", "block", "archive")
def rows(path):
with open(path, encoding="utf-8") as f:
return [ln.rstrip("\n") for ln in f if ln.strip()]
def kind(line):
return json.loads(line)["k"]
def main(argv):
census = "--census" in argv
argv = [a for a in argv if a != "--census"]
if len(argv) != 4:
sys.exit(__doc__)
live, base, out = rows(argv[1]), set(rows(argv[2])), argv[3]
kept = [ln for ln in live if kind(ln) in KEEP_ALWAYS or ln not in base]
with open(out, "w", encoding="utf-8", newline="\n") as f:
for ln in kept:
f.write(ln + "\n")
c = collections.Counter(kind(ln) for ln in kept)
hand = sum(v for k, v in c.items() if k not in KEEP_ALWAYS)
print(f"delta: {len(live)} live - {len(base)} baseline -> {len(kept)} rows ({hand} hand-authored) -> {out}",
file=sys.stderr)
if census:
for k in sorted(c):
print(f" {k:9} {c[k]}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+5 -4
View File
@@ -4,15 +4,16 @@
# the write open needs the project lock. Verify afterwards with tools/ghidra_mcp_verify.sh <addr> <name> (R9).
# tools/ghidra_apply_symbols.sh [PROG] [symbols files...] (defaults: SLUS_007.26 config/symbols.us.txt)
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # P33 B5: repo-relative, no $HOME/bfm-decomp assumption
GHIDRA="${GHIDRA_INSTALL_DIR:-$HOME/ghidra_12.1_PUBLIC}"
PROJ_DIR="$HOME/bfm-decomp/ghidra"; PROJ="bfm"
PROJ_DIR="${BFM_GHIDRA_PROJ:-$REPO/ghidra}"; PROJ="bfm"
PROG="${1:-SLUS_007.26}"; shift || true
SYMS=("$@"); [ ${#SYMS[@]} -eq 0 ] && SYMS=("$HOME/bfm-decomp/config/symbols.us.txt")
SCRIPTS="$HOME/bfm-decomp/tools/ghidra_scripts"
SYMS=("$@"); [ ${#SYMS[@]} -eq 0 ] && SYMS=("$REPO/config/symbols.us.txt")
SCRIPTS="$REPO/tools/ghidra_scripts"
if ss -tln 2>/dev/null | grep -qE ":8080([^0-9]|$)"; then
echo "apply-symbols: ERROR — MCP server serving on :8080. Run tools/ghidra_mcp_stop.sh first."; exit 2
fi
cd "$HOME/bfm-decomp"
cd "$REPO"
out=$(timeout 600 "$GHIDRA/support/analyzeHeadless" "$PROJ_DIR" "$PROJ" \
-process "$PROG" -noanalysis -scriptPath "$SCRIPTS" -postScript ApplySymbols.java "${SYMS[@]}" 2>&1)
rc=$?
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# tools/ghidra_export_annotations.sh [--proj DIR] [--out DIR] [PROG ...]
#
# Read-only export of a Ghidra program's annotations (types, signatures, data, comments, bookmarks,
# equates, labels) to byte-stable JSON-Lines via tools/ghidra_scripts/ExportAnnotations.java (P33 B5).
# With no PROG every program in the project is exported (headless `-process` with no name).
# Output: <out>/<PROG>.jsonl (default .run/ghidra_export/). The splat symbol files are passed so the
# labels they already carry are skipped (ApplySymbols.java restores those on a rebuild).
#
# PRECONDITION: the MCP server is STOPPED (tools/ghidra_mcp_stop.sh) — even a read-only open needs
# the project lock free. Never writes to the project (-readOnly).
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GHIDRA="${GHIDRA_INSTALL_DIR:-$HOME/ghidra_12.1_PUBLIC}"
PROJ_DIR="${BFM_GHIDRA_PROJ:-$REPO/ghidra}"; PROJ="bfm"
OUT="$REPO/.run/ghidra_export"
SCRIPTS="$REPO/tools/ghidra_scripts"
while [ $# -gt 0 ]; do
case "$1" in
--proj) PROJ_DIR="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
*) break ;;
esac
done
if ss -tln 2>/dev/null | grep -qE ":8080([^0-9]|$)"; then
echo "export-annotations: ERROR — MCP server serving on :8080. Run tools/ghidra_mcp_stop.sh first." >&2; exit 2
fi
[ -f "$PROJ_DIR/$PROJ.gpr" ] || { echo "export-annotations: ERROR — no project at $PROJ_DIR/$PROJ.gpr" >&2; exit 2; }
mkdir -p "$OUT"
cd "$REPO"
SYMS=(config/symbols.us.txt config/symbols.us.ram.txt config/symbols.resident.txt)
for f in config/symbols.ov_*.txt config/symbols.md_*.txt config/symbols.proto-*.txt; do [ -f "$f" ] && SYMS+=("$f"); done
if [ $# -eq 0 ]; then
procs=(-process) # every program in the project folder
else
procs=()
for p in "$@"; do procs+=(-process "$p"); done
fi
rc=0
if [ $# -eq 0 ]; then
timeout 7200 "$GHIDRA/support/analyzeHeadless" "$PROJ_DIR" "$PROJ" -process -noanalysis -readOnly \
-scriptPath "$SCRIPTS" -postScript ExportAnnotations.java "$OUT" "${SYMS[@]}" 2>&1 \
| grep -E 'BFMEXPORT|ERROR|Exception' ; rc=${PIPESTATUS[0]}
else
for p in "$@"; do
timeout 1800 "$GHIDRA/support/analyzeHeadless" "$PROJ_DIR" "$PROJ" -process "$p" -noanalysis -readOnly \
-scriptPath "$SCRIPTS" -postScript ExportAnnotations.java "$OUT" "${SYMS[@]}" 2>&1 \
| grep -E 'BFMEXPORT|ERROR|Exception' ; r=${PIPESTATUS[0]}; [ "$r" -eq 0 ] || rc=$r
done
fi
echo "export-annotations: analyzeHeadless exit=$rc -> $OUT/"
exit $rc
+3 -2
View File
@@ -14,12 +14,13 @@
# Run tools/ghidra_mcp_stop.sh first. Used in Phase 3.5 to add the two prototypes
# (sep8_SLUS_007.26, aug31_USA_DEMO.EXE) alongside retail; reusable for any PS-X EXE.
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # P33 B5: repo-relative, no $HOME/bfm-decomp assumption
EXE="${1:?usage: ghidra_import.sh <path-to-ps-x-exe>}"
GHIDRA="${GHIDRA_INSTALL_DIR:-$HOME/ghidra_12.1_PUBLIC}"
PROJ_DIR="$HOME/bfm-decomp/ghidra"
PROJ_DIR="${BFM_GHIDRA_PROJ:-$REPO/ghidra}"
PROJ="bfm"
SCRIPTS="$HOME/bfm-decomp/tools/ghidra_scripts"
SCRIPTS="$REPO/tools/ghidra_scripts"
GDT="${PSYQ_GDT:-$GHIDRA/Ghidra/Extensions/ghidra_psx_ldr/data/psyq400.gdt}"
PORT="8080"
+4 -3
View File
@@ -18,18 +18,19 @@
# PRECONDITION: the MCP server must be STOPPED (exclusive project lock) — run
# tools/ghidra_mcp_stop.sh first. analyzeHeadless commits + saves on a clean exit.
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # P33 B5: repo-relative, no $HOME/bfm-decomp assumption
BLOB="${1:?usage: ghidra_import_raw.sh <blob-path> <base-vram> <program-name>}"
BASE="${2:?usage: ghidra_import_raw.sh <blob-path> <base-vram> <program-name>}"
NAME="${3:?usage: ghidra_import_raw.sh <blob-path> <base-vram> <program-name>}"
GHIDRA="${GHIDRA_INSTALL_DIR:-$HOME/ghidra_12.1_PUBLIC}"
PROJ_DIR="$HOME/bfm-decomp/ghidra"
PROJ_DIR="${BFM_GHIDRA_PROJ:-$REPO/ghidra}"
PROJ="bfm"
SCRIPTS="$HOME/bfm-decomp/tools/ghidra_scripts"
SCRIPTS="$REPO/tools/ghidra_scripts"
GDT="${PSYQ_GDT:-$GHIDRA/Ghidra/Extensions/ghidra_psx_ldr/data/psyq400.gdt}"
LANG_ID="PSX:LE:32:default"
PORT="8080"
STAGE="$HOME/bfm-decomp/.run/$NAME" # import filename -> program name
STAGE="$REPO/.run/$NAME" # import filename -> program name
[ -f "$BLOB" ] || { echo "import-raw: ERROR — blob not found: $BLOB" >&2; exit 2; }
[ -f "$GDT" ] || { echo "import-raw: ERROR — PsyQ gdt not found: $GDT" >&2; exit 2; }
+4 -3
View File
@@ -19,15 +19,16 @@
# open transaction + saves on close). Save+stop with tools/ghidra_mcp_stop.sh; resume by
# re-running this script. There is no mid-session save (GhidrAssist holds an open txn).
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # P33 B5: repo-relative, no $HOME/bfm-decomp assumption
GHIDRA="${GHIDRA_INSTALL_DIR:-$HOME/ghidra_12.1_PUBLIC}"
PROJ_DIR="$HOME/bfm-decomp/ghidra"
PROJ_DIR="${BFM_GHIDRA_PROJ:-$REPO/ghidra}"
PROJ="bfm"
PROG="${1:-SLUS_007.26}" # program to serve; default retail, pass a proto name to override
PORT="8080"
RUNDIR="$HOME/bfm-decomp/.run"; mkdir -p "$RUNDIR" # project-local runtime scratch (never /tmp)
RUNDIR="$REPO/.run"; mkdir -p "$RUNDIR" # project-local runtime scratch (never /tmp)
LOG="$RUNDIR/ghidra-mcp.log"
SCRIPTS="$HOME/bfm-decomp/tools/ghidra_scripts"
SCRIPTS="$REPO/tools/ghidra_scripts"
STOPREQ="${BFM_MCP_STOPREQ:-$RUNDIR/mcp-stop.req}"
# 1) Already serving? no-op (idempotent across sessions).
+2 -1
View File
@@ -4,9 +4,10 @@
# analyzeHeadless COMMITS the pending transaction and saves+closes the project (releasing
# the .rep lock). The next session resumes from the saved state. Use at task/phase boundaries.
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # P33 B5: repo-relative, no $HOME/bfm-decomp assumption
PORT="8080"
RUNDIR="$HOME/bfm-decomp/.run"
RUNDIR="$REPO/.run"
LOG="$RUNDIR/ghidra-mcp.log"
STOPREQ="${BFM_MCP_STOPREQ:-$RUNDIR/mcp-stop.req}"
+4 -3
View File
@@ -9,15 +9,16 @@
# Example (after a phase's last edit was renaming 0x80018730 to LzssDecodeSector):
# tools/ghidra_mcp_stop.sh && tools/ghidra_mcp_verify.sh 0x80018730 LzssDecodeSector
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # P33 B5: repo-relative, no $HOME/bfm-decomp assumption
ADDR="${1:?usage: ghidra_mcp_verify.sh <addr e.g. 0x80018730> <expected-name>}"
EXPECT="${2:?usage: ghidra_mcp_verify.sh <addr> <expected-name>}"
GHIDRA="${GHIDRA_INSTALL_DIR:-$HOME/ghidra_12.1_PUBLIC}"
PROJ_DIR="$HOME/bfm-decomp/ghidra"
PROJ_DIR="${BFM_GHIDRA_PROJ:-$REPO/ghidra}"
PROJ="bfm"
PROG="SLUS_007.26"
SCRIPTS="$HOME/bfm-decomp/tools/ghidra_scripts"
PROG="${3:-${BFM_GHIDRA_PROG:-SLUS_007.26}}" # P33 B5: 3rd arg / env selects the program
SCRIPTS="$REPO/tools/ghidra_scripts"
PORT="8080"
if ss -tln 2>/dev/null | grep -qE ":$PORT([^0-9]|$)"; then
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# tools/ghidra_rebuild.sh <program> [--proof] [--keep]
#
# Rebuild ONE Ghidra program FROM TEXT + the disc, in a scratch project (.run/ghidra_rebuild/proj), and
# prove it: the committed config/ghidra/<program>.jsonl (ExportAnnotations.java's delta format) is the
# hand-authored RE work; everything else — the bytes, auto-analysis, the function set, the names — comes
# from the extracted payload, Ghidra, the built ELF and the splat symbol files. P33 B5; makes the Ghidra
# database regenerable so it can leave git (it embeds the program bytes).
#
# 1. import PS-X EXEs (SLUS_007.26, the two prototypes) via the PSX loader; every other program is a
# raw blob at its vram (`make -s print-EXE / print-VRAM_BASE BINARY=<alias>` — the build's
# own registry, R33), + auto-analysis + ImportPsyqGdt.java (the psyq400 types).
# 2. functions DefineFunctions.java from the built ELF's text symbols (splat's validated boundaries).
# 3. symbols ApplySymbols.java with the binary's splat symbol files (the yaml's symbol_addrs_path).
# 4. baseline ExportAnnotations.java -> .run/ghidra_rebuild/<program>.baseline.jsonl (nothing hand-made yet)
# 5. import ImportAnnotations.java config/ghidra/<program>.jsonl (when the file exists)
# 6. export -> .run/ghidra_rebuild/<program>.after.jsonl; delta(after, baseline) -> <program>.delta.jsonl
# --proof cmp <program>.delta.jsonl config/ghidra/<program>.jsonl -> "PROOF PASS" (exit 0) or FAIL (exit 1).
# Without a committed file, step 5 is skipped and the delta of the LIVE export (.run/ghidra_export/<program>.jsonl,
# from tools/ghidra_export_annotations.sh) against the baseline is written to <program>.candidate.jsonl — the
# file to review and commit as config/ghidra/<program>.jsonl.
#
# PRECONDITION: the MCP server is STOPPED (tools/ghidra_mcp_stop.sh); the extracted payload and the built ELF
# exist (`make check BINARY=<alias>`). The live project (ghidra/) is never touched. --keep leaves the scratch
# project for inspection (the next run wipes it).
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO"
PROG="${1:?usage: ghidra_rebuild.sh <program> [--proof] [--keep]}"; shift
PROOF=0; KEEP=0
for a in "$@"; do case "$a" in --proof) PROOF=1 ;; --keep) KEEP=1 ;; *) echo "rebuild: unknown arg $a" >&2; exit 2 ;; esac; done
GHIDRA="${GHIDRA_INSTALL_DIR:-$HOME/ghidra_12.1_PUBLIC}"
HEADLESS="$GHIDRA/support/analyzeHeadless"
SCRIPTS="$REPO/tools/ghidra_scripts"
GDT="${PSYQ_GDT:-$GHIDRA/Ghidra/Extensions/ghidra_psx_ldr/data/psyq400.gdt}"
SCR="$REPO/.run/ghidra_rebuild" # exports, deltas, function lists (plain files)
# Ghidra refuses a project path with a component starting with '.', so the scratch PROJECT lives under
# the gitignored build/ tree (wiped by `make clean`, which is the right lifetime for it).
PROJ_DIR="$REPO/build/ghidra_rebuild/proj"; PROJ="bfm"; STAGEDIR="$REPO/build/ghidra_rebuild"
CONF="$REPO/config/ghidra/$PROG.jsonl"
LIVE="$REPO/.run/ghidra_export/$PROG.jsonl"
say() { printf 'rebuild[%s]: %s\n' "$PROG" "$*"; }
die() { printf 'rebuild[%s]: %s\n' "$PROG" "$*" >&2; exit 1; }
[ -x "$HEADLESS" ] || die "no analyzeHeadless at $HEADLESS (GHIDRA_INSTALL_DIR)"
[ -f "$GDT" ] || die "no psyq400.gdt at $GDT (PSYQ_GDT)"
if ss -tln 2>/dev/null | grep -qE ":8080([^0-9]|$)"; then die "MCP server serving on :8080 — run tools/ghidra_mcp_stop.sh first"; fi
# ---- what is this program? ----
case "$PROG" in
SLUS_007.26) KIND=exe; ALIAS=main; EXE=extracted/retail/SLUS_007.26 ;;
sep8_SLUS_007.26) KIND=exe; ALIAS=proto-sep8; EXE=extracted/proto/sep8_SLUS_007.26 ;;
aug31_USA_DEMO.EXE) KIND=exe; ALIAS=proto-demo; EXE=extracted/proto/aug31_USA_DEMO.EXE ;;
*) KIND=raw; ALIAS="$PROG"; EXE="$(make -s print-EXE BINARY="$ALIAS")"; VRAM="$(make -s print-VRAM_BASE BINARY="$ALIAS")" ;;
esac
[ -n "$EXE" ] && [ -f "$EXE" ] || die "payload not found for $PROG ($EXE) — run make disc-extract (or extract_proto_exe.py for a prototype)"
case "$ALIAS" in
proto-sep8) SYMS=(config/symbols.proto-sep8.txt); ELF="" ;;
proto-demo) SYMS=(config/symbols.proto-demo.txt); ELF="" ;;
*) ELF="$(make -s print-ELF BINARY="$ALIAS")"
mapfile -t SYMS < <(.venv/bin/python - "$ALIAS" <<'EOF'
import sys, yaml
y = yaml.safe_load(open(f"config/splat.{'us.exe' if sys.argv[1]=='main' else sys.argv[1]}.yaml"))
p = y["options"].get("symbol_addrs_path", [])
print("\n".join(p if isinstance(p, list) else [p]))
EOF
) ;;
esac
# ---- scratch project ----
rm -rf "$PROJ_DIR"; mkdir -p "$PROJ_DIR" "$SCR"
run_headless() { # args: the analyzeHeadless tail; prints the BFM* / REPORT / ERROR lines; returns its exit
timeout 3600 "$HEADLESS" "$PROJ_DIR" "$PROJ" "$@" 2>&1 | grep -E 'BFM|REPORT:|ERROR|Exception|Analysis succeeded|Import' | grep -vE 'INFO (Analysis|REPORT: Import)' | head -40
return "${PIPESTATUS[0]}"
}
say "1. import ($KIND) $EXE"
if [ "$KIND" = exe ]; then
run_headless -import "$EXE" -overwrite -scriptPath "$SCRIPTS" -postScript ImportPsyqGdt.java "$GDT" -postScript DumpProgramInfo.java || die "import failed"
else
STAGE="$STAGEDIR/$PROG"; cp -f "$EXE" "$STAGE"
run_headless -import "$STAGE" -overwrite -loader BinaryLoader -loader-baseAddr "$VRAM" -processor PSX:LE:32:default \
-scriptPath "$SCRIPTS" -postScript ImportPsyqGdt.java "$GDT" -postScript DumpProgramInfo.java || die "import failed"
rm -f "$STAGE"
fi
if [ -n "$ELF" ] && [ -f "$ELF" ]; then
say "2. functions from $ELF"
LO="$(make -s print-TEXT_LO BINARY="$ALIAS")"; HI="$(make -s print-TEXT_HI BINARY="$ALIAS")"
mipsel-linux-gnu-nm "$ELF" | awk -v lo="$LO" -v hi="$HI" '
$2=="T" || $2=="t" { a=strtonum("0x" $1); if (a%4==0 && a>=strtonum(lo) && a<strtonum(hi)) printf "0x%08X\n", a }' \
| sort -u > "$SCR/${PROG}_funcs.txt"
run_headless -process "$PROG" -noanalysis -scriptPath "$SCRIPTS" -postScript DefineFunctions.java "$SCR/${PROG}_funcs.txt" || die "DefineFunctions failed"
else
say "2. functions: no built ELF for $ALIAS — keeping the analysis-found set"
fi
say "3. symbols: ${SYMS[*]}"
run_headless -process "$PROG" -noanalysis -scriptPath "$SCRIPTS" -postScript ApplySymbols.java "${SYMS[@]}" || die "ApplySymbols failed"
say "4. baseline export"
run_headless -process "$PROG" -noanalysis -readOnly -scriptPath "$SCRIPTS" -postScript ExportAnnotations.java "$SCR/$PROG.baseline.jsonl" "${SYMS[@]}" || die "baseline export failed"
if [ ! -f "$CONF" ]; then
[ -f "$LIVE" ] || die "no $CONF and no live export $LIVE (run tools/ghidra_export_annotations.sh $PROG first)"
.venv/bin/python tools/ghidra_annotations_delta.py "$LIVE" "$SCR/$PROG.baseline.jsonl" "$SCR/$PROG.candidate.jsonl" --census
say "no committed file — CANDIDATE written: $SCR/$PROG.candidate.jsonl (review, then cp to $CONF)"
[ "$KEEP" = 1 ] || rm -rf "$PROJ_DIR"
exit 0
fi
say "5. import $CONF"
run_headless -process "$PROG" -noanalysis -scriptPath "$SCRIPTS" -postScript ImportAnnotations.java "$CONF" || die "ImportAnnotations failed"
say "6. export after import + delta"
run_headless -process "$PROG" -noanalysis -readOnly -scriptPath "$SCRIPTS" -postScript ExportAnnotations.java "$SCR/$PROG.after.jsonl" "${SYMS[@]}" || die "export failed"
.venv/bin/python tools/ghidra_annotations_delta.py "$SCR/$PROG.after.jsonl" "$SCR/$PROG.baseline.jsonl" "$SCR/$PROG.delta.jsonl" --census
if [ "$PROOF" = 1 ]; then
if cmp -s "$SCR/$PROG.delta.jsonl" "$CONF"; then
say "PROOF PASS — the rebuilt program's hand-authored delta == $CONF ($(wc -l < "$CONF") rows)"
[ "$KEEP" = 1 ] || rm -rf "$PROJ_DIR"
exit 0
fi
say "PROOF FAIL — delta differs from $CONF (diff below, first 40 lines; scratch kept in $SCR)"
diff "$CONF" "$SCR/$PROG.delta.jsonl" | head -40
exit 1
fi
[ "$KEEP" = 1 ] || rm -rf "$PROJ_DIR"
say "done (no --proof requested)"
+6 -2
View File
@@ -18,8 +18,12 @@ import java.util.List;
public class DefineFunctions extends GhidraScript {
public void run() throws Exception {
String prog = currentProgram.getName();
Path in = Paths.get(System.getProperty("user.home"), "bfm-decomp", ".run", prog + "_funcs.txt");
if (!Files.exists(in)) { println("DefineFunctions: no list at " + in); return; }
// P33 B5: the list path is arg 1 (tools/ghidra_rebuild.sh passes .run/ghidra_rebuild/<prog>_funcs.txt);
// without an arg the historical location .run/<prog>_funcs.txt under the CWD (the repo) is used —
// no more `user.home/bfm-decomp` assumption.
String[] args = getScriptArgs();
Path in = (args != null && args.length > 0) ? Paths.get(args[0]) : Paths.get(".run", prog + "_funcs.txt");
if (!Files.exists(in)) { println("DefineFunctions: no list at " + in.toAbsolutePath()); return; }
List<String> lines = Files.readAllLines(in);
int created = 0, existed = 0, failed = 0;
for (String ln : lines) {
+295
View File
@@ -0,0 +1,295 @@
// ExportAnnotations.java — headless, READ-ONLY: serialize a program's ANNOTATIONS to byte-stable JSON-Lines
// (P33 B5, the Ghidra-regenerability deliverable carried since Phase 9/10).
//
// WHAT: everything a human (or the MCP) can author in a program that is NOT already text in the repo —
// data types (structs/unions/enums/typedefs/function definitions of the program's LOCAL archive; the
// psyq400 archive types are re-imported by ImportPsyqGdt.java), function signatures (return type,
// calling convention, parameters, locals, flags), defined data, the five comment kinds, bookmarks,
// equates, and non-default labels that the splat symbol files do not already carry — plus the
// container facts (language, image base, memory blocks) so an image-base drift shows as a diff.
//
// HOW IT IS USED: tools/ghidra_rebuild.sh exports the LIVE program and a FRESH rebuild (import +
// analysis + DefineFunctions + ApplySymbols) and stores their DELTA (tools/ghidra_annotations_delta.py)
// as config/ghidra/<program>.jsonl — every row analysis produces on its own subtracts itself out, so
// what is committed is the hand-authored RE work. The proof re-imports that delta and re-exports.
// Byte-stability is the whole contract: fixed key order, sorted rows, 0x%08x addresses, one JSON object
// per line, no whitespace. Deterministic across machines given the same program.
//
// analyzeHeadless <proj_dir> bfm -process <PROG> -noanalysis -readOnly \
// -scriptPath tools/ghidra_scripts -postScript ExportAnnotations.java <out.jsonl|out_dir> [symbols.txt ...]
//
// Arg 1: an output FILE, or a DIRECTORY (then <dir>/<program>.jsonl — the all-programs form, run with
// `-process` and no program name). Args 2..: splat symbol files whose `name = 0xADDR;` rows are
// skipped from the label export (ApplySymbols.java restores those).
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressIterator;
import ghidra.program.model.data.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.symbol.*;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ExportAnnotations extends GhidraScript {
// ---- JSON helpers (the DumpFunctionSignatures.java convention: hand-serialized, canonical) ----
private static String esc(String s) {
if (s == null) return "null";
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 == '\n') sb.append("\\n");
else if (c == '\r') sb.append("\\r");
else if (c == '\t') sb.append("\\t");
else if (c < 0x20) sb.append(String.format("\\u%04x", (int) c));
else sb.append(c);
}
return sb.append('"').toString();
}
private static String hexAddr(Address a) { return String.format("\"0x%08x\"", a.getOffset()); }
private static String b(boolean v) { return v ? "true" : "false"; }
private final Map<Long, Set<String>> symfileNames = new HashMap<>(); // addr -> names the symbol files carry
@Override
protected void run() throws Exception {
String[] args = getScriptArgs();
if (args == null || args.length < 1) { println("BFMEXPORT ERROR: usage <out.jsonl|out_dir> [symbols.txt ...]"); return; }
Path out = Paths.get(args[0]);
if (Files.isDirectory(out)) out = out.resolve(currentProgram.getName() + ".jsonl");
Pattern sp = Pattern.compile("^\\s*([A-Za-z_]\\w*)\\s*=\\s*0x([0-9A-Fa-f]+)\\s*;");
for (int i = 1; i < args.length; i++) {
Path f = Paths.get(args[i]);
if (!Files.isRegularFile(f)) { println("BFMEXPORT WARN: symbols file not found: " + f); continue; }
for (String ln : Files.readAllLines(f)) {
Matcher m = sp.matcher(ln);
if (m.find()) symfileNames.computeIfAbsent(Long.parseLong(m.group(2), 16), k -> new HashSet<>()).add(m.group(1));
}
}
List<String> rows = new ArrayList<>();
int nTypes = program(rows);
int nFuncs = functions(rows);
int nData = data(rows);
int nComments = comments(rows);
int nBookmarks = bookmarks(rows);
int nEquates = equates(rows);
int nLabels = labels(rows);
StringBuilder sb = new StringBuilder();
for (String r : rows) sb.append(r).append('\n');
Files.createDirectories(out.toAbsolutePath().getParent());
Files.write(out, sb.toString().getBytes(StandardCharsets.UTF_8));
println("BFMEXPORT program=" + currentProgram.getName() + " types=" + nTypes + " funcs=" + nFuncs
+ " data=" + nData + " comments=" + nComments + " bookmarks=" + nBookmarks + " equates=" + nEquates
+ " labels=" + nLabels + " rows=" + rows.size() + " -> " + out);
}
// ---- program, blocks, archives, LOCAL data types (sorted by path) ----
private int program(List<String> rows) {
Program p = currentProgram;
rows.add("{\"k\":\"program\",\"name\":" + esc(p.getName()) + ",\"lang\":" + esc(p.getLanguageID().getIdAsString())
+ ",\"cspec\":" + esc(p.getCompilerSpec().getCompilerSpecID().getIdAsString())
+ ",\"image_base\":" + hexAddr(p.getImageBase()) + ",\"format\":" + esc(p.getExecutableFormat()) + "}");
List<String> blocks = new ArrayList<>();
for (MemoryBlock mb : p.getMemory().getBlocks()) {
String perm = (mb.isRead() ? "r" : "-") + (mb.isWrite() ? "w" : "-") + (mb.isExecute() ? "x" : "-");
blocks.add("{\"k\":\"block\",\"name\":" + esc(mb.getName()) + ",\"start\":" + hexAddr(mb.getStart())
+ ",\"end\":" + hexAddr(mb.getEnd()) + ",\"perm\":\"" + perm + "\",\"init\":" + b(mb.isInitialized()) + "}");
}
rows.addAll(blocks); // memory order as Ghidra lists them (address order)
DataTypeManager dtm = p.getDataTypeManager();
SourceArchive local = dtm.getLocalSourceArchive();
TreeSet<String> archives = new TreeSet<>();
for (SourceArchive sa : dtm.getSourceArchives()) {
if (sa.getSourceArchiveID().equals(local.getSourceArchiveID())) continue;
if (sa.getArchiveType() == ArchiveType.BUILT_IN) continue;
archives.add("{\"k\":\"archive\",\"name\":" + esc(sa.getName()) + "}");
}
rows.addAll(archives);
TreeMap<String, String> types = new TreeMap<>();
Iterator<DataType> it = dtm.getAllDataTypes();
while (it.hasNext()) {
DataType dt = it.next();
if (dt instanceof Pointer || dt instanceof Array || dt instanceof BuiltInDataType) continue;
SourceArchive sa = dt.getSourceArchive();
if (sa != null && !sa.getSourceArchiveID().equals(local.getSourceArchiveID())) continue; // psyq400 etc.
String row = typeRow(dt);
if (row != null) types.put(dt.getPathName(), row);
}
rows.addAll(types.values());
return types.size();
}
private String typeRow(DataType dt) {
String path = esc(dt.getPathName());
if (dt instanceof Structure) {
Structure s = (Structure) dt;
StringBuilder f = new StringBuilder("[");
boolean first = true;
for (DataTypeComponent c : s.getDefinedComponents()) {
if (!first) f.append(','); first = false;
f.append("{\"off\":").append(c.getOffset()).append(",\"len\":").append(c.getLength())
.append(",\"name\":").append(esc(c.getFieldName())).append(",\"type\":").append(esc(c.getDataType().getPathName()))
.append(",\"comment\":").append(esc(c.getComment())).append('}');
}
return "{\"k\":\"type\",\"kind\":\"struct\",\"path\":" + path + ",\"size\":" + s.getLength()
+ ",\"packed\":" + b(s.isPackingEnabled()) + ",\"fields\":" + f + "]}";
}
if (dt instanceof Union) {
Union u = (Union) dt;
StringBuilder f = new StringBuilder("[");
boolean first = true;
for (DataTypeComponent c : u.getComponents()) {
if (!first) f.append(','); first = false;
f.append("{\"len\":").append(c.getLength()).append(",\"name\":").append(esc(c.getFieldName()))
.append(",\"type\":").append(esc(c.getDataType().getPathName())).append(",\"comment\":").append(esc(c.getComment())).append('}');
}
return "{\"k\":\"type\",\"kind\":\"union\",\"path\":" + path + ",\"size\":" + u.getLength()
+ ",\"packed\":" + b(u.isPackingEnabled()) + ",\"fields\":" + f + "]}";
}
if (dt instanceof ghidra.program.model.data.Enum) {
ghidra.program.model.data.Enum e = (ghidra.program.model.data.Enum) dt;
TreeMap<String, String> vals = new TreeMap<>();
for (String n : e.getNames()) vals.put(String.format("%020d|%s", e.getValue(n), n),
"{\"name\":" + esc(n) + ",\"value\":" + e.getValue(n) + "}");
return "{\"k\":\"type\",\"kind\":\"enum\",\"path\":" + path + ",\"size\":" + e.getLength()
+ ",\"values\":[" + String.join(",", vals.values()) + "]}";
}
if (dt instanceof TypeDef) {
return "{\"k\":\"type\",\"kind\":\"typedef\",\"path\":" + path + ",\"base\":" + esc(((TypeDef) dt).getDataType().getPathName()) + "}";
}
if (dt instanceof FunctionDefinition) {
FunctionDefinition fd = (FunctionDefinition) dt;
StringBuilder ps = new StringBuilder("[");
boolean first = true;
for (ParameterDefinition pd : fd.getArguments()) {
if (!first) ps.append(','); first = false;
ps.append("{\"name\":").append(esc(pd.getName())).append(",\"type\":").append(esc(pd.getDataType().getPathName()))
.append(",\"comment\":").append(esc(pd.getComment())).append('}');
}
return "{\"k\":\"type\",\"kind\":\"fndef\",\"path\":" + path + ",\"ret\":" + esc(fd.getReturnType().getPathName())
+ ",\"cc\":" + esc(fd.getCallingConventionName()) + ",\"varargs\":" + b(fd.hasVarArgs()) + ",\"params\":" + ps + "]}";
}
return "{\"k\":\"type\",\"kind\":\"other\",\"path\":" + path + ",\"class\":" + esc(dt.getClass().getSimpleName()) + "}";
}
// ---- functions (every non-external function; the delta keeps the user-touched ones) ----
private int functions(List<String> rows) {
TreeMap<Long, String> m = new TreeMap<>();
for (Function f : currentProgram.getFunctionManager().getFunctions(true)) {
if (f.isExternal()) continue;
StringBuilder ps = new StringBuilder("[");
boolean first = true;
for (Parameter p : f.getParameters()) {
if (!first) ps.append(','); first = false;
ps.append("{\"name\":").append(esc(p.getName())).append(",\"type\":").append(esc(p.getDataType().getPathName()))
.append(",\"storage\":").append(esc(p.getVariableStorage().toString())).append(",\"src\":").append(esc(p.getSource().name())).append('}');
}
ps.append(']');
TreeMap<String, String> locals = new TreeMap<>();
for (Variable v : f.getLocalVariables()) {
String key = String.format("%010d|%s|%s", v.getFirstUseOffset(), v.getVariableStorage().toString(), v.getName());
locals.put(key, "{\"name\":" + esc(v.getName()) + ",\"type\":" + esc(v.getDataType().getPathName())
+ ",\"storage\":" + esc(v.getVariableStorage().toString()) + ",\"first\":" + v.getFirstUseOffset()
+ ",\"src\":" + esc(v.getSource().name()) + ",\"comment\":" + esc(v.getComment()) + "}");
}
String row = "{\"k\":\"func\",\"addr\":" + hexAddr(f.getEntryPoint()) + ",\"name\":" + esc(f.getName())
+ ",\"ret\":" + esc(f.getReturnType().getPathName()) + ",\"cc\":" + esc(f.getCallingConventionName())
+ ",\"varargs\":" + b(f.hasVarArgs()) + ",\"noreturn\":" + b(f.hasNoReturn()) + ",\"inline\":" + b(f.isInline())
+ ",\"custom\":" + b(f.hasCustomVariableStorage()) + ",\"sigsrc\":" + esc(f.getSignatureSource().name())
+ ",\"params\":" + ps + ",\"locals\":[" + String.join(",", locals.values()) + "]"
+ ",\"comment\":" + esc(f.getComment()) + "}";
m.put(f.getEntryPoint().getOffset(), row);
}
rows.addAll(m.values());
return m.size();
}
// ---- defined data (every typed unit; the delta keeps the user-typed ones) ----
private int data(List<String> rows) {
TreeMap<Long, String> m = new TreeMap<>();
DataIterator it = currentProgram.getListing().getDefinedData(true);
while (it.hasNext()) {
Data d = it.next();
DataType dt = d.getDataType();
if (dt instanceof DefaultDataType || dt instanceof Undefined) continue;
m.put(d.getAddress().getOffset(), "{\"k\":\"data\",\"addr\":" + hexAddr(d.getAddress()) + ",\"type\":" + esc(dt.getPathName())
+ ",\"len\":" + d.getLength() + "}");
}
rows.addAll(m.values());
return m.size();
}
// ---- comments, five kinds ----
private int comments(List<String> rows) {
TreeMap<String, String> m = new TreeMap<>();
Listing l = currentProgram.getListing();
CommentType[] kinds = {CommentType.EOL, CommentType.PRE, CommentType.POST, CommentType.PLATE, CommentType.REPEATABLE};
String[] names = {"eol", "pre", "post", "plate", "repeat"};
for (int i = 0; i < kinds.length; i++) {
AddressIterator ai = l.getCommentAddressIterator(kinds[i], currentProgram.getMemory(), true);
while (ai.hasNext()) {
Address a = ai.next();
String c = l.getComment(kinds[i], a);
if (c == null) continue;
m.put(String.format("%016x|%d", a.getOffset(), i), "{\"k\":\"comment\",\"addr\":" + hexAddr(a) + ",\"type\":\"" + names[i] + "\",\"text\":" + esc(c) + "}");
}
}
rows.addAll(m.values());
return m.size();
}
private int bookmarks(List<String> rows) {
TreeMap<String, String> m = new TreeMap<>();
Iterator<Bookmark> it = currentProgram.getBookmarkManager().getBookmarksIterator();
while (it.hasNext()) {
Bookmark bk = it.next();
String key = String.format("%016x|%s|%s|%s", bk.getAddress().getOffset(), bk.getTypeString(), bk.getCategory(), bk.getComment());
m.put(key, "{\"k\":\"bookmark\",\"addr\":" + hexAddr(bk.getAddress()) + ",\"type\":" + esc(bk.getTypeString())
+ ",\"cat\":" + esc(bk.getCategory()) + ",\"text\":" + esc(bk.getComment()) + "}");
}
rows.addAll(m.values());
return m.size();
}
private int equates(List<String> rows) {
TreeMap<String, String> m = new TreeMap<>();
Iterator<Equate> it = currentProgram.getEquateTable().getEquates();
while (it.hasNext()) {
Equate e = it.next();
TreeSet<String> refs = new TreeSet<>();
for (EquateReference r : e.getReferences())
refs.add("[" + hexAddr(r.getAddress()) + "," + r.getOpIndex() + "]");
m.put(e.getName(), "{\"k\":\"equate\",\"name\":" + esc(e.getName()) + ",\"value\":" + e.getValue()
+ ",\"refs\":[" + String.join(",", refs) + "]}");
}
rows.addAll(m.values());
return m.size();
}
// ---- labels: every non-DEFAULT, non-external symbol the symbol files do not already carry ----
private int labels(List<String> rows) {
TreeMap<String, String> m = new TreeMap<>();
for (Symbol s : currentProgram.getSymbolTable().getAllSymbols(true)) {
if (s.isExternal() || s.getSource() == SourceType.DEFAULT) continue;
if (!currentProgram.getMemory().contains(s.getAddress())) continue;
Set<String> have = symfileNames.get(s.getAddress().getOffset());
if (have != null && have.contains(s.getName())) continue;
boolean isFunc = s.getSymbolType() == SymbolType.FUNCTION;
m.put(String.format("%016x|%s", s.getAddress().getOffset(), s.getName()),
"{\"k\":\"label\",\"addr\":" + hexAddr(s.getAddress()) + ",\"name\":" + esc(s.getName()) + ",\"primary\":" + b(s.isPrimary())
+ ",\"src\":" + esc(s.getSource().name()) + ",\"func\":" + b(isFunc) + "}");
}
rows.addAll(m.values());
return m.size();
}
}
+16 -1
View File
@@ -38,7 +38,22 @@ public class ExportSymbols extends GhidraScript {
sb.append("// Durable text record of manual RE work (the .rep is gitignored). Phase-5 splat refines this.\n");
sb.append("// count=").append(n).append("\n\n");
for (String v : rows.values()) sb.append(v).append("\n");
java.nio.file.Path out = Paths.get(System.getProperty("user.home"), "bfm-decomp", "config", "symbols.us.txt");
// P33 B5 (rule R15): NEVER overwrite the curated config/symbols.us.txt — re-exporting is a reviewed
// re-merge. Arg 1 = output path (default .run/ghidra_export/<program>.symbols.txt under the CWD);
// an existing output is refused unless arg 2 is the literal `overwrite`.
String[] args = getScriptArgs();
java.nio.file.Path out = (args != null && args.length > 0) ? Paths.get(args[0])
: Paths.get(".run", "ghidra_export", currentProgram.getName() + ".symbols.txt");
boolean overwrite = args != null && args.length > 1 && "overwrite".equals(args[1]);
if (Files.exists(out) && !overwrite) {
println("BFMSYM REFUSED: " + out + " exists (pass `overwrite` as the 2nd arg; config/symbols*.txt is merged by hand, R15)");
return;
}
if (out.toAbsolutePath().normalize().toString().contains("/config/")) {
println("BFMSYM REFUSED: will not write into config/ (R15 — merge the export by hand): " + out);
return;
}
Files.createDirectories(out.toAbsolutePath().getParent());
Files.write(out, sb.toString().getBytes());
println("BFMSYM exported " + n + " USER_DEFINED symbols -> " + out);
}
+370
View File
@@ -0,0 +1,370 @@
// ImportAnnotations.java — headless: apply an annotations JSON-Lines file (ExportAnnotations.java's format,
// normally the committed config/ghidra/<program>.jsonl) to the current program. The inverse of the export;
// idempotent (compare-before-write everywhere); run WITHOUT -readOnly so analyzeHeadless commits + saves
// on exit (the ApplySymbols.java precedent). P33 B5.
//
// analyzeHeadless <proj_dir> bfm -process <PROG> -noanalysis \
// -scriptPath tools/ghidra_scripts -postScript ImportAnnotations.java <in.jsonl>
//
// Refuses (R43) a file whose `program` row names another language / image base, and any row kind it does
// not know — nothing is applied in that case. Prints one BFMANN summary line with every counter.
// Types are applied in two passes (shells first, then fields) so self- and mutually-referencing structs
// resolve; type strings are Ghidra path names with pointer/array suffixes (`/BFM/Actor *[4]`).
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.*;
import ghidra.program.model.lang.Register;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import ghidra.program.model.util.CodeUnitInsertionException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
public class ImportAnnotations extends GhidraScript {
private static final Set<String> KINDS = new HashSet<>(Arrays.asList(
"program", "block", "archive", "type", "func", "data", "comment", "bookmark", "equate", "label"));
private int nTypes, nFuncs, nData, nComments, nBookmarks, nEquates, nLabels, unchanged, failed, skippedStorage;
private DataTypeManager dtm;
private static String s(JsonObject o, String k) { JsonElement e = o.get(k); return (e == null || e.isJsonNull()) ? null : e.getAsString(); }
private static boolean bo(JsonObject o, String k) { JsonElement e = o.get(k); return e != null && !e.isJsonNull() && e.getAsBoolean(); }
private static int in(JsonObject o, String k) { JsonElement e = o.get(k); return (e == null || e.isJsonNull()) ? 0 : e.getAsInt(); }
private Address addr(String hex) { return currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(Long.decode(hex)); }
@Override
protected void run() throws Exception {
String[] args = getScriptArgs();
if (args == null || args.length < 1) { println("BFMANN ERROR: usage <in.jsonl>"); return; }
List<JsonObject> rows = new ArrayList<>();
for (String ln : Files.readAllLines(Paths.get(args[0]), StandardCharsets.UTF_8)) {
if (ln.trim().isEmpty()) continue;
JsonObject o = JsonParser.parseString(ln).getAsJsonObject();
String k = s(o, "k");
if (k == null || !KINDS.contains(k)) { println("BFMANN ERROR: unknown row kind " + k + " — nothing applied (R43)"); return; }
rows.add(o);
}
for (JsonObject o : rows) {
if (!"program".equals(s(o, "k"))) continue;
String lang = s(o, "lang");
long base = Long.decode(s(o, "image_base"));
if (!currentProgram.getLanguageID().getIdAsString().equals(lang) || currentProgram.getImageBase().getOffset() != base) {
println("BFMANN ERROR: file is for " + lang + " @" + s(o, "image_base") + " but the program is "
+ currentProgram.getLanguageID().getIdAsString() + " @" + currentProgram.getImageBase() + " — nothing applied (R43)");
return;
}
}
dtm = currentProgram.getDataTypeManager();
// pass 1: type shells; pass 2: type bodies (so fields can reference any struct in the file)
List<JsonObject> types = new ArrayList<>();
for (JsonObject o : rows) if ("type".equals(s(o, "k"))) types.add(o);
for (JsonObject o : types) typeShell(o);
for (JsonObject o : types) typeBody(o);
for (JsonObject o : rows) {
String k = s(o, "k");
try {
switch (k) {
case "func": func(o); break;
case "data": data(o); break;
case "comment": comment(o); break;
case "bookmark": bookmark(o); break;
case "equate": equate(o); break;
case "label": label(o); break;
default: break; // program/block/archive/type handled above
}
} catch (Exception e) {
failed++;
println(" fail " + k + " " + (s(o, "addr") != null ? s(o, "addr") : s(o, "name")) + ": " + e);
}
}
println("BFMANN types=" + nTypes + " funcs=" + nFuncs + " data=" + nData + " comments=" + nComments
+ " bookmarks=" + nBookmarks + " equates=" + nEquates + " labels=" + nLabels + " unchanged=" + unchanged
+ " skipped_storage=" + skippedStorage + " failed=" + failed + " rows=" + rows.size());
}
// ---- data types ----
private static CategoryPath catOf(String path) {
int i = path.lastIndexOf('/');
return new CategoryPath(i <= 0 ? "/" : path.substring(0, i));
}
private static String nameOf(String path) { return path.substring(path.lastIndexOf('/') + 1); }
/** Resolve a Ghidra path name with optional pointer/array suffixes (`/u8 *[4]`) to a DataType. */
private DataType resolveType(String spec) {
String base = spec;
int cut = -1;
for (int i = 0; i < spec.length(); i++) {
char c = spec.charAt(i);
if (c == '[' || (c == '*' && i > 0 && spec.charAt(i - 1) == ' ')) { cut = i; break; }
}
String suffix = "";
if (cut >= 0) { base = spec.substring(0, cut).trim(); suffix = spec.substring(cut); }
DataType dt = dtm.getDataType(base);
if (dt == null) dt = BuiltInDataTypeManager.getDataTypeManager().getDataType(base);
if (dt == null && !base.startsWith("/")) dt = dtm.getDataType("/" + base);
if (dt == null) throw new IllegalArgumentException("unresolvable type " + spec);
int i = 0;
while (i < suffix.length()) {
char c = suffix.charAt(i);
if (c == ' ') { i++; continue; }
if (c == '*') { dt = new PointerDataType(dt, dtm); i++; continue; }
if (c == '[') {
int j = suffix.indexOf(']', i);
int n = Integer.parseInt(suffix.substring(i + 1, j).trim());
dt = new ArrayDataType(dt, n, dt.getLength(), dtm);
i = j + 1; continue;
}
throw new IllegalArgumentException("bad type suffix in " + spec);
}
return dt;
}
private void typeShell(JsonObject o) {
String kind = s(o, "kind"), path = s(o, "path");
CategoryPath cat = catOf(path); String name = nameOf(path);
if (dtm.getDataType(path) != null) return;
DataType dt;
switch (kind) {
case "struct": dt = new StructureDataType(cat, name, in(o, "size"), dtm); break;
case "union": dt = new UnionDataType(cat, name, dtm); break;
case "enum": dt = new EnumDataType(cat, name, in(o, "size"), dtm); break;
case "typedef": dt = new TypedefDataType(cat, name, resolveType(s(o, "base")), dtm); break;
case "fndef": dt = new FunctionDefinitionDataType(cat, name, dtm); break;
default: failed++; println(" fail type " + path + ": unsupported kind " + kind); return;
}
dtm.resolve(dt, DataTypeConflictHandler.REPLACE_HANDLER);
nTypes++;
}
private void typeBody(JsonObject o) {
String kind = s(o, "kind"), path = s(o, "path");
DataType dt = dtm.getDataType(path);
if (dt == null) return;
try {
if ("struct".equals(kind) && dt instanceof Structure) {
Structure st = (Structure) dt;
boolean packed = bo(o, "packed");
if (st.isPackingEnabled() != packed) st.setPackingEnabled(packed);
if (!packed && st.getLength() != in(o, "size")) { st.deleteAll(); st.growStructure(in(o, "size")); }
for (JsonElement fe : o.getAsJsonArray("fields")) {
JsonObject f = fe.getAsJsonObject();
DataType ft = resolveType(s(f, "type"));
DataTypeComponent cur = st.getComponentAt(in(f, "off"));
if (cur != null && cur.getOffset() == in(f, "off") && Objects.equals(cur.getFieldName(), s(f, "name"))
&& cur.getDataType().getPathName().equals(ft.getPathName()) && cur.getLength() == in(f, "len")
&& Objects.equals(cur.getComment(), s(f, "comment"))) { unchanged++; continue; }
if (packed) st.add(ft, in(f, "len"), s(f, "name"), s(f, "comment"));
else st.replaceAtOffset(in(f, "off"), ft, in(f, "len"), s(f, "name"), s(f, "comment"));
}
} else if ("union".equals(kind) && dt instanceof Union) {
Union u = (Union) dt;
if (u.getNumComponents() == 0)
for (JsonElement fe : o.getAsJsonArray("fields")) {
JsonObject f = fe.getAsJsonObject();
u.add(resolveType(s(f, "type")), in(f, "len"), s(f, "name"), s(f, "comment"));
}
else unchanged++;
} else if ("enum".equals(kind) && dt instanceof ghidra.program.model.data.Enum) {
ghidra.program.model.data.Enum e = (ghidra.program.model.data.Enum) dt;
for (JsonElement ve : o.getAsJsonArray("values")) {
JsonObject v = ve.getAsJsonObject();
String n = s(v, "name"); long val = v.get("value").getAsLong();
if (e.contains(n) && e.getValue(n) == val) { unchanged++; continue; }
if (e.contains(n)) e.remove(n);
e.add(n, val);
}
} else if ("fndef".equals(kind) && dt instanceof FunctionDefinition) {
FunctionDefinition fd = (FunctionDefinition) dt;
fd.setReturnType(resolveType(s(o, "ret")));
List<ParameterDefinition> ps = new ArrayList<>();
for (JsonElement pe : o.getAsJsonArray("params")) {
JsonObject p = pe.getAsJsonObject();
ps.add(new ParameterDefinitionImpl(s(p, "name"), resolveType(s(p, "type")), s(p, "comment")));
}
fd.setArguments(ps.toArray(new ParameterDefinition[0]));
fd.setVarArgs(bo(o, "varargs"));
String cc = s(o, "cc");
if (cc != null) try { fd.setCallingConvention(cc); } catch (Exception ex) { /* unknown cc: keep default */ }
}
} catch (Exception e) {
failed++; println(" fail type body " + path + ": " + e);
}
}
// ---- functions ----
private VariableStorage storageOf(String spec) {
// "Stack[-0x10]:4" | "a0:4" | "s0:4" ... ; anything else -> null (dynamic storage)
try {
if (spec.startsWith("Stack[")) {
int j = spec.indexOf(']');
int off = (int) Long.decode(spec.substring(6, j));
int size = Integer.parseInt(spec.substring(spec.lastIndexOf(':') + 1));
return new VariableStorage(currentProgram, off, size);
}
String rn = spec.contains(":") ? spec.substring(0, spec.indexOf(':')) : spec;
Register r = currentProgram.getRegister(rn);
if (r != null) return new VariableStorage(currentProgram, r);
} catch (Exception e) { /* fall through */ }
return null;
}
private void func(JsonObject o) throws Exception {
Address a = addr(s(o, "addr"));
FunctionManager fm = currentProgram.getFunctionManager();
Function f = fm.getFunctionAt(a);
if (f == null) {
if (getInstructionAt(a) == null) disassemble(a);
f = createFunction(a, s(o, "name"));
if (f == null) { failed++; println(" fail func " + s(o, "addr") + ": cannot create"); return; }
}
SourceType src = SourceType.valueOf(s(o, "sigsrc"));
boolean changed = false;
String name = s(o, "name");
if (name != null && !name.equals(f.getName())) { f.setName(name, src == SourceType.DEFAULT ? SourceType.USER_DEFINED : src); changed = true; }
DataType ret = resolveType(s(o, "ret"));
if (!f.getReturnType().getPathName().equals(ret.getPathName())) { f.setReturnType(ret, src); changed = true; }
String cc = s(o, "cc");
if (cc != null && !cc.equals(f.getCallingConventionName())) { try { f.setCallingConvention(cc); changed = true; } catch (Exception e) { skippedStorage++; } }
if (f.hasVarArgs() != bo(o, "varargs")) { f.setVarArgs(bo(o, "varargs")); changed = true; }
if (f.hasNoReturn() != bo(o, "noreturn")) { f.setNoReturn(bo(o, "noreturn")); changed = true; }
if (f.isInline() != bo(o, "inline")) { f.setInline(bo(o, "inline")); changed = true; }
// parameters
JsonArray ps = o.getAsJsonArray("params");
boolean custom = bo(o, "custom");
List<Variable> want = new ArrayList<>();
boolean allStorage = true;
for (JsonElement pe : ps) {
JsonObject p = pe.getAsJsonObject();
DataType pt = resolveType(s(p, "type"));
VariableStorage vs = custom ? storageOf(s(p, "storage")) : null;
if (custom && vs == null) allStorage = false;
want.add(vs != null ? new ParameterImpl(s(p, "name"), pt, vs, currentProgram) : new ParameterImpl(s(p, "name"), pt, currentProgram));
}
boolean same = f.getParameterCount() == want.size();
for (int i = 0; same && i < want.size(); i++) {
Parameter cur = f.getParameter(i);
same = Objects.equals(cur.getName(), want.get(i).getName()) && cur.getDataType().getPathName().equals(want.get(i).getDataType().getPathName())
&& (!custom || cur.getVariableStorage().toString().equals(want.get(i).getVariableStorage().toString()));
}
if (!same) {
Function.FunctionUpdateType ut = (custom && allStorage) ? Function.FunctionUpdateType.CUSTOM_STORAGE : Function.FunctionUpdateType.DYNAMIC_STORAGE_ALL_PARAMS;
if (custom && !allStorage) skippedStorage++;
f.replaceParameters(want, ut, true, src == SourceType.DEFAULT ? SourceType.USER_DEFINED : src);
changed = true;
}
// locals: match by storage + first-use; update name/type/comment, else add
for (JsonElement le : o.getAsJsonArray("locals")) {
JsonObject l = le.getAsJsonObject();
String storage = s(l, "storage"); int first = in(l, "first");
DataType lt = resolveType(s(l, "type"));
Variable hit = null;
for (Variable v : f.getLocalVariables())
if (v.getVariableStorage().toString().equals(storage) && v.getFirstUseOffset() == first) { hit = v; break; }
SourceType lsrc = SourceType.valueOf(s(l, "src"));
if (lsrc == SourceType.DEFAULT) lsrc = SourceType.USER_DEFINED;
if (hit != null) {
boolean lc = false;
if (!Objects.equals(hit.getName(), s(l, "name"))) { hit.setName(s(l, "name"), lsrc); lc = true; }
if (!hit.getDataType().getPathName().equals(lt.getPathName())) { hit.setDataType(lt, lsrc); lc = true; }
if (!Objects.equals(hit.getComment(), s(l, "comment"))) { hit.setComment(s(l, "comment")); lc = true; }
if (lc) changed = true;
continue;
}
Variable nv;
if (storage.startsWith("Stack[")) {
int off = (int) Long.decode(storage.substring(6, storage.indexOf(']')));
nv = new LocalVariableImpl(s(l, "name"), first, lt, off, currentProgram);
} else {
String rn = storage.contains(":") ? storage.substring(0, storage.indexOf(':')) : storage;
Register r = currentProgram.getRegister(rn);
if (r == null) { skippedStorage++; continue; }
nv = new LocalVariableImpl(s(l, "name"), first, lt, r, currentProgram);
}
Variable added = f.addLocalVariable(nv, lsrc);
if (added != null && s(l, "comment") != null) added.setComment(s(l, "comment"));
changed = true;
}
String fc = s(o, "comment");
if (!Objects.equals(fc, f.getComment())) { f.setComment(fc); changed = true; }
if (changed) nFuncs++; else unchanged++;
}
// ---- data, comments, bookmarks, equates, labels ----
private void data(JsonObject o) throws Exception {
Address a = addr(s(o, "addr"));
DataType dt = resolveType(s(o, "type"));
Data cur = currentProgram.getListing().getDefinedDataAt(a);
if (cur != null && cur.getDataType().getPathName().equals(dt.getPathName()) && cur.getLength() == in(o, "len")) { unchanged++; return; }
try {
DataUtilities.createData(currentProgram, a, dt, in(o, "len"), DataUtilities.ClearDataMode.CLEAR_ALL_CONFLICT_DATA);
nData++;
} catch (CodeUnitInsertionException e) {
failed++; println(" fail data " + s(o, "addr") + ": " + e.getMessage());
}
}
private void comment(JsonObject o) {
Address a = addr(s(o, "addr"));
CommentType t;
switch (s(o, "type")) {
case "eol": t = CommentType.EOL; break;
case "pre": t = CommentType.PRE; break;
case "post": t = CommentType.POST; break;
case "plate": t = CommentType.PLATE; break;
default: t = CommentType.REPEATABLE; break;
}
Listing l = currentProgram.getListing();
if (Objects.equals(l.getComment(t, a), s(o, "text"))) { unchanged++; return; }
l.setComment(a, t, s(o, "text"));
nComments++;
}
private void bookmark(JsonObject o) {
Address a = addr(s(o, "addr"));
BookmarkManager bm = currentProgram.getBookmarkManager();
Bookmark cur = bm.getBookmark(a, s(o, "type"), s(o, "cat"));
if (cur != null && Objects.equals(cur.getComment(), s(o, "text"))) { unchanged++; return; }
bm.setBookmark(a, s(o, "type"), s(o, "cat"), s(o, "text"));
nBookmarks++;
}
private void equate(JsonObject o) throws Exception {
EquateTable et = currentProgram.getEquateTable();
String name = s(o, "name"); long value = o.get("value").getAsLong();
Equate e = et.getEquate(name);
boolean changed = false;
if (e == null) { e = et.createEquate(name, value); changed = true; }
Set<String> have = new HashSet<>();
for (EquateReference r : e.getReferences()) have.add(r.getAddress().getOffset() + "|" + r.getOpIndex());
for (JsonElement re : o.getAsJsonArray("refs")) {
JsonArray r = re.getAsJsonArray();
Address a = addr(r.get(0).getAsString()); int op = r.get(1).getAsInt();
if (have.contains(a.getOffset() + "|" + op)) continue;
e.addReference(a, op); changed = true;
}
if (changed) nEquates++; else unchanged++;
}
private void label(JsonObject o) throws Exception {
Address a = addr(s(o, "addr"));
String name = s(o, "name");
SourceType src = SourceType.valueOf(s(o, "src"));
if (src == SourceType.DEFAULT) src = SourceType.USER_DEFINED;
SymbolTable st = currentProgram.getSymbolTable();
for (Symbol sym : st.getSymbols(a))
if (sym.getName().equals(name)) {
if (bo(o, "primary") && !sym.isPrimary()) { sym.setPrimary(); nLabels++; } else unchanged++;
return;
}
Function f = currentProgram.getFunctionManager().getFunctionAt(a);
if (bo(o, "func") && f != null) { f.setName(name, src); nLabels++; return; }
Symbol created = st.createLabel(a, name, src);
if (bo(o, "primary")) created.setPrimary();
nLabels++;
}
}
+3 -1
View File
@@ -17,7 +17,9 @@ import java.util.Iterator;
public class ImportPsyqGdt extends GhidraScript {
@Override
public void run() throws Exception {
String gdt = "/home/musashi/ghidra_12.1_PUBLIC/Ghidra/Extensions/ghidra_psx_ldr/data/psyq400.gdt";
// P33 B5: default = THIS Ghidra installation's ghidra_psx_ldr extension (no home-directory assumption)
String gdt = new File(ghidra.framework.Application.getInstallationDirectory().getFile(false),
"Ghidra/Extensions/ghidra_psx_ldr/data/psyq400.gdt").getPath();
String[] a = getScriptArgs();
if (a.length > 0) gdt = a[0];
File f = new File(gdt);