trace FIFA17 provider lifecycle

This commit is contained in:
funman300
2026-08-25 23:18:08 +00:00
parent 5eed124b85
commit 43c460741b
4 changed files with 608 additions and 16 deletions
+11 -16
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
"""Trace FIFA17 ACTION_ADVANCE dispatch after MATCH_CREATED delivery.
"""Trace FIFA17 provider dispatch and ACTION_ADVANCE delivery boundaries.
Stage one (`match_transition_trace.py`) proves the HTTP response, deserializer,
and FUT_CREATE_MATCH_DP delivery. This stage uses that provider callback to
capture the screen key, then records every nested global UI dispatch for that
same screen until the positive-control FUT_GET_MATCH_KITS_DP arrives. It also
captures the low-level create event and final native-to-UI provider bridge.
This probe correlates the global UI dispatch of FUT_CREATE_MATCH_DP and
FUT_GET_MATCH_KITS_DP, the subscribed CardsDLL provider, the internal 0x7546
create-response callback that can replay FUT_CREATE_MATCH_DP, and the final
native-to-UI bridge. At global dispatch, r8d is the provider ID and rdx is the
payload; neither register is a screen key.
The generated GDB program uses hardware-assisted execution breakpoints only.
It never writes client memory and never drives game input.
@@ -84,28 +84,22 @@ handle SIGUSR1 nostop noprint pass
handle SIGUSR2 nostop noprint pass
attach {pid}
set $screen_key = 0
set $target_seen = 0
hbreak *0x{address['provider']:x}
condition 1 $edx == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edx == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
commands
silent
if $edx == 0x{transition.FUT_CREATE_MATCH_DP:x}
set $screen_key = $r8
set $target_seen = 1
end
python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d PROVIDER" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d provider=%#x screen_key=%p controller=%p caller=%p\\n", $_thread, $edx, $r8, $rcx, *(void**)$rsp
printf "thread=%d provider=%#x payload=%p controller=%p caller=%p\\n", $_thread, $edx, $r8, $rcx, *(void**)$rsp
continue
end
hbreak *0x{address['global_dispatch']:x}
condition 2 $target_seen != 0 && $r8 == $screen_key
condition 2 $r8d == 0x{transition.FUT_CREATE_MATCH_DP:x} || $r8d == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
commands
silent
python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d GLOBAL_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d id=%#x screen_key=%p registry=%p caller=%p\\n", $_thread, $rdx, $r8, $rcx, *(void**)$rsp
printf "thread=%d provider=%#x payload=%p manager=%p caller=%p\\n", $_thread, $r8d, $rdx, $rcx, *(void**)$rsp
continue
end
@@ -144,7 +138,8 @@ def selftest() -> None:
assert script.count("hbreak *") == 4
assert f"$edx == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script
assert f"$edx == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" in script
assert "$r8 == $screen_key" 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 "CREATE_MATCH_CONTROLLER" in script
assert "PROVIDER_BRIDGE" in script
assert "set *(" not in script
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""Trace the FIFA17 ACTION_CREATE_MATCH-to-provider lifecycle.
The probe correlates:
* the select-team action handler for UIF action IDs 0x7574..0x757b;
* DataManager's request dispatch for FutCreateMatchServerResponse (0x7546);
* the concrete FutCreateMatchServerResponse data-source request method;
* FIFA's global UI dispatch of providers 0x7563 and 0x7565.
Static decoding identifies action 0x7577 as the branch that constructs the
create-match request and calls DataManager for source 0x7546. The trace proves
whether that authentic trigger executes in the failing flow. It uses four
hardware-assisted execution breakpoints, never writes client memory, and never
drives game input.
match_create_action_trace.py [pid] [--output PATH]
match_create_action_trace.py --print-script [pid]
match_create_action_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
SELECT_TEAM_ACTION_HANDLER_RVA = 0x0BFCC0
DATA_MANAGER_REQUEST_RVA = 0x80D2340
DATA_SOURCE_REQUEST_RVA = 0x120270
GLOBAL_UI_DISPATCH_RVA = advance.GLOBAL_UI_DISPATCH_RVA
FIRST_SELECT_TEAM_ACTION = 0x7574
LAST_SELECT_TEAM_ACTION = 0x757B
ACTION_CREATE_MATCH = 0x7577
CREATE_DATA_SOURCE = 0x7546
def trace_addresses(cards_base: int, fifa_base: int) -> dict[str, int]:
return {
"action_handler": cards_base + SELECT_TEAM_ACTION_HANDLER_RVA,
"manager_request": fifa_base + DATA_MANAGER_REQUEST_RVA,
"data_source_request": cards_base + DATA_SOURCE_REQUEST_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 $create_action_seen = 0
set $manager_request_seen = 0
set $data_source_request_seen = 0
hbreak *0x{address['action_handler']:x}
condition 1 $edx >= 0x{FIRST_SELECT_TEAM_ACTION:x} && $edx <= 0x{LAST_SELECT_TEAM_ACTION:x}
commands
silent
if $edx == 0x{ACTION_CREATE_MATCH:x}
set $create_action_seen = 1
end
python import time; print("ACTIONTRACE epoch_ns=%d mono_ns=%d SELECT_TEAM_ACTION" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d action=%#x is_create=%d controller=%p payload=%p create_seen=%d\\n", $_thread, $edx, $edx==0x{ACTION_CREATE_MATCH:x}, $rcx, $r8, $create_action_seen
bt 10
continue
end
hbreak *0x{address['manager_request']:x}
condition 2 $edx == 0x{CREATE_DATA_SOURCE:x}
commands
silent
set $manager_request_seen = 1
set $tree_sentinel = $rcx + 0x10
set $tree_cursor = *(void**)($rcx+0x20)
set $data_node = $tree_sentinel
while $tree_cursor != 0 && $tree_cursor != $tree_sentinel
if *(unsigned int*)($tree_cursor+0x20) >= 0x{CREATE_DATA_SOURCE:x}
set $data_node = $tree_cursor
set $tree_cursor = *(void**)($tree_cursor+0x08)
else
set $tree_cursor = *(void**)$tree_cursor
end
end
set $data_source = 0
set $request_method = 0
if $data_node != $tree_sentinel && *(unsigned int*)($data_node+0x20) == 0x{CREATE_DATA_SOURCE:x}
set $data_source = *(void**)($data_node+0x28)
if $data_source != 0
set $request_method = *(void**)(*(void**)$data_source+0x18)
end
end
python import time; print("ACTIONTRACE epoch_ns=%d mono_ns=%d MANAGER_REQUEST" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d source=%#x manager=%p request=%p node=%p data_source=%p request_method=%p create_seen=%d\\n", $_thread, $edx, $rcx, $r8, $data_node, $data_source, $request_method, $create_action_seen
bt 10
continue
end
hbreak *0x{address['data_source_request']:x}
commands
silent
set $data_source_request_seen = 1
python import time; print("ACTIONTRACE 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 create_seen=%d manager_seen=%d\\n", $_thread, $rcx-0x50, $rcx, $rdx, *(unsigned char*)($rcx+0x38), $create_action_seen, $manager_request_seen
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("ACTIONTRACE epoch_ns=%d mono_ns=%d UI_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d provider=%#x payload=%p ui_manager=%p create_seen=%d manager_seen=%d data_source_seen=%d\\n", $_thread, $r8d, $rdx, $rcx, $create_action_seen, $manager_request_seen, $data_source_request_seen
bt 10
continue
end
printf "ACTIONTRACE ARMED pid={pid} action_handler=0x{address['action_handler']:x} manager_request=0x{address['manager_request']:x} data_source_request=0x{address['data_source_request']:x} ui_dispatch=0x{address['ui_dispatch']:x}\\n"
continue
"""
def selftest() -> None:
address = trace_addresses(0x180000000, 0x140000000)
assert address == {
"action_handler": 0x1800BFCC0,
"manager_request": 0x1480D2340,
"data_source_request": 0x180120270,
"ui_dispatch": 0x1480D1070,
}
script = build_gdb_script(
45949, 0x180000000, 0x140000000, "/tmp/create-action.log"
)
assert script.count("hbreak *") == 4
assert f"$edx == 0x{ACTION_CREATE_MATCH:x}" in script
assert f"$edx == 0x{CREATE_DATA_SOURCE:x}" 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 "request_method" in script
assert "set *(" not in script
print("match_create_action_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-create-action-{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-create-action-{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())
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""Trace FIFA17 provider delivery lookup without heap-address assumptions.
The probe anchors the real CardsDLL call sequence in FUN_1801a4cd0 and the
provider-specific FUT_CREATE_MATCH_DP readiness check in FUN_1800be500:
vslot +0x38 call -> create gate return -> returned target -> UI bridge
For FUT_CREATE_MATCH_DP and FUT_GET_MATCH_KITS_DP it records the live controller
vtable, concrete lookup function, event service, readiness-gate implementation,
every register input, returned target, and whether the native-to-UI bridge
executes. No post-event object identity is used.
The generated GDB program uses hardware-assisted execution breakpoints only.
It never writes client memory and never drives game input.
match_delivery_lifecycle_trace.py [pid] [--output PATH]
match_delivery_lifecycle_trace.py --print-script [pid]
match_delivery_lifecycle_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
LOOKUP_CALL_RVA = transition.PROVIDER_DISPATCH_RVA + 0x2C
LOOKUP_RETURN_RVA = transition.PROVIDER_DISPATCH_RVA + 0x2F
BRIDGE_CALL_RVA = transition.PROVIDER_DISPATCH_RVA + 0x71
CREATE_GATE_RETURN_RVA = 0x0BE647
def trace_addresses(cards_base: int) -> dict[str, int]:
return {
"lookup_call": cards_base + LOOKUP_CALL_RVA,
"gate_return": cards_base + CREATE_GATE_RETURN_RVA,
"lookup_return": cards_base + LOOKUP_RETURN_RVA,
"bridge": cards_base + BRIDGE_CALL_RVA,
}
def build_gdb_script(pid: int, cards_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)
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 $current_provider = 0
set $current_payload = 0
set $current_controller = 0
set $current_vtable = 0
set $current_lookup = 0
set $current_service = 0
set $current_service_vtable = 0
set $current_gate = 0
hbreak *0x{address['lookup_call']:x}
condition 1 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
commands
silent
set $current_provider = $edi
set $current_payload = $rbp
set $current_controller = $rcx
set $current_vtable = *(void**)$rcx
set $current_lookup = *(void**)(*(void**)$rcx+0x38)
set $current_service = *(void**)($rcx+0x18)
set $current_service_vtable = *(void**)$current_service
set $current_gate = *(void**)($current_service_vtable+0x58)
python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d LOOKUP_CALL" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d provider=%#x this=%p outer_controller=%p payload=%p vtable=%p lookup_fn=%p service=%p service_vtable=%p gate_fn=%p controller_mode=%#x controller_flag=%#x rdx=%p r8=%p r9=%p state_rbx=%p state_rbp=%p\\n", $_thread, $edi, $rcx, $rbx, $rbp, $current_vtable, $current_lookup, $current_service, $current_service_vtable, $current_gate, *(unsigned int*)($rcx+0x140), *(unsigned char*)($rcx+0x152), $rdx, $r8, $r9, $rbx, $rbp
continue
end
hbreak *0x{address['gate_return']:x}
condition 2 $current_provider == 0x{transition.FUT_CREATE_MATCH_DP:x} && $rbx == $current_controller
commands
silent
python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d CREATE_GATE_RETURN" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d provider=%#x controller=%p service=%p service_vtable=%p gate_fn=%p selector=0x7546 result_al=%#x\\n", $_thread, $current_provider, $current_controller, $current_service, $current_service_vtable, $current_gate, $al
continue
end
hbreak *0x{address['lookup_return']:x}
condition 3 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
commands
silent
python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d LOOKUP_RETURN" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d provider=%#x controller=%p payload=%p vtable=%p lookup_fn=%p result=%p\\n", $_thread, $edi, $rbx, $rbp, $current_vtable, $current_lookup, $rax
continue
end
hbreak *0x{address['bridge']:x}
condition 4 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
commands
silent
python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d BRIDGE" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d provider=%#x controller=%p payload=%p target=%p bridge=%p callback=%p\\n", $_thread, $edi, $current_controller, $current_payload, $rsi, $rbx, *(void**)(*(void**)$rbx+0x48)
continue
end
printf "LOOKUPTRACE ARMED pid={pid} lookup_call=0x{address['lookup_call']:x} gate_return=0x{address['gate_return']:x} lookup_return=0x{address['lookup_return']:x} bridge=0x{address['bridge']:x}\\n"
continue
"""
def selftest() -> None:
address = trace_addresses(0x180000000)
assert address == {
"lookup_call": 0x1801A4CFC,
"gate_return": 0x1800BE647,
"lookup_return": 0x1801A4CFF,
"bridge": 0x1801A4D41,
}
script = build_gdb_script(35632, 0x180000000, "/tmp/lookup.log")
assert script.count("hbreak *") == 4
assert f"$edi == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script
assert f"$edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" in script
assert "LOOKUP_CALL" in script
assert "CREATE_GATE_RETURN" in script
assert "LOOKUP_RETURN" in script
assert "gate_fn" in script
assert "BRIDGE" in script
assert "set *(" not in script
print("match_delivery_lifecycle_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-provider-lookup-{pid}.log"
script = build_gdb_script(pid, cards_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-provider-lookup-{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())
+201
View File
@@ -0,0 +1,201 @@
#!/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())