149 lines
4.7 KiB
Python
149 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from collections.abc import Mapping
|
|
|
|
from .formatting import h16, parse_int
|
|
from .memory import region_for
|
|
from .model import Instruction
|
|
from .rom import Rom
|
|
|
|
|
|
INDEXED_WORD_LOAD_RE = re.compile(r"^@\((?P<base>[^,]+),\s*(?P<index>R[0-7])\),\s*(?P<dest>R[0-7])$")
|
|
INDIRECT_FLOW_RE = re.compile(r"^@(?P<reg>R[0-7])$")
|
|
|
|
|
|
def analyze_indirect_flow(
|
|
rom: Rom,
|
|
instructions: Mapping[int, Instruction],
|
|
labels: Mapping[int, str] | None = None,
|
|
*,
|
|
max_entries: int = 128,
|
|
) -> dict[str, object]:
|
|
labels = labels or {}
|
|
ordered = [instructions[address] for address in sorted(instructions)]
|
|
by_address = {ins.address: ins for ins in ordered}
|
|
known_code = set(by_address)
|
|
sites: list[dict[str, object]] = []
|
|
|
|
for index, ins in enumerate(ordered):
|
|
if ins.kind not in {"call", "jump"} or ins.targets:
|
|
continue
|
|
target_reg = _indirect_target_register(ins.operands)
|
|
if target_reg is None:
|
|
continue
|
|
previous = ordered[index - 1] if index else None
|
|
table = _table_from_previous_load(rom, previous, target_reg, known_code, labels, max_entries)
|
|
site: dict[str, object] = {
|
|
"address": ins.address,
|
|
"instruction": ins.text,
|
|
"kind": ins.kind,
|
|
"target_register": target_reg,
|
|
"confidence": "table_load" if table else "unknown",
|
|
}
|
|
if table:
|
|
site["table"] = table
|
|
site["summary"] = _site_summary(ins, target_reg, table)
|
|
else:
|
|
site["summary"] = f"{ins.text} uses {target_reg}; target not resolved"
|
|
sites.append(site)
|
|
|
|
return {"sites": sites}
|
|
|
|
|
|
def indirect_comment_for_instruction(analysis: Mapping[str, object] | None, address: int) -> str:
|
|
if not analysis:
|
|
return ""
|
|
for site in analysis.get("sites", []):
|
|
if isinstance(site, Mapping) and int(site.get("address", -1)) == address:
|
|
return str(site.get("summary", ""))
|
|
return ""
|
|
|
|
|
|
def indirect_metadata_for_instruction(
|
|
analysis: Mapping[str, object] | None,
|
|
address: int,
|
|
) -> dict[str, object] | None:
|
|
if not analysis:
|
|
return None
|
|
for site in analysis.get("sites", []):
|
|
if isinstance(site, dict) and int(site.get("address", -1)) == address:
|
|
return site
|
|
return None
|
|
|
|
|
|
def _indirect_target_register(operands: str) -> str | None:
|
|
match = INDIRECT_FLOW_RE.match(operands.strip())
|
|
return match.group("reg") if match else None
|
|
|
|
|
|
def _table_from_previous_load(
|
|
rom: Rom,
|
|
previous: Instruction | None,
|
|
target_reg: str,
|
|
known_code: set[int],
|
|
labels: Mapping[int, str],
|
|
max_entries: int,
|
|
) -> dict[str, object] | None:
|
|
if previous is None or not previous.mnemonic.startswith("MOV:G.W"):
|
|
return None
|
|
match = INDEXED_WORD_LOAD_RE.match(previous.operands.strip())
|
|
if not match or match.group("dest") != target_reg:
|
|
return None
|
|
try:
|
|
base = parse_int(match.group("base"))
|
|
except ValueError:
|
|
return None
|
|
if not rom.contains(base, 2):
|
|
return None
|
|
|
|
entries: list[dict[str, object]] = []
|
|
for entry_index in range(max_entries):
|
|
address = base + entry_index * 2
|
|
if not rom.contains(address, 2):
|
|
break
|
|
target = rom.u16(address)
|
|
if target in (0x0000, 0xFFFF):
|
|
break
|
|
region = region_for(target)
|
|
is_code = target in known_code
|
|
plausible = is_code or region.kind == "program"
|
|
if not plausible and entries:
|
|
break
|
|
entries.append(
|
|
{
|
|
"index": entry_index,
|
|
"entry_address": address,
|
|
"target": target,
|
|
"target_label": labels.get(target),
|
|
"target_region": region.name,
|
|
"decoded_code": is_code,
|
|
},
|
|
)
|
|
if not plausible:
|
|
break
|
|
|
|
if not entries:
|
|
return None
|
|
decoded = sum(1 for entry in entries if entry["decoded_code"])
|
|
return {
|
|
"base": base,
|
|
"index_register": match.group("index"),
|
|
"target_register": target_reg,
|
|
"load_address": previous.address,
|
|
"load_instruction": previous.text,
|
|
"entry_size": 2,
|
|
"entry_count": len(entries),
|
|
"decoded_target_count": decoded,
|
|
"entries": entries,
|
|
}
|
|
|
|
|
|
def _site_summary(ins: Instruction, target_reg: str, table: Mapping[str, object]) -> str:
|
|
decoded = int(table["decoded_target_count"])
|
|
total = int(table["entry_count"])
|
|
return (
|
|
f"{ins.text} uses {target_reg} loaded from pointer table {h16(int(table['base']))} "
|
|
f"via {table['index_register']} ({decoded}/{total} decoded targets)"
|
|
)
|