858 lines
32 KiB
Python
858 lines
32 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from collections.abc import Iterable, Mapping
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .formatting import h16, label_for
|
|
from .serial_semantics import DIRECT_TABLE_TO_LOGICAL_OFFSET, LOGICAL_TABLES
|
|
|
|
|
|
JsonObject = dict[str, Any]
|
|
|
|
|
|
TABLES: tuple[JsonObject, ...] = (
|
|
{
|
|
"name": "primary_value_table_candidate",
|
|
"logical_base_address": 0xE000,
|
|
"logical_range_end": 0xE3FF,
|
|
"negative_offset": 0x2000,
|
|
"element_candidate": "word_value",
|
|
"direct_addresses": [0xF900],
|
|
"direct_range_end": 0xF91F,
|
|
},
|
|
{
|
|
"name": "secondary_value_table_candidate",
|
|
"logical_base_address": 0xE400,
|
|
"logical_range_end": 0xE7FF,
|
|
"negative_offset": 0x1C00,
|
|
"element_candidate": "word_value",
|
|
"direct_addresses": [0xF940],
|
|
"direct_range_end": 0xF95F,
|
|
},
|
|
{
|
|
"name": "current_value_table_candidate",
|
|
"logical_base_address": 0xE800,
|
|
"logical_range_end": 0xEBFF,
|
|
"negative_offset": 0x1800,
|
|
"element_candidate": "word_value",
|
|
"direct_addresses": [0xF920],
|
|
"direct_range_end": 0xF93F,
|
|
},
|
|
{
|
|
"name": "flag_table_candidate",
|
|
"logical_base_address": 0xEC00,
|
|
"logical_range_end": 0xEFFF,
|
|
"negative_offset": 0x1400,
|
|
"element_candidate": "bit_flags",
|
|
"direct_addresses": [0xF980],
|
|
"direct_range_end": 0xF99F,
|
|
},
|
|
)
|
|
|
|
_TABLE_BY_NEGATIVE_OFFSET = {int(item["negative_offset"]): item for item in TABLES}
|
|
_TABLE_BY_DIRECT_ADDRESS = {
|
|
address: item
|
|
for item in TABLES
|
|
for address in item["direct_addresses"]
|
|
}
|
|
LCD_CORRELATION_TERMS = (
|
|
"CONNECT",
|
|
"CONNECT: OK",
|
|
"CONNECT: NOT ACT",
|
|
"NOT ACT",
|
|
"COMM LINK",
|
|
"COMPLETED",
|
|
)
|
|
|
|
|
|
def load_table_xref_input(path: Path) -> JsonObject:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
if not isinstance(payload, dict) or "instructions" not in payload:
|
|
raise ValueError(f"{path} does not look like h8536_decompiler JSON output")
|
|
return payload
|
|
|
|
|
|
def analyze_table_xrefs(payload: Mapping[str, Any]) -> JsonObject:
|
|
instructions = _instruction_sequence(payload.get("instructions"))
|
|
functions = _function_ranges(payload)
|
|
semantic_accesses = _semantic_access_locations(payload)
|
|
accesses_by_table = {str(table["name"]): [] for table in TABLES}
|
|
|
|
for index, ins in enumerate(instructions):
|
|
for access in _logical_operand_accesses(instructions, index, functions, semantic_accesses):
|
|
accesses_by_table.setdefault(str(access["table"]), []).append(access)
|
|
for access in _direct_address_accesses(ins, functions, semantic_accesses):
|
|
accesses_by_table.setdefault(str(access["table"]), []).append(access)
|
|
|
|
tables: list[JsonObject] = []
|
|
for table in TABLES:
|
|
name = str(table["name"])
|
|
accesses = sorted(accesses_by_table.get(name, []), key=lambda item: int(item["instruction_address"]))
|
|
reads = sum(1 for access in accesses if access["access"] == "read")
|
|
writes = sum(1 for access in accesses if access["access"] == "write")
|
|
read_write = sum(1 for access in accesses if access["access"] == "read_write_candidate")
|
|
dynamic = sum(1 for access in accesses if access.get("index") == "dynamic")
|
|
static_offsets = sorted(
|
|
{
|
|
int(access["offset"])
|
|
for access in accesses
|
|
if isinstance(access.get("offset"), int)
|
|
}
|
|
)
|
|
tables.append(
|
|
{
|
|
"name": name,
|
|
"logical_base_address": table["logical_base_address"],
|
|
"logical_base_address_hex": h16(int(table["logical_base_address"])),
|
|
"logical_range_end": table["logical_range_end"],
|
|
"logical_range_end_hex": h16(int(table["logical_range_end"])),
|
|
"negative_offset": table["negative_offset"],
|
|
"negative_offset_hex": h16(int(table["negative_offset"])),
|
|
"element_candidate": table["element_candidate"],
|
|
"direct_addresses": table["direct_addresses"],
|
|
"direct_addresses_hex": [h16(int(address)) for address in table["direct_addresses"]],
|
|
"direct_range_end": table["direct_range_end"],
|
|
"direct_range_end_hex": h16(int(table["direct_range_end"])),
|
|
"access_count": len(accesses),
|
|
"read_count": reads,
|
|
"write_count": writes,
|
|
"read_write_candidate_count": read_write,
|
|
"dynamic_index_count": dynamic,
|
|
"static_offsets": static_offsets,
|
|
"static_offsets_hex": [h16(offset) for offset in static_offsets],
|
|
"functions": _summarize_functions(accesses),
|
|
"accesses": accesses,
|
|
}
|
|
)
|
|
|
|
return {
|
|
"kind": "table_xrefs",
|
|
"tables": tables,
|
|
"summary": {
|
|
"table_count": len(tables),
|
|
"access_count": sum(int(table["access_count"]) for table in tables),
|
|
"dynamic_index_count": sum(int(table["dynamic_index_count"]) for table in tables),
|
|
"source_instruction_count": len(instructions),
|
|
},
|
|
"lcd_correlation": _lcd_correlation_hints(payload),
|
|
"caveat": (
|
|
"Static offsets are emitted only when an index register value can be derived from "
|
|
"nearby immediate loads in the current JSON. Other indexed accesses are dynamic."
|
|
),
|
|
}
|
|
|
|
|
|
def generate_table_xref_report(payload: Mapping[str, Any], *, source_name: str = "") -> str:
|
|
analysis = analyze_table_xrefs(payload)
|
|
lines: list[str] = []
|
|
suffix = f" for {source_name}" if source_name else ""
|
|
lines.append(f"Table/Index Cross-Reference Report{suffix}")
|
|
lines.append("=" * len(lines[0]))
|
|
lines.append("")
|
|
lines.append(str(analysis["caveat"]))
|
|
lines.append("")
|
|
lines.extend(_format_lcd_correlation_lines(analysis.get("lcd_correlation")))
|
|
if lines[-1] != "":
|
|
lines.append("")
|
|
|
|
for table in analysis["tables"]:
|
|
name = str(table["name"])
|
|
direct = ", ".join(str(item) for item in table["direct_addresses_hex"])
|
|
lines.append(
|
|
f"{name} {table['logical_base_address_hex']}-{table['logical_range_end_hex']} "
|
|
f"(negative {table['negative_offset_hex']}; direct {direct}-{table['direct_range_end_hex']})"
|
|
)
|
|
lines.append(
|
|
f" accesses={table['access_count']} reads={table['read_count']} "
|
|
f"writes={table['write_count']} dynamic={table['dynamic_index_count']}"
|
|
)
|
|
offsets = table.get("static_offsets_hex") or []
|
|
if offsets:
|
|
lines.append(f" static offsets: {', '.join(str(item) for item in offsets[:16])}")
|
|
function_summaries = table.get("functions") or []
|
|
if function_summaries:
|
|
joined = ", ".join(
|
|
f"{item['label']}:{item['access_count']}" for item in function_summaries[:12]
|
|
)
|
|
lines.append(f" functions: {joined}")
|
|
accesses = table.get("accesses")
|
|
if isinstance(accesses, list) and accesses:
|
|
for access in accesses[:80]:
|
|
lines.append(f" - {_format_access_line(access)}")
|
|
if len(accesses) > 80:
|
|
lines.append(f" - ... {len(accesses) - 80} more accesses omitted")
|
|
else:
|
|
lines.append(" no references found in current JSON")
|
|
lines.append("")
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def _format_lcd_correlation_lines(value: Any) -> list[str]:
|
|
if not isinstance(value, Mapping):
|
|
return []
|
|
lines = ["LCD correlation hints"]
|
|
for hit in value.get("term_hits", []):
|
|
if not isinstance(hit, Mapping):
|
|
continue
|
|
term = hit.get("term")
|
|
count = int(hit.get("hit_count", 0))
|
|
if count:
|
|
samples = ", ".join(
|
|
f"{item['address_hex']} {item['trimmed']!r}"
|
|
for item in hit.get("hits", [])[:4]
|
|
if isinstance(item, Mapping)
|
|
)
|
|
lines.append(f" term {term!r}: {count} candidate hit(s): {samples}")
|
|
else:
|
|
lines.append(f" term {term!r}: no LCD/text candidate hits in current decompile")
|
|
builders = value.get("display_builder_targets", [])
|
|
if isinstance(builders, list) and builders:
|
|
parts = [
|
|
f"{item['target_hex']}:{item['xref_count']}"
|
|
for item in builders[:8]
|
|
if isinstance(item, Mapping)
|
|
]
|
|
lines.append(f" display builder xrefs: {', '.join(parts)}")
|
|
routines = value.get("lcd_driver_routines", [])
|
|
if isinstance(routines, list) and routines:
|
|
parts = [
|
|
f"{item['start_hex']} {item['role_hint']}"
|
|
for item in routines[:4]
|
|
if isinstance(item, Mapping)
|
|
]
|
|
lines.append(f" LCD driver routines: {', '.join(parts)}")
|
|
lines.append(
|
|
" caveat: LCD strings can be builder/script output; absence of a literal term does not disprove runtime composition."
|
|
)
|
|
return lines
|
|
|
|
|
|
def write_table_xrefs(input_path: Path, output_path: Path, *, as_json: bool = False) -> None:
|
|
payload = load_table_xref_input(input_path)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
if as_json:
|
|
analysis = analyze_table_xrefs(payload)
|
|
analysis["source"] = str(input_path)
|
|
output_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8")
|
|
else:
|
|
output_path.write_text(generate_table_xref_report(payload, source_name=str(input_path)), encoding="utf-8")
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate table/index cross-references for candidate serial protocol data tables.",
|
|
)
|
|
parser.add_argument(
|
|
"input",
|
|
nargs="?",
|
|
type=Path,
|
|
default=Path("build/rom_decompiled.json"),
|
|
help="structured JSON emitted by h8536_decompiler.py",
|
|
)
|
|
parser.add_argument(
|
|
"--out",
|
|
type=Path,
|
|
default=Path("build/rom_table_xrefs.txt"),
|
|
help="table cross-reference report output path",
|
|
)
|
|
parser.add_argument("--json", action="store_true", help="write structured JSON instead of text")
|
|
args = parser.parse_args(argv)
|
|
|
|
write_table_xrefs(args.input, args.out, as_json=args.json)
|
|
print(f"wrote {args.out}")
|
|
return 0
|
|
|
|
|
|
def _logical_operand_accesses(
|
|
instructions: list[JsonObject],
|
|
index: int,
|
|
functions: list[JsonObject],
|
|
semantic_accesses: Mapping[int, list[JsonObject]],
|
|
) -> list[JsonObject]:
|
|
ins = instructions[index]
|
|
accesses: list[JsonObject] = []
|
|
operands = str(ins.get("operands", ""))
|
|
for operand in _negative_indexed_operands(operands):
|
|
table = _TABLE_BY_NEGATIVE_OFFSET.get(int(operand["negative_offset"]))
|
|
if table is None:
|
|
continue
|
|
register = str(operand["index_register"])
|
|
known = _nearby_register_value(instructions, index, register)
|
|
offset: int | str = known if known is not None else "dynamic"
|
|
logical_address: int | None = None
|
|
if isinstance(offset, int):
|
|
logical_address = (int(table["logical_base_address"]) + offset) & 0xFFFF
|
|
selector = _selector_for_table_offset(table, offset)
|
|
access = _base_access(ins, functions, semantic_accesses)
|
|
access.update(
|
|
{
|
|
"table": table["name"],
|
|
"table_base_address": table["logical_base_address"],
|
|
"table_base_address_hex": h16(int(table["logical_base_address"])),
|
|
"kind": "logical_negative_indexed_access",
|
|
"operand": operand["operand"],
|
|
"negative_offset": operand["negative_offset"],
|
|
"negative_offset_hex": h16(int(operand["negative_offset"])),
|
|
"index_register": register,
|
|
"index": offset,
|
|
"offset": offset,
|
|
"access": _operand_access_kind(ins, str(operand["operand"])),
|
|
}
|
|
)
|
|
if logical_address is not None:
|
|
access["logical_address"] = logical_address
|
|
access["logical_address_hex"] = h16(logical_address)
|
|
if selector is not None:
|
|
access["selector"] = selector
|
|
access["selector_hex"] = f"0x{selector:03X}"
|
|
accesses.append(access)
|
|
return accesses
|
|
|
|
|
|
def _direct_address_accesses(
|
|
ins: Mapping[str, Any],
|
|
functions: list[JsonObject],
|
|
semantic_accesses: Mapping[int, list[JsonObject]],
|
|
) -> list[JsonObject]:
|
|
accesses: list[JsonObject] = []
|
|
refs = _references(ins)
|
|
for address in refs:
|
|
logical_table = _table_for_logical_address(address)
|
|
if logical_table is not None:
|
|
accesses.append(
|
|
_direct_logical_address_access(ins, logical_table, address, functions, semantic_accesses),
|
|
)
|
|
continue
|
|
|
|
direct_table = _table_for_direct_candidate_address(address)
|
|
if direct_table is not None:
|
|
accesses.append(
|
|
_direct_candidate_address_access(ins, direct_table, address, functions, semantic_accesses),
|
|
)
|
|
return accesses
|
|
|
|
|
|
def _direct_logical_address_access(
|
|
ins: Mapping[str, Any],
|
|
table: Mapping[str, Any],
|
|
address: int,
|
|
functions: list[JsonObject],
|
|
semantic_accesses: Mapping[int, list[JsonObject]],
|
|
) -> JsonObject:
|
|
base = int(table["logical_base_address"])
|
|
offset = address - base
|
|
selector = _selector_for_table_offset(table, offset)
|
|
access = _base_access(ins, functions, semantic_accesses)
|
|
access.update(
|
|
{
|
|
"table": table["name"],
|
|
"table_base_address": base,
|
|
"table_base_address_hex": h16(base),
|
|
"kind": "direct_logical_address_access",
|
|
"direct_address": address,
|
|
"direct_address_hex": h16(address),
|
|
"logical_address": address,
|
|
"logical_address_hex": h16(address),
|
|
"index": offset,
|
|
"offset": offset,
|
|
"offset_hex": h16(offset),
|
|
"access": _access_direction(ins, address) or "read_write_candidate",
|
|
}
|
|
)
|
|
if selector is not None:
|
|
access["selector"] = selector
|
|
access["selector_hex"] = f"0x{selector:03X}"
|
|
return access
|
|
|
|
|
|
def _direct_candidate_address_access(
|
|
ins: Mapping[str, Any],
|
|
table: Mapping[str, Any],
|
|
address: int,
|
|
functions: list[JsonObject],
|
|
semantic_accesses: Mapping[int, list[JsonObject]],
|
|
) -> JsonObject:
|
|
base = min(int(item) for item in table["direct_addresses"])
|
|
offset = address - base
|
|
access = _base_access(ins, functions, semantic_accesses)
|
|
logical_offset = DIRECT_TABLE_TO_LOGICAL_OFFSET.get(base)
|
|
selector = _selector_for_table_offset(table, offset)
|
|
access.update(
|
|
{
|
|
"table": table["name"],
|
|
"table_base_address": table["logical_base_address"],
|
|
"table_base_address_hex": h16(int(table["logical_base_address"])),
|
|
"kind": "direct_candidate_address_access",
|
|
"direct_address": address,
|
|
"direct_address_hex": h16(address),
|
|
"direct_base_address": base,
|
|
"direct_base_address_hex": h16(base),
|
|
"index": offset,
|
|
"offset": offset,
|
|
"offset_hex": h16(offset),
|
|
"access": _access_direction(ins, address) or "read_write_candidate",
|
|
}
|
|
)
|
|
if logical_offset is not None:
|
|
access["semantic_negative_offset"] = logical_offset
|
|
access["semantic_negative_offset_hex"] = h16(logical_offset)
|
|
if selector is not None:
|
|
access["selector"] = selector
|
|
access["selector_hex"] = f"0x{selector:03X}"
|
|
return access
|
|
|
|
|
|
def _lcd_correlation_hints(payload: Mapping[str, Any]) -> JsonObject:
|
|
lcd_text = payload.get("lcd_text")
|
|
strings = []
|
|
if isinstance(lcd_text, Mapping) and isinstance(lcd_text.get("strings"), list):
|
|
strings = [item for item in lcd_text["strings"] if isinstance(item, Mapping)]
|
|
|
|
term_hits = []
|
|
for term in LCD_CORRELATION_TERMS:
|
|
hits = []
|
|
upper_term = term.upper()
|
|
for item in strings:
|
|
text = f"{item.get('text', '')} {item.get('trimmed', '')}".upper()
|
|
if upper_term not in text:
|
|
continue
|
|
hits.append(_lcd_string_summary(item))
|
|
term_hits.append(
|
|
{
|
|
"term": term,
|
|
"hit_count": len(hits),
|
|
"hits": hits[:24],
|
|
"status": "candidate_hits" if hits else "not_found",
|
|
}
|
|
)
|
|
|
|
builder_targets: dict[int, JsonObject] = {}
|
|
for item in strings:
|
|
for xref in item.get("xrefs", []):
|
|
if not isinstance(xref, Mapping):
|
|
continue
|
|
following = xref.get("following_bsr")
|
|
if not isinstance(following, Mapping) or not isinstance(following.get("target"), int):
|
|
continue
|
|
target = int(following["target"])
|
|
record = builder_targets.setdefault(
|
|
target,
|
|
{
|
|
"target": target,
|
|
"target_hex": h16(target),
|
|
"xref_count": 0,
|
|
"examples": [],
|
|
},
|
|
)
|
|
record["xref_count"] = int(record["xref_count"]) + 1
|
|
examples = record["examples"]
|
|
if isinstance(examples, list) and len(examples) < 8:
|
|
examples.append(
|
|
{
|
|
"text_address": item.get("address"),
|
|
"text_address_hex": h16(int(item["address"])) if isinstance(item.get("address"), int) else None,
|
|
"trimmed": item.get("trimmed"),
|
|
"xref_address": xref.get("address"),
|
|
"xref_address_hex": h16(int(xref["address"])) if isinstance(xref.get("address"), int) else None,
|
|
}
|
|
)
|
|
|
|
lcd_driver = payload.get("lcd_driver")
|
|
routines = []
|
|
if isinstance(lcd_driver, Mapping) and isinstance(lcd_driver.get("routines"), list):
|
|
for routine in lcd_driver["routines"]:
|
|
if not isinstance(routine, Mapping) or not isinstance(routine.get("start"), int):
|
|
continue
|
|
routines.append(
|
|
{
|
|
"start": routine["start"],
|
|
"start_hex": h16(int(routine["start"])),
|
|
"end": routine.get("end"),
|
|
"end_hex": h16(int(routine["end"])) if isinstance(routine.get("end"), int) else None,
|
|
"role_hint": routine.get("role_hint"),
|
|
"roles": routine.get("roles", []),
|
|
}
|
|
)
|
|
|
|
return {
|
|
"terms": list(LCD_CORRELATION_TERMS),
|
|
"term_hits": term_hits,
|
|
"display_builder_targets": sorted(
|
|
builder_targets.values(),
|
|
key=lambda item: (-int(item["xref_count"]), int(item["target"])),
|
|
),
|
|
"lcd_driver_routines": routines,
|
|
"caveat": (
|
|
"This is a static correlation helper. It reports text/script candidates and LCD driver "
|
|
"routines in the same decompile; it does not prove a protocol field directly causes a string."
|
|
),
|
|
}
|
|
|
|
|
|
def _lcd_string_summary(item: Mapping[str, Any]) -> JsonObject:
|
|
address = item.get("address")
|
|
return {
|
|
"address": address,
|
|
"address_hex": h16(int(address)) if isinstance(address, int) else None,
|
|
"text": item.get("text"),
|
|
"trimmed": item.get("trimmed"),
|
|
"confidence": item.get("confidence"),
|
|
"xref_count": item.get("xref_count", 0),
|
|
}
|
|
|
|
|
|
def _base_access(
|
|
ins: Mapping[str, Any],
|
|
functions: list[JsonObject],
|
|
semantic_accesses: Mapping[int, list[JsonObject]],
|
|
) -> JsonObject:
|
|
address = int(ins["address"])
|
|
function = _function_for_address(functions, address)
|
|
access: JsonObject = {
|
|
"instruction_address": address,
|
|
"instruction_address_hex": h16(address),
|
|
"mnemonic": str(ins.get("mnemonic", "")),
|
|
"operands": str(ins.get("operands", "")),
|
|
"instruction": str(ins.get("text") or _instruction_text(ins)),
|
|
"references": _references(ins),
|
|
"references_hex": [h16(ref) for ref in _references(ins)],
|
|
"targets": _targets(ins),
|
|
"targets_hex": [h16(target) for target in _targets(ins)],
|
|
"label": _label_for_instruction(ins),
|
|
"semantic_candidates": semantic_accesses.get(address, []),
|
|
}
|
|
if function:
|
|
access["function_start"] = function["start"]
|
|
access["function_start_hex"] = h16(int(function["start"]))
|
|
access["function_label"] = function["label"]
|
|
return access
|
|
|
|
|
|
def _semantic_access_locations(payload: Mapping[str, Any]) -> dict[int, list[JsonObject]]:
|
|
locations: dict[int, list[JsonObject]] = {}
|
|
semantics = payload.get("serial_semantics")
|
|
if not isinstance(semantics, Mapping):
|
|
return locations
|
|
sources: list[Any] = []
|
|
protocols = semantics.get("protocol_semantics")
|
|
if isinstance(protocols, list):
|
|
sources.extend(protocols)
|
|
sources.append(semantics)
|
|
for source in sources:
|
|
if not isinstance(source, Mapping):
|
|
continue
|
|
for item in _table_candidate_items(source.get("table_map_candidates")):
|
|
for access in _table_candidate_items(item.get("accesses")):
|
|
address = access.get("instruction_address")
|
|
if isinstance(address, int):
|
|
locations.setdefault(address, []).append(
|
|
{
|
|
"name_candidate": item.get("name_candidate"),
|
|
"kind": item.get("kind"),
|
|
"confidence": item.get("confidence"),
|
|
}
|
|
)
|
|
return locations
|
|
|
|
|
|
def _table_candidate_items(value: Any) -> list[Mapping[str, Any]]:
|
|
if isinstance(value, Mapping):
|
|
return [item for item in value.values() if isinstance(item, Mapping)]
|
|
if isinstance(value, list):
|
|
return [item for item in value if isinstance(item, Mapping)]
|
|
return []
|
|
|
|
|
|
def _format_access_line(access: Mapping[str, Any]) -> str:
|
|
function = access.get("function_label") or "<no function>"
|
|
operand = access.get("operand") or access.get("direct_address_hex")
|
|
index = access.get("index")
|
|
if index == "dynamic":
|
|
index_text = f"index dynamic via {access.get('index_register')} operand {operand}"
|
|
else:
|
|
index_text = f"offset {h16(int(index or 0))}"
|
|
if access.get("selector_hex"):
|
|
index_text += f" selector {access['selector_hex']}"
|
|
if access.get("logical_address_hex"):
|
|
index_text += f" -> {access['logical_address_hex']}"
|
|
elif access.get("direct_address_hex"):
|
|
index_text += f" at {access['direct_address_hex']}"
|
|
return (
|
|
f"{access['instruction_address_hex']} {access['access']} {index_text}; "
|
|
f"{function}; {access['instruction']}"
|
|
)
|
|
|
|
|
|
def _summarize_functions(accesses: Iterable[Mapping[str, Any]]) -> list[JsonObject]:
|
|
summaries: dict[int, JsonObject] = {}
|
|
for access in accesses:
|
|
start = access.get("function_start")
|
|
if not isinstance(start, int):
|
|
start = -1
|
|
summary = summaries.setdefault(
|
|
start,
|
|
{
|
|
"start": start if start >= 0 else None,
|
|
"start_hex": h16(start) if start >= 0 else None,
|
|
"label": access.get("function_label") or "<no function>",
|
|
"access_count": 0,
|
|
"reads": 0,
|
|
"writes": 0,
|
|
},
|
|
)
|
|
summary["access_count"] = int(summary["access_count"]) + 1
|
|
if access.get("access") == "read":
|
|
summary["reads"] = int(summary["reads"]) + 1
|
|
elif access.get("access") == "write":
|
|
summary["writes"] = int(summary["writes"]) + 1
|
|
return sorted(summaries.values(), key=lambda item: (-int(item["access_count"]), str(item["label"])))
|
|
|
|
|
|
def _selector_for_table_offset(table: Mapping[str, Any], offset: int | str) -> int | None:
|
|
if not isinstance(offset, int):
|
|
return None
|
|
element = str(table.get("element_candidate") or "")
|
|
if element == "word_value":
|
|
if offset % 2:
|
|
return None
|
|
return (offset // 2) & 0x01FF
|
|
if element == "bit_flags":
|
|
return offset & 0x01FF
|
|
return None
|
|
|
|
|
|
def _function_ranges(payload: Mapping[str, Any]) -> list[JsonObject]:
|
|
call_graph = payload.get("call_graph")
|
|
if not isinstance(call_graph, Mapping):
|
|
return []
|
|
nodes = call_graph.get("nodes")
|
|
if not isinstance(nodes, list):
|
|
return []
|
|
ranges: list[JsonObject] = []
|
|
for node in nodes:
|
|
if not isinstance(node, Mapping):
|
|
continue
|
|
start = node.get("start")
|
|
end = node.get("end")
|
|
if isinstance(start, int) and isinstance(end, int):
|
|
ranges.append({"start": start, "end": end, "label": str(node.get("label") or label_for(start))})
|
|
return sorted(ranges, key=lambda item: int(item["start"]))
|
|
|
|
|
|
def _function_for_address(functions: list[JsonObject], address: int) -> JsonObject | None:
|
|
for function in functions:
|
|
if int(function["start"]) <= address <= int(function["end"]):
|
|
return function
|
|
return None
|
|
|
|
|
|
def _nearby_register_value(instructions: list[JsonObject], index: int, register: str) -> int | None:
|
|
register = register.upper()
|
|
for prior_index in range(index - 1, max(-1, index - 10), -1):
|
|
prior = instructions[prior_index]
|
|
source, destination = _source_destination_operands(str(prior.get("operands", "")))
|
|
if destination.upper() != register:
|
|
continue
|
|
value = _parse_immediate(source)
|
|
if value is not None:
|
|
return value
|
|
if _writes_register(prior, register):
|
|
return None
|
|
return None
|
|
|
|
|
|
def _writes_register(ins: Mapping[str, Any], register: str) -> bool:
|
|
_source, destination = _source_destination_operands(str(ins.get("operands", "")))
|
|
return destination.upper() == register
|
|
|
|
|
|
def _instruction_sequence(value: object) -> list[JsonObject]:
|
|
if isinstance(value, Mapping):
|
|
values: Iterable[Any] = value.values()
|
|
elif isinstance(value, list):
|
|
values = value
|
|
else:
|
|
values = []
|
|
return sorted(
|
|
[item for item in values if isinstance(item, dict) and isinstance(item.get("address"), int)],
|
|
key=lambda item: int(item["address"]),
|
|
)
|
|
|
|
|
|
def _label_for_instruction(ins: Mapping[str, Any]) -> str | None:
|
|
address = int(ins["address"])
|
|
for key in ("label", "target_label"):
|
|
value = ins.get(key)
|
|
if isinstance(value, str) and value:
|
|
return value
|
|
if _targets(ins):
|
|
return label_for(address)
|
|
return None
|
|
|
|
|
|
def _instruction_text(ins: Mapping[str, Any]) -> str:
|
|
operands = str(ins.get("operands", ""))
|
|
return f"{ins.get('mnemonic', '')} {operands}".strip()
|
|
|
|
|
|
def _references(ins: Mapping[str, Any]) -> list[int]:
|
|
refs = ins.get("references", [])
|
|
if not isinstance(refs, list):
|
|
return []
|
|
output: list[int] = []
|
|
for ref in refs:
|
|
if isinstance(ref, Mapping) and isinstance(ref.get("address"), int):
|
|
output.append(int(ref["address"]))
|
|
elif isinstance(ref, int):
|
|
output.append(ref)
|
|
return output
|
|
|
|
|
|
def _targets(ins: Mapping[str, Any]) -> list[int]:
|
|
targets = ins.get("targets", [])
|
|
if not isinstance(targets, list):
|
|
return []
|
|
return [int(target) for target in targets if isinstance(target, int)]
|
|
|
|
|
|
def _negative_indexed_operands(operands: str) -> list[JsonObject]:
|
|
matches: list[JsonObject] = []
|
|
for match in re.finditer(r"@\(-H'([0-9A-Fa-f]+),\s*(R[0-7])\)", operands):
|
|
offset = int(match.group(1), 16) & 0xFFFF
|
|
if offset not in LOGICAL_TABLES:
|
|
continue
|
|
matches.append(
|
|
{
|
|
"operand": match.group(0),
|
|
"negative_offset": offset,
|
|
"index_register": match.group(2).upper(),
|
|
}
|
|
)
|
|
return matches
|
|
|
|
|
|
def _table_for_logical_address(address: int) -> Mapping[str, Any] | None:
|
|
for table in TABLES:
|
|
if int(table["logical_base_address"]) <= address <= int(table["logical_range_end"]):
|
|
return table
|
|
return None
|
|
|
|
|
|
def _table_for_direct_candidate_address(address: int) -> Mapping[str, Any] | None:
|
|
for table in TABLES:
|
|
direct_addresses = [int(item) for item in table["direct_addresses"]]
|
|
if min(direct_addresses) <= address <= int(table["direct_range_end"]):
|
|
return table
|
|
return None
|
|
|
|
|
|
def _operand_access_kind(ins: Mapping[str, Any], operand: str) -> str:
|
|
root = _mnemonic_root(str(ins.get("mnemonic", "")))
|
|
source, destination = _source_destination_operands(str(ins.get("operands", "")))
|
|
if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "TST"}:
|
|
return "read"
|
|
if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}:
|
|
return "write"
|
|
if operand in destination and operand not in source:
|
|
return "write"
|
|
if operand in source and operand not in destination:
|
|
return "read"
|
|
if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
|
|
return "write"
|
|
return "read_write_candidate"
|
|
|
|
|
|
def _access_direction(ins: Mapping[str, Any], address: int) -> str | None:
|
|
root = _mnemonic_root(str(ins.get("mnemonic", "")))
|
|
if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}:
|
|
return "read"
|
|
if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}:
|
|
return "write"
|
|
if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
|
|
return "write"
|
|
if root in {"MOV:G", "MOV:S", "MOVTPE"}:
|
|
source, destination = _source_destination_operands(str(ins.get("operands", "")))
|
|
if _operand_mentions_address(destination, address):
|
|
return "write"
|
|
if _operand_mentions_address(source, address):
|
|
return "read"
|
|
if address in _references(ins):
|
|
if destination.startswith("@") and not _operand_mentions_any_reference(source, _references(ins)):
|
|
return "write"
|
|
if source.startswith("@") and not _operand_mentions_any_reference(destination, _references(ins)):
|
|
return "read"
|
|
if root in {"MOV:L", "MOV:F"}:
|
|
return "read"
|
|
if root == "STC":
|
|
return "write"
|
|
if root == "LDC":
|
|
return "read"
|
|
return None
|
|
|
|
|
|
def _source_destination_operands(operands: str) -> tuple[str, str]:
|
|
depth = 0
|
|
split_at: int | None = None
|
|
for index, char in enumerate(operands):
|
|
if char in "({":
|
|
depth += 1
|
|
elif char in ")}" and depth:
|
|
depth -= 1
|
|
elif char == "," and depth == 0:
|
|
split_at = index
|
|
if split_at is None:
|
|
operand = operands.strip()
|
|
return "", operand
|
|
return operands[:split_at].strip(), operands[split_at + 1 :].strip()
|
|
|
|
|
|
def _parse_immediate(operand: str) -> int | None:
|
|
text = operand.strip()
|
|
if text.startswith("#"):
|
|
text = text[1:].strip()
|
|
try:
|
|
if text.upper().startswith("H'"):
|
|
return int(text[2:], 16) & 0xFFFF
|
|
if text.upper().startswith("0X"):
|
|
return int(text, 16) & 0xFFFF
|
|
if text.upper().startswith("$"):
|
|
return int(text[1:], 16) & 0xFFFF
|
|
return int(text, 10) & 0xFFFF
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _operand_mentions_any_reference(operand: str, references: list[int]) -> bool:
|
|
return any(_operand_mentions_address(operand, address) for address in references)
|
|
|
|
|
|
def _operand_mentions_address(operand: str, address: int) -> bool:
|
|
operand_upper = operand.upper().replace(" ", "")
|
|
negative = (0x10000 - address) & 0xFFFF
|
|
return (
|
|
f"H'{address:04X}" in operand_upper
|
|
or f"0X{address:04X}" in operand_upper
|
|
or f"${address:04X}" in operand_upper
|
|
or f"-H'{negative:04X}" in operand_upper
|
|
or f"-0X{negative:04X}" in operand_upper
|
|
or f"-${negative:04X}" in operand_upper
|
|
)
|
|
|
|
|
|
def _mnemonic_root(mnemonic: str) -> str:
|
|
return mnemonic.rsplit(".", 1)[0].upper()
|
|
|
|
|
|
__all__ = [
|
|
"analyze_table_xrefs",
|
|
"generate_table_xref_report",
|
|
"load_table_xref_input",
|
|
"main",
|
|
"write_table_xrefs",
|
|
]
|