Files
OpenFUT/fifa17-recon/tools/scenario_mode_start_trace.py
T
2026-08-26 17:34:43 +00:00

769 lines
34 KiB
Python
Executable File

#!/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. The upstream profile
traces the three literal ``0x128`` sources. The instructions, lifecycle, and
screen profiles follow the natural PMA state transition through event ``0x30``.
The kickoff-control profile records a bounded command stream and dynamically
rotates breakpoints into the complete ScenarioModeStart chain after command
``0x128`` while retaining the consumer as an architecture-independent anchor.
Every profile respects the four-breakpoint hardware limit.
The generated GDB programs 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 [pid] --profile instructions
scenario_mode_start_trace.py [pid] --profile lifecycle
scenario_mode_start_trace.py [pid] --profile screen
scenario_mode_start_trace.py [pid] --profile kickoff-control
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
PMA_STATE_CHANGE_RVA = 0x07ADB870
PMA_COMMAND_27_CALLSITE_RVA = 0x07ADBCB2
PMA_EVENT_30_CALLSITE_RVA = 0x07ADBCF9
PMA_ADVANCE_RVA = 0x07A9FAA0
PMA_TRANSITION_DONE_CALLSITE_RVA = 0x07A9D9FD
GAMEPLAY_COMMAND_DISPATCH_RVA = 0x07A8F6C0
GAMEPLAY_GLOBAL_RVA = 0x04BFB910
PMA_INSTRUCTIONS_HANDLER_RVA = 0x07AC91E0
PMA_UI_SET_STATE_RVA = 0x07B0EBB0
TESTING_GAME_UPDATE_RVA = 0x05A410C8
SCENARIO_MODE_START_CONSUMER_RVA = 0x05A58EC0
STATE_INITIALIZER_RVA = 0x05A54CF0
STATE_INITIALIZER_DONE_RVA = 0x05A54DDB
GAME_MODE_FREE_ROAM_VTABLE_RVA = 0x03AEDF58
TESTING_GAME_VTABLE_RVA = 0x035C58A8
TESTING_GAME_STATE_VTABLE_RVA = 0x035C2EE0
OWNER_STATE_OFFSET = 0x1958
STATE_GAME_DATABASE_OFFSET = 0x17450
STATE_PHASE_OFFSET = 0x27BEC
STATE_SCENARIO_MODE_START_GATE_OFFSET = 0x359E8
DATABASE_IS_SKILL_GAME_OFFSET = 0x7382
DATABASE_TEAM_PAIR_OFFSET = 0x73C4
EVENT_SOURCE_27F9_RETURN_RVA = 0x07DBD178
EVENT_SOURCE_30_RETURN_RVA = 0x07DCA4EF
EVENT_SOURCE_280E_RETURN_RVA = 0x07DD0669
MAX_KICKOFF_DISPATCHES = 512
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,
"pma_state_change": fifa_base + PMA_STATE_CHANGE_RVA,
"pma_command_27_callsite": fifa_base + PMA_COMMAND_27_CALLSITE_RVA,
"pma_event_30_callsite": fifa_base + PMA_EVENT_30_CALLSITE_RVA,
"pma_advance": fifa_base + PMA_ADVANCE_RVA,
"pma_transition_done_callsite": fifa_base + PMA_TRANSITION_DONE_CALLSITE_RVA,
"gameplay_command_dispatch": fifa_base + GAMEPLAY_COMMAND_DISPATCH_RVA,
"gameplay_global": fifa_base + GAMEPLAY_GLOBAL_RVA,
"pma_instructions_handler": fifa_base + PMA_INSTRUCTIONS_HANDLER_RVA,
"pma_ui_set_state": fifa_base + PMA_UI_SET_STATE_RVA,
"testing_game_update": fifa_base + TESTING_GAME_UPDATE_RVA,
"scenario_mode_start_consumer": fifa_base + SCENARIO_MODE_START_CONSUMER_RVA,
"state_initializer": fifa_base + STATE_INITIALIZER_RVA,
"state_initializer_done": fifa_base + STATE_INITIALIZER_DONE_RVA,
"game_mode_free_roam_vtable": fifa_base + GAME_MODE_FREE_ROAM_VTABLE_RVA,
"testing_game_vtable": fifa_base + TESTING_GAME_VTABLE_RVA,
"testing_game_state_vtable": fifa_base + TESTING_GAME_STATE_VTABLE_RVA,
"event_source_27f9_return": fifa_base + EVENT_SOURCE_27F9_RETURN_RVA,
"event_source_30_return": fifa_base + EVENT_SOURCE_30_RETURN_RVA,
"event_source_280e_return": fifa_base + EVENT_SOURCE_280E_RETURN_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 skip_countdown=%d scenario_mode=%d child=%p caller_return=%p\\n", $_thread, $manager, *(void**)$manager, $dl, *(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 context=%p scenario_value=%d skip_countdown=%d caller_return=%p\\n", $_thread, $rcx, *(void**)$rcx, $rdx, $r8d, $r9b, *(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 build_instructions_script(pid: int, fifa_base: int, output: str) -> str:
address = trace_addresses(fifa_base)
return (
gdb_prelude(pid, output)
+ f"""hbreak *0x{address['pma_instructions_handler']:x}
commands
silent
set $listener = $rcx
set $parent = *(void**)($listener+0x8)
set $owner = 0
set $current = 0
if $parent != 0
set $owner = *(void**)($parent+0x8)
if $owner != 0
set $current = *(void**)($owner+0x10)
end
end
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_INSTRUCTIONS_EVENT" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d listener=%p event=%d flag18=%d parent=%p owner=%p current=%p current_vtable=%p caller_return=%p\\n", $_thread, $listener, $edx, *(unsigned char*)($listener+0x18), $parent, $owner, $current, $current ? *(void**)$current : 0, *(void**)$rsp
bt 20
continue
end
hbreak *0x{address['pma_ui_set_state']:x}
commands
silent
set $parent = $rcx
set $owner = *(void**)($parent+0x8)
set $current = 0
if $owner != 0
set $current = *(void**)($owner+0x10)
end
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_UI_SET_STATE" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d parent=%p requested_state=%d owner=%p current=%p current_vtable=%p caller_return=%p\\n", $_thread, $parent, $edx, $owner, $current, $current ? *(void**)$current : 0, *(void**)$rsp
bt 20
continue
end
hbreak *0x{address['pma_state_change']:x}
commands
silent
set $state = $rcx
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_STATE_CHANGE" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d object=%p mode_state=%d old_state=%d new_state=%d reason=%d flag14=%d caller_return=%p\\n", $_thread, $state, *(int*)($state+0x8), *(int*)($state+0xc), $edx, $r8d, *(unsigned char*)($state+0x14), *(void**)$rsp
bt 20
continue
end
hbreak *0x{address['pma_command_27_callsite']:x}
commands
silent
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_COMMAND_27" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d state_object=%p mode_state=%d transition_state=%d new_state=%d caller_return=%p\\n", $_thread, $r13, *(int*)($r13+0x8), *(int*)($r13+0xc), $r15d, *(void**)$rsp
bt 20
continue
end
printf "SCENARIOTRACE ARMED profile=instructions pid={pid} instructions=0x{address['pma_instructions_handler']:x} ui_set_state=0x{address['pma_ui_set_state']:x} state_change=0x{address['pma_state_change']:x} command27=0x{address['pma_command_27_callsite']:x}\\n"
continue
"""
)
def build_lifecycle_script(pid: int, fifa_base: int, output: str) -> str:
address = trace_addresses(fifa_base)
return (
gdb_prelude(pid, output)
+ f"""hbreak *0x{address['pma_state_change']:x}
commands
silent
set $state = $rcx
set $gameplay_global = *(void**)0x{address['gameplay_global']:x}
set $listener_manager = 0
set $listener_table = 0
set $listener_index = -1
set $selected_listener = 0
if $gameplay_global != 0
set $listener_manager = *(void**)($gameplay_global+0x58)
if $listener_manager != 0
set $listener_table = *(void**)$listener_manager
if $listener_table != 0
set $listener_index = *(int*)($listener_table+0x20)
if $listener_index >= 0
set $selected_listener = *(void**)($listener_table+$listener_index*8)
end
end
end
end
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_STATE_CHANGE" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d object=%p mode_state=%d old_state=%d new_state=%d reason=%d flag14=%d selected_index=%d selected=%p selected_vtable=%p caller_return=%p\\n", $_thread, $state, *(int*)($state+0x8), *(int*)($state+0xc), $edx, $r8d, *(unsigned char*)($state+0x14), $listener_index, $selected_listener, $selected_listener ? *(void**)$selected_listener : 0, *(void**)$rsp
bt 20
continue
end
hbreak *0x{address['pma_command_27_callsite']:x}
commands
silent
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_COMMAND_27" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d state_object=%p mode_state=%d transition_state=%d new_state=%d selected_state=%d caller_return=%p\\n", $_thread, $r13, *(int*)($r13+0x8), *(int*)($r13+0xc), $r15d, *(int*)($r13+0xc), *(void**)$rsp
bt 20
continue
end
hbreak *0x{address['pma_advance']:x}
commands
silent
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_ADVANCE_FROM_PMA" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d free_roam=%p old_substate=%d guard124=%d manager=%p caller_return=%p\\n", $_thread, $rcx, *(int*)($rcx+0x30), *(int*)($rcx+0x124), *(void**)($rcx+0x168), *(void**)$rsp
bt 20
continue
end
hbreak *0x{address['pma_transition_done_callsite']:x}
commands
silent
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_TRANSITION_DONE" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d free_roam=%p substate=%d event=%#x event_target=%p target_vtable=%p caller_return=%p\\n", $_thread, $r14, *(int*)($r14+0x30), $edx, $rbx, *(void**)$rbx, *(void**)$rsp
bt 20
continue
end
printf "SCENARIOTRACE ARMED profile=lifecycle pid={pid} state_change=0x{address['pma_state_change']:x} command27=0x{address['pma_command_27_callsite']:x} advance=0x{address['pma_advance']:x} transition_done=0x{address['pma_transition_done_callsite']:x}\\n"
continue
"""
)
def build_screen_script(pid: int, fifa_base: int, output: str) -> str:
address = trace_addresses(fifa_base)
return (
gdb_prelude(pid, output)
+ f"""hbreak *0x{address['pma_state_change']:x}
commands
silent
set $state = $rcx
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_STATE_CHANGE" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d object=%p mode_state=%d old_state=%d new_state=%d reason=%d flag14=%d caller_return=%p\\n", $_thread, $state, *(int*)($state+0x8), *(int*)($state+0xc), $edx, $r8d, *(unsigned char*)($state+0x14), *(void**)$rsp
bt 20
continue
end
hbreak *0x{address['pma_event_30_callsite']:x}
commands
silent
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d PMA_EVENT_30" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d state_object=%p mode_state=%d transition_state=%d selected=%p selected_vtable=%p caller_return=%p\\n", $_thread, $r13, *(int*)($r13+0x8), *(int*)($r13+0xc), $rcx, *(void**)$rcx, *(void**)$rsp
bt 20
continue
end
hbreak *0x{address['gameplay_command_dispatch']:x}
condition 3 $edx == 0x128
commands
silent
python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d GAMEPLAY_COMMAND_128" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d dispatcher=%p command=%#x listeners=%p caller_return=%p\\n", $_thread, $rcx, $edx, *(void**)$rcx, *(void**)$rsp
bt 20
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 caller_return=%p\\n", $_thread, $rcx, *(void**)$rcx, $edx, *(void**)$rsp
bt 20
continue
end
printf "SCENARIOTRACE ARMED profile=screen pid={pid} state_change=0x{address['pma_state_change']:x} event30=0x{address['pma_event_30_callsite']:x} command128=0x{address['gameplay_command_dispatch']:x} publisher=0x{address['mode_zero_publisher']:x}\\n"
continue
"""
)
def build_kickoff_control_script(pid: int, fifa_base: int, output: str) -> str:
address = trace_addresses(fifa_base)
return (
gdb_prelude(pid, output)
+ f"""set breakpoint always-inserted off
set $dispatch_count = 0
set $command_128_seen = 0
hbreak *0x{address['gameplay_command_dispatch']:x}
commands
silent
set $dispatch_count = $dispatch_count + 1
set $caller = *(void**)$rsp
set $listeners = *(void**)$rcx
set $listener0 = 0
set $listener1 = 0
set $listener2 = 0
set $vtable0 = 0
set $vtable1 = 0
set $vtable2 = 0
if $listeners != 0
set $listener0 = *(void**)$listeners
set $listener1 = *(void**)($listeners+8)
set $listener2 = *(void**)($listeners+16)
end
if $listener0 != 0
set $vtable0 = *(void**)$listener0
end
if $listener1 != 0
set $vtable1 = *(void**)$listener1
end
if $listener2 != 0
set $vtable2 = *(void**)$listener2
end
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d GAMEPLAY_COMMAND_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d ordinal=%d command=%#x dispatcher=%p payload=%p r9=%p caller_return=%p listeners=%p entries=%p,%p,%p vtables=%p,%p,%p free_roam_present=%d\\n", $_thread, $dispatch_count, $edx, $rcx, $r8, $r9, $caller, $listeners, $listener0, $listener1, $listener2, $vtable0, $vtable1, $vtable2, $vtable0==0x{address['game_mode_free_roam_vtable']:x} || $vtable1==0x{address['game_mode_free_roam_vtable']:x} || $vtable2==0x{address['game_mode_free_roam_vtable']:x}
if $caller == 0x{address['event_source_27f9_return']:x}
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d SOURCE_EVENT_27F9" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d event=%#x object=%p vtable=%p payload=%p handler=0x{fifa_base + 0x07DBD070:x} caller_return=%p\\n", $_thread, $ebp, $rsi, *(void**)$rsi, $rdi, $caller
end
if $caller == 0x{address['event_source_30_return']:x}
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d SOURCE_EVENT_0030" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d event=%#x object=%p vtable=%p payload=%p handler=0x{fifa_base + 0x07DCA400:x} caller_return=%p\\n", $_thread, $ebx, $rsi, *(void**)$rsi, $rdi, $caller
end
if $caller == 0x{address['event_source_280e_return']:x}
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d SOURCE_EVENT_280E" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d event=0x280e object=%p vtable=%p payload=%p handler=0x{fifa_base + 0x07DD0280:x} caller_return=%p\\n", $_thread, $rbx, *(void**)$rbx, $r10, $caller
end
if $edx == 0x128
set $command_128_seen = 1
disable 4
enable 7
bt 20
end
if $dispatch_count >= {MAX_KICKOFF_DISPATCHES}
printf "KICKOFFTRACE DISPATCH_LIMIT_REACHED count=%d\\n", $dispatch_count
disable 1
end
continue
end
hbreak *0x{address['mode_zero_publisher']:x}
commands
silent
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d MODE_ZERO_PUBLISHER" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d object=%p vtable=%p context=%p scenario_value=%d skip_countdown=%d caller_return=%p\\n", $_thread, $rcx, *(void**)$rcx, $rdx, $r8d, $r9b, *(void**)$rsp
bt 20
disable 2
continue
end
hbreak *0x{address['scenario_mode_start_consumer']:x}
commands
silent
set $wrapper = $rcx
set $state = *(void**)($wrapper+0x30)
set $database = 0
if $state != 0
set $database = *(void**)($state+0x{STATE_GAME_DATABASE_OFFSET:x})
end
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d SCENARIO_MODE_START_CONSUMER" % (time.time_ns(), time.monotonic_ns()), end=" ")
if $database != 0
printf "thread=%d wrapper=%p wrapper_vtable=%p state=%p state_vtable=%p payload_provider=%p phase_before=%d alternate_gate_before=%d database=%p is_skill_game=%d teams=%d,%d caller_return=%p\\n", $_thread, $wrapper, *(void**)$wrapper, $state, *(void**)$state, $r9, *(unsigned int*)($state+0x{STATE_PHASE_OFFSET:x}), *(unsigned char*)($state+0x{STATE_SCENARIO_MODE_START_GATE_OFFSET:x}), $database, *(unsigned char*)($database+0x{DATABASE_IS_SKILL_GAME_OFFSET:x}), *(unsigned int*)($database+0x{DATABASE_TEAM_PAIR_OFFSET:x}), *(unsigned int*)($database+0x{DATABASE_TEAM_PAIR_OFFSET + 4:x}), *(void**)$rsp
else
printf "thread=%d wrapper=%p state=%p payload_provider=%p database=0 caller_return=%p\\n", $_thread, $wrapper, $state, $r9, *(void**)$rsp
end
bt 20
disable 1
disable 2
disable 7
enable 5
continue
end
hbreak *0x{address['testing_game_update']:x}
commands
silent
set $owner = $rsi
set $state = *(void**)($owner+0x{OWNER_STATE_OFFSET:x})
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d GAMEPLAY_UPDATE_POSITIVE" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d owner=%p owner_vtable=%p state=%p state_vtable=%p expected_owner=%d expected_state=%d\\n", $_thread, $owner, *(void**)$owner, $state, *(void**)$state, *(void**)$owner==0x{address['testing_game_vtable']:x}, *(void**)$state==0x{address['testing_game_state_vtable']:x}
disable 4
continue
end
hbreak *0x{address['state_initializer']:x}
commands
silent
set $state = $rcx
set $database = *(void**)($state+0x{STATE_GAME_DATABASE_OFFSET:x})
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d STATE_INITIALIZER" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d state=%p state_vtable=%p payload_provider=%p phase_before=%d alternate_gate_before=%d database=%p is_skill_game=%d teams=%d,%d caller_return=%p\\n", $_thread, $state, *(void**)$state, $rdx, *(unsigned int*)($state+0x{STATE_PHASE_OFFSET:x}), *(unsigned char*)($state+0x{STATE_SCENARIO_MODE_START_GATE_OFFSET:x}), $database, *(unsigned char*)($database+0x{DATABASE_IS_SKILL_GAME_OFFSET:x}), *(unsigned int*)($database+0x{DATABASE_TEAM_PAIR_OFFSET:x}), *(unsigned int*)($database+0x{DATABASE_TEAM_PAIR_OFFSET + 4:x}), *(void**)$rsp
disable 3
disable 5
enable 6
continue
end
disable 5
hbreak *0x{address['state_initializer_done']:x}
commands
silent
set $state = $rbx
set $database = *(void**)($state+0x{STATE_GAME_DATABASE_OFFSET:x})
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d STATE_INITIALIZER_DONE" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d state=%p payload_provider=%p phase_after=%d alternate_gate_after=%d database=%p is_skill_game=%d teams=%d,%d\\n", $_thread, $state, $rdi, *(unsigned int*)($state+0x{STATE_PHASE_OFFSET:x}), *(unsigned char*)($state+0x{STATE_SCENARIO_MODE_START_GATE_OFFSET:x}), $database, *(unsigned char*)($database+0x{DATABASE_IS_SKILL_GAME_OFFSET:x}), *(unsigned int*)($database+0x{DATABASE_TEAM_PAIR_OFFSET:x}), *(unsigned int*)($database+0x{DATABASE_TEAM_PAIR_OFFSET + 4:x})
disable 6
continue
end
disable 6
hbreak *0x{address['free_roam_event_128_callsite']:x}
commands
silent
set $owner = $rbx
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d FREE_ROAM_EVENT_128" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d owner=%p owner_vtable=%p command=%#x payload=%p ready=%d manager=%p caller_return=%p\\n", $_thread, $owner, *(void**)$owner, $esi, $rdi, *(unsigned char*)($owner+0x124), *(void**)($owner+0x168), *(void**)$rsp
disable 1
disable 7
enable 8
continue
end
disable 7
hbreak *0x{address['scenario_scheduler']:x}
commands
silent
set $owner = $rcx
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d SCENARIO_SCHEDULER" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d owner=%p owner_vtable=%p command=%#x payload=%p ready=%d manager=%p caller_return=%p\\n", $_thread, $owner, *(void**)$owner, $edx, $r8, *(unsigned char*)($owner+0x124), *(void**)($owner+0x168), *(void**)$rsp
disable 8
enable 9
continue
end
disable 8
hbreak *0x{address['scenario_manager_start']:x}
commands
silent
set $manager = $rcx
set $child = *(void**)($manager+0x8)
python import time; print("KICKOFFTRACE epoch_ns=%d mono_ns=%d SCENARIO_MANAGER_START" % (time.time_ns(), time.monotonic_ns()), end=" ")
printf "thread=%d manager=%p manager_vtable=%p skip_countdown=%d mode=%d child=%p child_vtable=%p caller_return=%p\\n", $_thread, $manager, *(void**)$manager, $dl, *(unsigned int*)($manager+0x50), $child, *(void**)$child, *(void**)$rsp
disable 9
continue
end
disable 9
printf "KICKOFFTRACE ARMED profile=kickoff-control pid={pid} dispatch=0x{address['gameplay_command_dispatch']:x} publisher=0x{address['mode_zero_publisher']:x} consumer=0x{address['scenario_mode_start_consumer']:x} update=0x{address['testing_game_update']:x} initializer=0x{address['state_initializer']:x} free_roam=0x{address['free_roam_event_128_callsite']:x} scheduler=0x{address['scenario_scheduler']:x} manager=0x{address['scenario_manager_start']:x} max_dispatches={MAX_KICKOFF_DISPATCHES}\\n"
continue
"""
)
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
assert address["pma_state_change"] == 0x147ADB870
assert address["pma_command_27_callsite"] == 0x147ADBCB2
assert address["pma_event_30_callsite"] == 0x147ADBCF9
assert address["pma_advance"] == 0x147A9FAA0
assert address["pma_transition_done_callsite"] == 0x147A9D9FD
assert address["gameplay_command_dispatch"] == 0x147A8F6C0
assert address["pma_instructions_handler"] == 0x147AC91E0
assert address["pma_ui_set_state"] == 0x147B0EBB0
assert address["testing_game_update"] == 0x145A410C8
assert address["scenario_mode_start_consumer"] == 0x145A58EC0
assert address["state_initializer"] == 0x145A54CF0
assert address["state_initializer_done"] == 0x145A54DDB
assert address["game_mode_free_roam_vtable"] == 0x143AEDF58
assert address["event_source_27f9_return"] == 0x147DBD178
assert address["event_source_30_return"] == 0x147DCA4EF
assert address["event_source_280e_return"] == 0x147DD0669
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")
lifecycle_script = build_lifecycle_script(61470, 0x140000000, "/tmp/lifecycle.log")
screen_script = build_screen_script(61470, 0x140000000, "/tmp/screen.log")
instructions_script = build_instructions_script(
61470, 0x140000000, "/tmp/instructions.log"
)
kickoff_script = build_kickoff_control_script(
61470, 0x140000000, "/tmp/kickoff-control.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 instructions_script.count("hbreak *") == 4
assert "PMA_INSTRUCTIONS_EVENT" in instructions_script
assert "PMA_UI_SET_STATE" in instructions_script
assert "PMA_STATE_CHANGE" in instructions_script
assert "PMA_COMMAND_27" in instructions_script
assert lifecycle_script.count("hbreak *") == 4
assert "PMA_STATE_CHANGE" in lifecycle_script
assert "PMA_COMMAND_27" in lifecycle_script
assert "PMA_ADVANCE_FROM_PMA" in lifecycle_script
assert "PMA_TRANSITION_DONE" in lifecycle_script
assert screen_script.count("hbreak *") == 4
assert "PMA_EVENT_30" in screen_script
assert "GAMEPLAY_COMMAND_128" in screen_script
assert "MODE_ZERO_PUBLISHER" in screen_script
assert kickoff_script.count("hbreak *") == 9
assert "GAMEPLAY_COMMAND_DISPATCH" in kickoff_script
assert "GAMEPLAY_UPDATE_POSITIVE" in kickoff_script
assert "SOURCE_EVENT_27F9" in kickoff_script
assert "SOURCE_EVENT_0030" in kickoff_script
assert "SOURCE_EVENT_280E" in kickoff_script
assert "FREE_ROAM_EVENT_128" in kickoff_script
assert "SCENARIO_SCHEDULER" in kickoff_script
assert "SCENARIO_MANAGER_START" in kickoff_script
assert "MODE_ZERO_PUBLISHER" in kickoff_script
assert "SCENARIO_MODE_START_CONSUMER" in kickoff_script
assert "STATE_INITIALIZER_DONE" in kickoff_script
assert "set *(" not in kickoff_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
assert "set *(" not in lifecycle_script
assert "set *(" not in screen_script
assert "set *(" not in instructions_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",
"instructions",
"lifecycle",
"screen",
"kickoff-control",
),
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)
elif args.profile == "upstream":
script = build_upstream_script(pid, fifa_base, output)
elif args.profile == "instructions":
script = build_instructions_script(pid, fifa_base, output)
elif args.profile == "lifecycle":
script = build_lifecycle_script(pid, fifa_base, output)
elif args.profile == "screen":
script = build_screen_script(pid, fifa_base, output)
else:
script = build_kickoff_control_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())