tools: trace FIFA17 scenario start source chain

This commit is contained in:
funman300
2026-08-26 04:46:42 +00:00
parent f608dbc438
commit 2c572e918f
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""Trace FIFA17 ScenarioModeStart sources, scheduling, and publication.
The candidate profile samples the four statically recovered publisher clusters.
The scheduler profile follows the exact mode-0 chain: FreeRoam event ``0x128``,
its scheduler, the ScenarioManager start method, and the mode-0 publisher. The
upstream profile traces all three literal ``0x128`` sources and the FreeRoam
case they must reach.
Both generated GDB programs use at most four simultaneously enabled hardware
execution breakpoints. They only read registers and client memory, log, and
continue. They never call client functions, write client memory, emit events,
or drive input.
scenario_mode_start_trace.py [pid] --profile candidates
scenario_mode_start_trace.py [pid] --profile scheduler
scenario_mode_start_trace.py [pid] --profile upstream
scenario_mode_start_trace.py --selftest
"""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import shutil
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
import match_advance_trace as advance
import match_transition_trace as transition
CANDIDATE_A_RVA = 0x07AFDCC0
CANDIDATE_B_RVA = 0x07B1BA90
CANDIDATE_C_RVA = 0x07B1C190
CANDIDATE_D_RVA = 0x07E0B200
FREE_ROAM_EVENT_128_CALLSITE_RVA = 0x07A92B0F
SCENARIO_SCHEDULER_RVA = 0x07AC3A40
SCENARIO_MANAGER_START_RVA = 0x07B1C2B0
MODE_ZERO_PUBLISHER_RVA = 0x07B1C190
EVENT_SOURCE_27F9_RVA = 0x07DBD16B
EVENT_SOURCE_30_RVA = 0x07DCA4E5
EVENT_SOURCE_280E_RVA = 0x07DD065F
def trace_addresses(fifa_base: int) -> dict[str, int]:
return {
"candidate_a": fifa_base + CANDIDATE_A_RVA,
"candidate_b": fifa_base + CANDIDATE_B_RVA,
"candidate_c": fifa_base + CANDIDATE_C_RVA,
"candidate_d": fifa_base + CANDIDATE_D_RVA,
"free_roam_event_128_callsite": fifa_base + FREE_ROAM_EVENT_128_CALLSITE_RVA,
"scenario_scheduler": fifa_base + SCENARIO_SCHEDULER_RVA,
"scenario_manager_start": fifa_base + SCENARIO_MANAGER_START_RVA,
"mode_zero_publisher": fifa_base + MODE_ZERO_PUBLISHER_RVA,
"event_source_27f9": fifa_base + EVENT_SOURCE_27F9_RVA,
"event_source_30": fifa_base + EVENT_SOURCE_30_RVA,
"event_source_280e": fifa_base + EVENT_SOURCE_280E_RVA,
}
def gdb_prelude(pid: int, output: str) -> str:
if any(character in output for character in "\n\r"):
raise ValueError("output path cannot contain a newline")
return f"""set pagination off
set confirm off
set print thread-events off
set breakpoint always-inserted on
set logging file {output}
set logging overwrite on
set logging redirect off
set logging enabled on
handle SIGSEGV nostop noprint pass
handle SIGILL nostop noprint pass
handle SIGFPE nostop noprint pass
handle SIGPIPE nostop noprint pass
handle SIGALRM nostop noprint pass
handle SIGUSR1 nostop noprint pass
handle SIGUSR2 nostop noprint pass
attach {pid}
"""
def build_candidate_script(pid: int, fifa_base: int, output: str) -> str:
address = trace_addresses(fifa_base)
sections = [gdb_prelude(pid, output)]
for label in ("a", "b", "c", "d"):
sections.append(
f"""hbreak *0x{address[f'candidate_{label}']:x}
commands
silent
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d CANDIDATE_{label.upper()}" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d rip=%p caller_return=%p object=%p vtable=%p edx=%#x r8=%p r9=%p field_40=%#x field_50=%#x field_b8=%#x\\n", $_thread, $pc, *(void**)$rsp, $rcx, *(void**)$rcx, $edx, $r8, $r9, *(unsigned char*)($rcx+0x40), *(unsigned int*)($rcx+0x50), *(unsigned int*)($rcx+0xb8)
bt 16
continue
end
"""
)
sections.append(
"printf \"SCENARIOTRACE ARMED profile=candidates "
f"pid={pid} candidate_a=0x{address['candidate_a']:x} "
f"candidate_b=0x{address['candidate_b']:x} "
f"candidate_c=0x{address['candidate_c']:x} "
f"candidate_d=0x{address['candidate_d']:x}\\n\"\ncontinue\n"
)
return "".join(sections)
def build_scheduler_script(pid: int, fifa_base: int, output: str) -> str:
address = trace_addresses(fifa_base)
return (
gdb_prelude(pid, output)
+ f"""hbreak *0x{address['free_roam_event_128_callsite']:x}
commands
silent
set $owner = $rbx
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d FREE_ROAM_EVENT_128" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d owner=%p event=%#x payload=%p mode=%d manager=%p caller_return=%p\\n", $_thread, $owner, $esi, $rdi, *(unsigned int*)($owner+0x124), *(void**)($owner+0x168), *(void**)$rsp
bt 16
continue
end
hbreak *0x{address['scenario_scheduler']:x}
commands
silent
set $owner = $rcx
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d SCENARIO_SCHEDULER" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d owner=%p event=%#x payload=%p mode=%d manager=%p caller_return=%p\\n", $_thread, $owner, $edx, $r8, *(unsigned int*)($owner+0x124), *(void**)($owner+0x168), *(void**)$rsp
bt 16
continue
end
hbreak *0x{address['scenario_manager_start']:x}
commands
silent
set $manager = $rcx
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d SCENARIO_MANAGER_START" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d manager=%p vtable=%p requested_countdown=%d scenario_mode=%d child=%p caller_return=%p\\n", $_thread, $manager, *(void**)$manager, $edx, *(unsigned int*)($manager+0x50), *(void**)($manager+0x8), *(void**)$rsp
bt 16
continue
end
hbreak *0x{address['mode_zero_publisher']:x}
commands
silent
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d MODE_ZERO_PUBLISHER" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d object=%p vtable=%p requested_countdown=%d r8=%p r9=%p caller_return=%p\\n", $_thread, $rcx, *(void**)$rcx, $edx, $r8, $r9, *(void**)$rsp
bt 20
continue
end
printf "SCENARIOTRACE ARMED profile=scheduler pid={pid} event_callsite=0x{address['free_roam_event_128_callsite']:x} scheduler=0x{address['scenario_scheduler']:x} manager_start=0x{address['scenario_manager_start']:x} mode_zero_publisher=0x{address['mode_zero_publisher']:x}\\n"
continue
"""
)
def build_upstream_script(pid: int, fifa_base: int, output: str) -> str:
address = trace_addresses(fifa_base)
sections = [gdb_prelude(pid, output)]
for label, trigger in (("27f9", 0x27F9), ("30", 0x30), ("280e", 0x280E)):
sections.append(
f"""hbreak *0x{address[f'event_source_{label}']:x}
commands
silent
set $gameplay = $rax
set $listeners = 0
set $listener0 = 0
set $listener1 = 0
set $listener2 = 0
if $gameplay != 0
set $listeners = *(void**)$gameplay
if $listeners != 0
set $listener0 = *(void**)$listeners
set $listener1 = *(void**)($listeners+8)
set $listener2 = *(void**)($listeners+16)
end
end
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d EVENT_128_SOURCE_{label.upper()}" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d trigger=%#x gameplay=%p listeners=%p entries=%p,%p,%p caller_return=%p\\n", $_thread, {trigger}, $gameplay, $listeners, $listener0, $listener1, $listener2, *(void**)$rsp
bt 20
continue
end
"""
)
sections.append(
f"""hbreak *0x{address['free_roam_event_128_callsite']:x}
commands
silent
set $owner = $rbx
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d FREE_ROAM_EVENT_128" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d owner=%p event=%#x payload=%p mode=%d manager=%p caller_return=%p\\n", $_thread, $owner, $esi, $rdi, *(unsigned int*)($owner+0x124), *(void**)($owner+0x168), *(void**)$rsp
bt 20
continue
end
printf "SCENARIOTRACE ARMED profile=upstream pid={pid} source_27f9=0x{address['event_source_27f9']:x} source_30=0x{address['event_source_30']:x} source_280e=0x{address['event_source_280e']:x} event_callsite=0x{address['free_roam_event_128_callsite']:x}\\n"
continue
"""
)
return "".join(sections)
def selftest() -> None:
address = trace_addresses(0x140000000)
assert address["candidate_a"] == 0x147AFDCC0
assert address["candidate_b"] == 0x147B1BA90
assert address["candidate_c"] == 0x147B1C190
assert address["candidate_d"] == 0x147E0B200
assert address["free_roam_event_128_callsite"] == 0x147A92B0F
assert address["scenario_scheduler"] == 0x147AC3A40
assert address["scenario_manager_start"] == 0x147B1C2B0
assert address["mode_zero_publisher"] == 0x147B1C190
assert address["event_source_27f9"] == 0x147DBD16B
assert address["event_source_30"] == 0x147DCA4E5
assert address["event_source_280e"] == 0x147DD065F
candidate_script = build_candidate_script(61470, 0x140000000, "/tmp/candidates.log")
scheduler_script = build_scheduler_script(61470, 0x140000000, "/tmp/scheduler.log")
upstream_script = build_upstream_script(61470, 0x140000000, "/tmp/upstream.log")
assert candidate_script.count("hbreak *") == 4
assert all(f"CANDIDATE_{label}" in candidate_script for label in "ABCD")
assert scheduler_script.count("hbreak *") == 4
assert "FREE_ROAM_EVENT_128" in scheduler_script
assert "SCENARIO_SCHEDULER" in scheduler_script
assert "SCENARIO_MANAGER_START" in scheduler_script
assert "MODE_ZERO_PUBLISHER" in scheduler_script
assert upstream_script.count("hbreak *") == 4
assert "EVENT_128_SOURCE_27F9" in upstream_script
assert "EVENT_128_SOURCE_30" in upstream_script
assert "EVENT_128_SOURCE_280E" in upstream_script
assert "FREE_ROAM_EVENT_128" in upstream_script
assert "0x147ac9900" not in scheduler_script.lower()
assert "CANDIDATE_B_GATE" not in scheduler_script
assert "set *(" not in candidate_script
assert "set *(" not in scheduler_script
assert "set *(" not in upstream_script
print("scenario_mode_start_trace selftest: PASS")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("pid", nargs="?", type=int)
parser.add_argument(
"--profile",
choices=("candidates", "scheduler", "upstream"),
default="candidates",
)
parser.add_argument("--output")
parser.add_argument("--print-script", action="store_true")
parser.add_argument("--selftest", action="store_true")
args = parser.parse_args()
if args.selftest:
selftest()
return 0
pid = args.pid or transition.find_pid()
if not pid:
print("FIFA17.exe not found", file=sys.stderr)
return 2
try:
fifa_base, fifa_path = advance.module_mapping(pid, advance.FIFA_MODULE)
advance.validate_file(
fifa_path,
advance.PINNED_FIFA_SHA256,
advance.FIFA_MODULE,
)
output = args.output or f"/tmp/fifa17-scenario-{args.profile}-{pid}.log"
if args.profile == "candidates":
script = build_candidate_script(pid, fifa_base, output)
elif args.profile == "scheduler":
script = build_scheduler_script(pid, fifa_base, output)
else:
script = build_upstream_script(pid, fifa_base, output)
except (OSError, RuntimeError, ValueError) as error:
print(error, file=sys.stderr)
return 2
if args.print_script:
print(script, end="")
return 0
if not shutil.which("gdb"):
print("gdb not found", file=sys.stderr)
return 2
script_path = f"/tmp/fifa17-scenario-{args.profile}-{pid}.gdb"
with open(script_path, "w", encoding="utf-8") as handle:
handle.write(script)
os.execvp("gdb", ["gdb", "-q", "-nx", "-batch", "-x", script_path])
return 127
if __name__ == "__main__":
raise SystemExit(main())