#!/usr/bin/env python3 """Trace FIFA17 create-response readiness versus UI provider dispatch. The probe correlates four concrete lifecycle boundaries: * FutCreateMatchServerResponse data-source request; * the POST /match network response callback; * the response readiness/completion callback; * FIFA's global UI dispatch of providers 0x7563 and 0x7565. This distinguishes network completion from the separate DataManager readiness lifecycle without assuming any screen or heap-object identity. The generated GDB program uses hardware-assisted execution breakpoints only. It never writes client memory and never drives game input. match_provider_producer_trace.py [pid] [--output PATH] match_provider_producer_trace.py --print-script [pid] match_provider_producer_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 DATA_SOURCE_REQUEST_RVA = 0x120270 NETWORK_RESPONSE_RVA = transition.RESPONSE_CALLBACK_RVA CREATE_COMPLETE_RVA = 0x120000 GLOBAL_UI_DISPATCH_RVA = advance.GLOBAL_UI_DISPATCH_RVA CREATE_RESPONSE_OFFSET = 0xA0 CREATE_DATA_SOURCE_OFFSET = CREATE_RESPONSE_OFFSET + 0x50 CREATE_READY_OFFSET = CREATE_RESPONSE_OFFSET + 0x88 ACTIVE_CALLBACK_OFFSET = 0x47D0 def trace_addresses(cards_base: int, fifa_base: int) -> dict[str, int]: return { "data_source_request": cards_base + DATA_SOURCE_REQUEST_RVA, "network_response": cards_base + NETWORK_RESPONSE_RVA, "create_complete": cards_base + CREATE_COMPLETE_RVA, "ui_dispatch": fifa_base + GLOBAL_UI_DISPATCH_RVA, } def build_gdb_script( pid: int, cards_base: int, fifa_base: int, output: str ) -> str: if any(character in output for character in "\n\r"): raise ValueError("output path cannot contain a newline") address = trace_addresses(cards_base, fifa_base) 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} set $last_central = 0 set $last_response = 0 set $last_data_source = 0 set $last_descriptor = 0 hbreak *0x{address['data_source_request']:x} commands silent set $request_data_source = $rcx set $request_response = $rcx - 0x50 python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d DATA_SOURCE_REQUEST" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d response=%p data_source=%p request=%p ready_before=%#x callback_adapter=%p callback_context=%p callback_target=%p\\n", $_thread, $request_response, $request_data_source, $rdx, *(unsigned char*)($request_data_source+0x38), *(void**)($request_response+0x90), *(void**)($request_response+0x98), *(void**)($request_response+0xa0) bt 10 continue end hbreak *0x{address['network_response']:x} commands silent set $last_central = $rcx set $last_response = $rcx + 0x{CREATE_RESPONSE_OFFSET:x} set $last_data_source = $rcx + 0x{CREATE_DATA_SOURCE_OFFSET:x} set $last_descriptor = $rdx python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d NETWORK_RESPONSE" % (time.time_ns(), time.monotonic_ns()), end=" ") if $rdx == 0 printf "thread=%d central=%p descriptor=(nil) status=UNKNOWN wire_payload=(nil) response=%p data_source=%p ready=%#x active_adapter=%p active_context=%p active_target=%p\\n", $_thread, $last_central, $last_response, $last_data_source, *(unsigned char*)($last_central+0x{CREATE_READY_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 8:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 16:x}) else printf "thread=%d central=%p descriptor=%p status=%#x wire_payload=%p response=%p data_source=%p ready=%#x active_adapter=%p active_context=%p active_target=%p\\n", $_thread, $last_central, $rdx, *(unsigned int*)($rdx+0x1c), *(void**)($rdx+0x28), $last_response, $last_data_source, *(unsigned char*)($last_central+0x{CREATE_READY_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 8:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 16:x}) end bt 10 continue end hbreak *0x{address['create_complete']:x} commands silent python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d CREATE_COMPLETE" % (time.time_ns(), time.monotonic_ns()), end=" ") if $rdx == 0 printf "thread=%d response=%p data_source=%p ready_before=%#x descriptor=(nil) status=UNKNOWN last_response=%p same_response=%d\\n", $_thread, $rcx, $rcx+0x50, *(unsigned char*)($rcx+0x88), $last_response, $rcx==$last_response else printf "thread=%d response=%p data_source=%p ready_before=%#x descriptor=%p status=%#x last_response=%p same_response=%d\\n", $_thread, $rcx, $rcx+0x50, *(unsigned char*)($rcx+0x88), $rdx, *(unsigned int*)($rdx+0x1c), $last_response, $rcx==$last_response end bt 10 continue end hbreak *0x{address['ui_dispatch']:x} condition 4 $r8d == 0x{transition.FUT_CREATE_MATCH_DP:x} || $r8d == 0x{transition.FUT_GET_MATCH_KITS_DP:x} commands silent python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d UI_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ") if $last_response == 0 printf "thread=%d provider=%#x payload=%p ui_manager=%p last_response=(nil) ready=UNKNOWN\\n", $_thread, $r8d, $rdx, $rcx else printf "thread=%d provider=%#x payload=%p ui_manager=%p last_response=%p data_source=%p ready=%#x descriptor=%p\\n", $_thread, $r8d, $rdx, $rcx, $last_response, $last_data_source, *(unsigned char*)($last_response+0x88), $last_descriptor end bt 10 continue end printf "RESPTRACE ARMED pid={pid} data_source_request=0x{address['data_source_request']:x} network_response=0x{address['network_response']:x} create_complete=0x{address['create_complete']:x} ui_dispatch=0x{address['ui_dispatch']:x}\\n" continue """ def selftest() -> None: address = trace_addresses(0x180000000, 0x140000000) assert address == { "data_source_request": 0x180120270, "network_response": 0x180114D90, "create_complete": 0x180120000, "ui_dispatch": 0x1480D1070, } script = build_gdb_script( 38872, 0x180000000, 0x140000000, "/tmp/response-lifecycle.log" ) assert script.count("hbreak *") == 4 assert "DATA_SOURCE_REQUEST" in script assert "NETWORK_RESPONSE" in script assert "CREATE_COMPLETE" in script assert "UI_DISPATCH" in script assert f"$r8d == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script assert f"$r8d == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" in script assert "set *(" not in script print("match_provider_producer_trace selftest: PASS") def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("pid", nargs="?", type=int) 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: cards_base, cards_path = transition.cards_mapping(pid) transition.validate_cards(cards_path) 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-match-response-lifecycle-{pid}.log" script = build_gdb_script(pid, cards_base, 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-match-response-lifecycle-{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())