Add hook debug tooling and refine RT3 atlas
This commit is contained in:
parent
860d1aed90
commit
57bf0666e0
38 changed files with 14437 additions and 873 deletions
260
tools/py/export_function_subgraph.py
Normal file
260
tools/py/export_function_subgraph.py
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import re
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ADDRESS_RE = re.compile(r"0x[0-9a-fA-F]{8}")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Row:
|
||||
address: int
|
||||
size: str
|
||||
name: str
|
||||
subsystem: str
|
||||
calling_convention: str
|
||||
prototype_status: str
|
||||
source_tool: str
|
||||
confidence: str
|
||||
notes: str
|
||||
verified_against: str
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Export a bounded function subgraph from function-map.csv notes."
|
||||
)
|
||||
parser.add_argument("function_map", type=Path)
|
||||
parser.add_argument("output_prefix", type=Path)
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Seed function address in hex. May be repeated.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--depth",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Traversal depth over note-address references.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--title",
|
||||
default="Function Subgraph",
|
||||
help="Title used in the markdown summary.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-backrefs",
|
||||
action="store_true",
|
||||
help="Also traverse rows that reference currently included nodes.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if not args.seed:
|
||||
parser.error("at least one --seed is required")
|
||||
return args
|
||||
|
||||
|
||||
def parse_hex(text: str) -> int:
|
||||
value = text.strip().lower()
|
||||
if value.startswith("0x"):
|
||||
value = value[2:]
|
||||
return int(value, 16)
|
||||
|
||||
|
||||
def fmt_addr(value: int) -> str:
|
||||
return f"0x{value:08x}"
|
||||
|
||||
|
||||
def load_rows(path: Path) -> dict[int, Row]:
|
||||
with path.open(newline="", encoding="utf-8") as handle:
|
||||
reader = csv.DictReader(handle)
|
||||
rows: dict[int, Row] = {}
|
||||
for raw in reader:
|
||||
address = parse_hex(raw["address"])
|
||||
rows[address] = Row(
|
||||
address=address,
|
||||
size=raw["size"],
|
||||
name=raw["name"],
|
||||
subsystem=raw["subsystem"],
|
||||
calling_convention=raw["calling_convention"],
|
||||
prototype_status=raw["prototype_status"],
|
||||
source_tool=raw["source_tool"],
|
||||
confidence=raw["confidence"],
|
||||
notes=raw["notes"],
|
||||
verified_against=raw["verified_against"],
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def extract_note_refs(rows: dict[int, Row]) -> dict[int, set[int]]:
|
||||
refs: dict[int, set[int]] = {}
|
||||
known = set(rows)
|
||||
for address, row in rows.items():
|
||||
hits = {parse_hex(match.group(0)) for match in ADDRESS_RE.finditer(row.notes)}
|
||||
refs[address] = {hit for hit in hits if hit in known and hit != address}
|
||||
return refs
|
||||
|
||||
|
||||
def build_backrefs(refs: dict[int, set[int]]) -> dict[int, set[int]]:
|
||||
backrefs: dict[int, set[int]] = {address: set() for address in refs}
|
||||
for src, dsts in refs.items():
|
||||
for dst in dsts:
|
||||
backrefs.setdefault(dst, set()).add(src)
|
||||
return backrefs
|
||||
|
||||
|
||||
def walk_subgraph(
|
||||
rows: dict[int, Row],
|
||||
refs: dict[int, set[int]],
|
||||
seeds: list[int],
|
||||
depth: int,
|
||||
include_backrefs: bool,
|
||||
) -> set[int]:
|
||||
backrefs = build_backrefs(refs)
|
||||
seen: set[int] = set()
|
||||
queue: deque[tuple[int, int]] = deque((seed, 0) for seed in seeds if seed in rows)
|
||||
|
||||
while queue:
|
||||
address, level = queue.popleft()
|
||||
if address in seen:
|
||||
continue
|
||||
seen.add(address)
|
||||
if level >= depth:
|
||||
continue
|
||||
|
||||
for dst in sorted(refs.get(address, ())):
|
||||
if dst not in seen:
|
||||
queue.append((dst, level + 1))
|
||||
|
||||
if include_backrefs:
|
||||
for src in sorted(backrefs.get(address, ())):
|
||||
if src not in seen:
|
||||
queue.append((src, level + 1))
|
||||
|
||||
return seen
|
||||
|
||||
|
||||
def quote_dot(text: str) -> str:
|
||||
return text.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
def emit_dot(
|
||||
rows: dict[int, Row],
|
||||
refs: dict[int, set[int]],
|
||||
included: set[int],
|
||||
seeds: set[int],
|
||||
output_path: Path,
|
||||
title: str,
|
||||
) -> None:
|
||||
subsystems: dict[str, list[Row]] = {}
|
||||
for address in sorted(included):
|
||||
row = rows[address]
|
||||
subsystems.setdefault(row.subsystem, []).append(row)
|
||||
|
||||
lines: list[str] = [
|
||||
"digraph shell_load {",
|
||||
' graph [rankdir=LR, labelloc="t", labeljust="l"];',
|
||||
f' label="{quote_dot(title)}";',
|
||||
' node [shape=box, style="rounded,filled", fillcolor="#f8f8f8", color="#555555", fontname="Helvetica"];',
|
||||
' edge [color="#666666", fontname="Helvetica"];',
|
||||
]
|
||||
|
||||
for subsystem in sorted(subsystems):
|
||||
cluster_id = subsystem.replace("-", "_")
|
||||
lines.append(f' subgraph cluster_{cluster_id} {{')
|
||||
lines.append(f' label="{quote_dot(subsystem)}";')
|
||||
lines.append(' color="#cccccc";')
|
||||
for row in subsystems[subsystem]:
|
||||
seed_mark = " [seed]" if row.address in seeds else ""
|
||||
label = f"{fmt_addr(row.address)}\\n{row.name}{seed_mark}"
|
||||
fill = "#ffe9a8" if row.address in seeds else "#f8f8f8"
|
||||
lines.append(
|
||||
f' "{fmt_addr(row.address)}" [label="{quote_dot(label)}", fillcolor="{fill}"];'
|
||||
)
|
||||
lines.append(" }")
|
||||
|
||||
for src in sorted(included):
|
||||
for dst in sorted(refs.get(src, ())):
|
||||
if dst not in included:
|
||||
continue
|
||||
lines.append(f' "{fmt_addr(src)}" -> "{fmt_addr(dst)}";')
|
||||
|
||||
lines.append("}")
|
||||
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def emit_markdown(
|
||||
rows: dict[int, Row],
|
||||
refs: dict[int, set[int]],
|
||||
included: set[int],
|
||||
seeds: set[int],
|
||||
output_path: Path,
|
||||
title: str,
|
||||
dot_path: Path,
|
||||
) -> None:
|
||||
included_rows = [rows[address] for address in sorted(included)]
|
||||
edge_count = sum(
|
||||
1
|
||||
for src in included
|
||||
for dst in refs.get(src, ())
|
||||
if dst in included
|
||||
)
|
||||
|
||||
lines = [
|
||||
f"# {title}",
|
||||
"",
|
||||
f"- Nodes: `{len(included_rows)}`",
|
||||
f"- Edges: `{edge_count}`",
|
||||
f"- Seeds: {', '.join(f'`{fmt_addr(seed)}`' for seed in sorted(seeds))}",
|
||||
f"- Graphviz: `{dot_path.name}`",
|
||||
"",
|
||||
"## Nodes",
|
||||
"",
|
||||
"| Address | Name | Subsystem | Confidence |",
|
||||
"| --- | --- | --- | --- |",
|
||||
]
|
||||
|
||||
for row in included_rows:
|
||||
lines.append(
|
||||
f"| `{fmt_addr(row.address)}` | `{row.name}` | `{row.subsystem}` | `{row.confidence}` |"
|
||||
)
|
||||
|
||||
lines.extend(["", "## Edges", ""])
|
||||
for src in sorted(included):
|
||||
dsts = [dst for dst in sorted(refs.get(src, ())) if dst in included]
|
||||
if not dsts:
|
||||
continue
|
||||
src_row = rows[src]
|
||||
lines.append(f"- `{fmt_addr(src)}` `{src_row.name}`")
|
||||
for dst in dsts:
|
||||
dst_row = rows[dst]
|
||||
lines.append(f" -> `{fmt_addr(dst)}` `{dst_row.name}`")
|
||||
|
||||
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
rows = load_rows(args.function_map)
|
||||
refs = extract_note_refs(rows)
|
||||
seeds = [parse_hex(seed) for seed in args.seed]
|
||||
included = walk_subgraph(rows, refs, seeds, args.depth, args.include_backrefs)
|
||||
|
||||
output_prefix = args.output_prefix.resolve()
|
||||
output_prefix.parent.mkdir(parents=True, exist_ok=True)
|
||||
dot_path = output_prefix.with_suffix(".dot")
|
||||
md_path = output_prefix.with_suffix(".md")
|
||||
|
||||
emit_dot(rows, refs, included, set(seeds), dot_path, args.title)
|
||||
emit_markdown(rows, refs, included, set(seeds), md_path, args.title, dot_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
43
tools/run_hook_auto_load.sh
Executable file
43
tools/run_hook_auto_load.sh
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
game_dir="$repo_root/rt3_wineprefix/drive_c/rt3"
|
||||
log_path="$game_dir/rrt_hook_attach.log"
|
||||
proxy_path="$game_dir/dinput8.dll"
|
||||
save_stem="${1:-hh}"
|
||||
timeout_seconds="${RRT_HOOK_TIMEOUT_SECONDS:-60}"
|
||||
. "$HOME/.local/share/cargo/env"
|
||||
|
||||
cargo build -p rrt-hook --target i686-pc-windows-gnu
|
||||
|
||||
rm -f "$log_path"
|
||||
cp "$repo_root/target/i686-pc-windows-gnu/debug/dinput8.dll" "$proxy_path"
|
||||
|
||||
echo "launcher: start=$(date --iso-8601=seconds)"
|
||||
echo "launcher: save_stem=$save_stem timeout_seconds=$timeout_seconds"
|
||||
|
||||
set +e
|
||||
(
|
||||
cd "$game_dir"
|
||||
export RRT_AUTO_LOAD_SAVE="$save_stem"
|
||||
export WINEPREFIX="$repo_root/rt3_wineprefix"
|
||||
export WINEDLLOVERRIDES="dinput8=n,b"
|
||||
|
||||
timeout "${timeout_seconds}s" /opt/wine-stable/bin/wine RT3.exe
|
||||
) >/tmp/rrt-hook-auto-load-wine.log 2>&1 &
|
||||
launcher_pid=$!
|
||||
echo "launcher: wrapper_pid=$launcher_pid"
|
||||
wait "$launcher_pid"
|
||||
launcher_status=$?
|
||||
set -e
|
||||
|
||||
echo "launcher: exit_status=$launcher_status"
|
||||
echo "launcher: end=$(date --iso-8601=seconds)"
|
||||
|
||||
if [[ -f "$log_path" ]]; then
|
||||
cat "$log_path"
|
||||
else
|
||||
echo "attach-log-missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
31
tools/run_hook_auto_load_winedbg.sh
Executable file
31
tools/run_hook_auto_load_winedbg.sh
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
game_dir="$repo_root/rt3_wineprefix/drive_c/rt3"
|
||||
proxy_path="$game_dir/dinput8.dll"
|
||||
save_stem="${1:-hh}"
|
||||
. "$HOME/.local/share/cargo/env"
|
||||
|
||||
cargo build -p rrt-hook --target i686-pc-windows-gnu
|
||||
cp "$repo_root/target/i686-pc-windows-gnu/debug/dinput8.dll" "$proxy_path"
|
||||
|
||||
cd "$game_dir"
|
||||
export RRT_AUTO_LOAD_SAVE="$save_stem"
|
||||
export WINEPREFIX="$repo_root/rt3_wineprefix"
|
||||
export WINEDLLOVERRIDES="dinput8=n,b"
|
||||
|
||||
cmd=(/opt/wine-stable/bin/winedbg)
|
||||
cmd_file="${RRT_WINEDBG_CMD_FILE:-$repo_root/tools/winedbg_auto_load_compare.cmd}"
|
||||
if [[ -n "$cmd_file" ]]; then
|
||||
cmd+=(--file "$cmd_file")
|
||||
fi
|
||||
cmd+=(RT3.exe)
|
||||
|
||||
log_file="${RRT_WINEDBG_LOG:-$repo_root/rt3_auto_load_winedbg.log}"
|
||||
if [[ -n "$log_file" ]]; then
|
||||
cmd_string="$(printf '%q ' "${cmd[@]}")"
|
||||
exec script -qefc "${cmd_string% }" "$log_file"
|
||||
fi
|
||||
|
||||
exec "${cmd[@]}"
|
||||
|
|
@ -13,9 +13,11 @@ cargo build -p rrt-hook --target i686-pc-windows-gnu
|
|||
rm -f "$log_path"
|
||||
cp "$repo_root/target/i686-pc-windows-gnu/debug/dinput8.dll" "$proxy_path"
|
||||
|
||||
#RRT_WRITE_FINANCE_TEMPLATE=1 \
|
||||
(
|
||||
cd "$game_dir"
|
||||
timeout 8s env \
|
||||
timeout 600s env \
|
||||
RRT_WRITE_FINANCE_CAPTURE=1 \
|
||||
WINEPREFIX="$repo_root/rt3_wineprefix" \
|
||||
WINEDLLOVERRIDES="dinput8=n,b" \
|
||||
/opt/wine-stable/bin/wine RT3.exe
|
||||
|
|
|
|||
29
tools/run_rt3_winedbg.sh
Executable file
29
tools/run_rt3_winedbg.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
game_dir="$repo_root/rt3_wineprefix/drive_c/rt3"
|
||||
proxy_path="$game_dir/dinput8.dll"
|
||||
. "$HOME/.local/share/cargo/env"
|
||||
|
||||
cargo build -p rrt-hook --target i686-pc-windows-gnu
|
||||
cp "$repo_root/target/i686-pc-windows-gnu/debug/dinput8.dll" "$proxy_path"
|
||||
|
||||
cd "$game_dir"
|
||||
export WINEPREFIX="$repo_root/rt3_wineprefix"
|
||||
export WINEDLLOVERRIDES="dinput8=n,b"
|
||||
|
||||
cmd=(/opt/wine-stable/bin/winedbg)
|
||||
cmd_file="${RRT_WINEDBG_CMD_FILE:-$repo_root/tools/winedbg_manual_load_445ac0.cmd}"
|
||||
if [[ -n "$cmd_file" ]]; then
|
||||
cmd+=(--file "$cmd_file")
|
||||
fi
|
||||
cmd+=(RT3.exe)
|
||||
|
||||
log_file="${RRT_WINEDBG_LOG:-$repo_root/rt3_manual_load_winedbg.log}"
|
||||
if [[ -n "$log_file" ]]; then
|
||||
cmd_string="$(printf '%q ' "${cmd[@]}")"
|
||||
exec script -qefc "${cmd_string% }" "$log_file"
|
||||
fi
|
||||
|
||||
exec "${cmd[@]}"
|
||||
76
tools/winedbg_auto_load_compare.cmd
Normal file
76
tools/winedbg_auto_load_compare.cmd
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
break *0x00438890
|
||||
break *0x004390cb
|
||||
break *0x00445ac0
|
||||
break *0x0053fea6
|
||||
cont
|
||||
info reg
|
||||
print/x *(unsigned int*)($esp)
|
||||
print/x *(unsigned int*)($esp+4)
|
||||
print/x *(unsigned int*)($esp+8)
|
||||
print/x *(unsigned int*)($esp+12)
|
||||
print/x *(unsigned int*)0x006cec74
|
||||
print/x *(unsigned int*)0x006cec7c
|
||||
print/x *(unsigned int*)0x006cec78
|
||||
print/x *(unsigned int*)0x006ce9b8
|
||||
print/x *(unsigned int*)0x006ce9bc
|
||||
print/x *(unsigned int*)0x006ce9c0
|
||||
print/x *(unsigned int*)0x006ce9c4
|
||||
print/x *(unsigned int*)0x006d1270
|
||||
print/x *(unsigned int*)0x006d1274
|
||||
print/x *(unsigned int*)0x006d1278
|
||||
print/x *(unsigned int*)0x006d127c
|
||||
bt
|
||||
cont
|
||||
info reg
|
||||
print/x *(unsigned int*)($esp)
|
||||
print/x *(unsigned int*)($esp+4)
|
||||
print/x *(unsigned int*)($esp+8)
|
||||
print/x *(unsigned int*)($esp+12)
|
||||
print/x *(unsigned int*)0x006cec74
|
||||
print/x *(unsigned int*)0x006cec7c
|
||||
print/x *(unsigned int*)0x006cec78
|
||||
print/x *(unsigned int*)0x006ce9b8
|
||||
print/x *(unsigned int*)0x006ce9bc
|
||||
print/x *(unsigned int*)0x006ce9c0
|
||||
print/x *(unsigned int*)0x006ce9c4
|
||||
print/x *(unsigned int*)0x006d1270
|
||||
print/x *(unsigned int*)0x006d1274
|
||||
print/x *(unsigned int*)0x006d1278
|
||||
print/x *(unsigned int*)0x006d127c
|
||||
bt
|
||||
cont
|
||||
info reg
|
||||
print/x *(unsigned int*)($esp)
|
||||
print/x *(unsigned int*)($esp+4)
|
||||
print/x *(unsigned int*)($esp+8)
|
||||
print/x *(unsigned int*)($esp+12)
|
||||
print/x *(unsigned int*)0x006cec74
|
||||
print/x *(unsigned int*)0x006cec7c
|
||||
print/x *(unsigned int*)0x006cec78
|
||||
print/x *(unsigned int*)0x006ce9b8
|
||||
print/x *(unsigned int*)0x006ce9bc
|
||||
print/x *(unsigned int*)0x006ce9c0
|
||||
print/x *(unsigned int*)0x006ce9c4
|
||||
print/x *(unsigned int*)0x006d1270
|
||||
print/x *(unsigned int*)0x006d1274
|
||||
print/x *(unsigned int*)0x006d1278
|
||||
print/x *(unsigned int*)0x006d127c
|
||||
bt
|
||||
cont
|
||||
info reg
|
||||
print/x *(unsigned int*)($esp)
|
||||
print/x *(unsigned int*)($esp+4)
|
||||
print/x *(unsigned int*)($esp+8)
|
||||
print/x *(unsigned int*)($esp+12)
|
||||
print/x *(unsigned int*)0x006cec74
|
||||
print/x *(unsigned int*)0x006cec7c
|
||||
print/x *(unsigned int*)0x006cec78
|
||||
print/x *(unsigned int*)0x006ce9b8
|
||||
print/x *(unsigned int*)0x006ce9bc
|
||||
print/x *(unsigned int*)0x006ce9c0
|
||||
print/x *(unsigned int*)0x006ce9c4
|
||||
print/x *(unsigned int*)0x006d1270
|
||||
print/x *(unsigned int*)0x006d1274
|
||||
print/x *(unsigned int*)0x006d1278
|
||||
print/x *(unsigned int*)0x006d127c
|
||||
bt
|
||||
20
tools/winedbg_manual_load_445ac0.cmd
Normal file
20
tools/winedbg_manual_load_445ac0.cmd
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
break *0x004390cb
|
||||
break *0x00445ac0
|
||||
cont
|
||||
info reg
|
||||
print/x *(unsigned int*)($esp)
|
||||
print/x *(unsigned int*)($esp+4)
|
||||
print/x *(unsigned int*)($esp+8)
|
||||
print/x *(unsigned int*)($esp+12)
|
||||
print/x *(unsigned int*)0x006cec74
|
||||
print/x *(unsigned int*)0x006cec7c
|
||||
print/x *(unsigned int*)0x006cec78
|
||||
print/x *(unsigned int*)0x006ce9b8
|
||||
print/x *(unsigned int*)0x006ce9bc
|
||||
print/x *(unsigned int*)0x006ce9c0
|
||||
print/x *(unsigned int*)0x006ce9c4
|
||||
print/x *(unsigned int*)0x006d1270
|
||||
print/x *(unsigned int*)0x006d1274
|
||||
print/x *(unsigned int*)0x006d1278
|
||||
print/x *(unsigned int*)0x006d127c
|
||||
bt
|
||||
Loading…
Add table
Add a link
Reference in a new issue