769 lines
34 KiB
Python
Executable File
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())
|