Debugging live ​
Reading the executable shows what code can do. A live trace shows what it does: which function builds a screen, how often, with which values, and who writes a field. On Linux, gdb attaches to F1 25 running under Proton or Wine. For the methods these traces support, see Techniques for menu edits.
Before you attach ​
- Addresses. The Game API and the RE knowledge base give addresses for one build of
F1_25.exe, with the image base0x140000000. swofty_core logs each hook it installs as@<address> (base+<offset>). When an address there is0x140000000plus its offset, the game loaded at that base and the documented addresses are live. Otherwise, add each address's offset to the logged base. - When to attach. Attach at a quiet screen, such as the main menu, before the action you trace. Some screens are built when a save loads, not when they open.
- Hooked bytes. A software breakpoint writes into the code, and so does a hook. On bytes that swofty_core or a plugin patches, use a hardware breakpoint (
hbreak) instead. Break only in.xtlscode, never in.data1bodies.
Attach ​
pid=$(for p in $(pgrep -f F1_25.exe); do grep -q F1_25.exe /proc/$p/maps 2>/dev/null && echo $p && break; done)
prior=$(cat /proc/sys/kernel/yama/ptrace_scope)
sudo sysctl -w kernel.yama.ptrace_scope=0
timeout -s INT 900 gdb -p "$pid" --batch -x trace.gdb 2>&1 | tee trace.log
sudo sysctl -w kernel.yama.ptrace_scope="$prior"- Proton starts several processes whose command line names
F1_25.exe. The game is the one that maps the executable. - With
ptrace_scopeat 1, the default on most distributions, gdb can't attach to a process it didn't start unless it runs as root. Set it to 0 for the session and put the old value back afterwards. --batchruns the script, then detaches when the script ends, when gdb getsSIGINTfromtimeout, or when the game exits. The game keeps running after a detach.- If gdb dies while the game is stopped, the game stays frozen. Its state in
/proc/<pid>/statisT; resume it withkill -CONT <pid>.
The script header ​
set pagination off
set confirm off
handle all nostop noprint pass
handle SIGTRAP stop noprint nopassWine delivers Windows exceptions and its own events through signals such as SIGSEGV and SIGUSR1. handle all nostop noprint pass hands every signal to the game untouched. SIGTRAP is how gdb's breakpoints arrive: gdb stops on it and keeps it from the game.
Log hits without stopping ​
A breakpoint whose commands end in continue works as a tracepoint: it prints and lets the game run on.
set $hits = 0
break *0x14xxxxxxx
commands
silent
set $hits = $hits + 1
printf "[t] hit %d rcx=%#lx rdx=%#lx caller=%#lx\n", $hits, $rcx, $rdx, *(long *)$rsp
continue
end
continue- At a function's first instruction,
*(long *)$rspis the return address: an address just after the call, inside the caller. - Add a condition to log only the case you want:
break *0x14xxxxxxx if $r13 == 4. - Read memory in the commands:
x/4gx $rbxdumps four qwords,printf "%s\n", *(char **)($rsp+0x20)prints a string argument passed on the stack. - Keep the commands short. The game is stopped while they run.
- Put a fixed prefix such as
[t]on every line, sogrep '\[t\]' trace.logseparates your lines from gdb's.
Find who writes a value ​
A hardware watchpoint stops right after a write to an address. $pc is then just past the writing instruction:
watch -l *(unsigned char *)0x14xxxxxxx
commands
silent
printf "[t] write -> %d at %#lx\n", *(unsigned char *)0x14xxxxxxx, $pc
continue
endA watchpoint that never fires is a result too: it shows that the value doesn't change during the action you traced.
hbreak *0x14xxxxxxx is the hardware form of a breakpoint. x86 has four debug registers, which watchpoints and hardware breakpoints share.
Read memory without stopping ​
With ptrace_scope at 0, /proc/<pid>/mem reads the game's memory while it runs:
import sys
pid, addr, size = int(sys.argv[1]), int(sys.argv[2], 16), int(sys.argv[3])
with open(f'/proc/{pid}/mem', 'rb') as mem:
mem.seek(addr)
print(mem.read(size).hex())The game keeps changing that memory as you read it, so compare a few reads before trusting a value.
Afterwards ​
- Put
ptrace_scopeback, and check that the game isn't left stopped. - Read
trace.lognext toswofty/loader.log: your addon's lines show what the hooks did around each hit. - Quit the game normally, so the session rolls back, then run
swofty verify. See the test loop.