| name | dynamic-analysis |
| description | Use when attaching to or spawning a process for live analysis, setting breakpoints, tracing functions, collecting execution data, inspecting registers/memory/stack at runtime, stepping through code, patching live memory, catching who writes to an address, counting D3D9 draw calls, sending keystrokes or clicks to a game window, analyzing JSONL trace dumps, or performing any dynamic analysis task. |
Dynamic Analysis with livetools
For DX9 FFP proxy porting and RTX Remix compatibility, invoke the dx9-ffp-port skill.
A live analysis toolkit for running processes. Attach, trace functions, collect data, inspect state, step through code, patch memory, analyze offline -- composable tools that chain naturally for any RE scenario.
All commands: python -m livetools <command> [args]
Quick Reference
Session
python -m livetools attach <name_or_pid> # attach to running process
python -m livetools attach <exe_path> --spawn # launch + instrument before init code runs
python -m livetools detach # release target, stop daemon
python -m livetools status # check state
Breakpoints (blocking)
python -m livetools bp add <addr> # set blocking code breakpoint
python -m livetools bp del <addr> # remove breakpoint
python -m livetools bp list # list all BPs + hit counts
Wait for Hit
python -m livetools watch --timeout 60 # block until BP hit (returns snapshot)
Inspect (while frozen)
python -m livetools regs # all registers
python -m livetools stack [N] # top N dwords from ESP (default 16)
python -m livetools mem read <addr> <size> # hex dump + multi-type interpretation
python -m livetools mem read <addr> <size> --as float32
python -m livetools disasm [addr] [-n count] # disassemble (default: EIP, 16 insns)
python -m livetools bt # call stack backtrace
Supported --as types: float32, float64, half, uint8, int8, uint16, int16, uint32, int32, ptr, ascii, utf16.
Control
python -m livetools step [over|into|out] # advance one instruction (returns snapshot)
python -m livetools resume # unfreeze target
Patch + Scan (anytime)
python -m livetools mem write <addr> <hex> # write bytes to live memory
python -m livetools mem alloc <size> # allocate rwx memory in target (for code caves)
python -m livetools scan <pattern> --range START:SIZE
Memory Write Watchpoint
python -m livetools memwatch start <addr> [--size N] [--max-hits N] # catch who writes (IP + backtrace per hit)
python -m livetools memwatch read # show captured hits
python -m livetools memwatch stop
D3D9 Draw Counter
python -m livetools dipcnt on <dev_ptr> # hook DrawIndexedPrimitive via global IDirect3DDevice9* address
python -m livetools dipcnt read # current count + delta since last read
python -m livetools dipcnt callers [N] # sample N DIP calls, histogram return addresses (default 200)
python -m livetools dipcnt off
Game Window Input (no Frida, no focus steal)
python -m livetools gamectl --exe <game.exe> info
python -m livetools gamectl --exe <game.exe> key RETURN
python -m livetools gamectl --exe <game.exe> keys "DOWN DOWN RETURN WAIT:1000 RETURN"
python -m livetools gamectl --exe <game.exe> click <x> <y>
python -m livetools gamectl --exe <game.exe> macro <name> --macro-file patches/<game>/macros.json
Non-blocking Tracing
python -m livetools trace <addr> [--count N] [--read SPEC] [--read-leave SPEC] [--filter EXPR] [--timeout T] [--output FILE]
python -m livetools steptrace <addr> [--max-insn N] [--call-depth D] [--detail LEVEL] [--timeout T] [--output FILE]
python -m livetools collect <addr> [addr2 ...] [--duration N] [--read SPEC] [--fence ADDR] [--label ADDR=NAME] [--output FILE]
python -m livetools modules [--filter PATTERN]
python -m livetools analyze <file.jsonl> [--summary] [--group-by FIELD] [--filter EXPR] [--cross-tab F1 F2] [--histogram FIELD] [--export-csv FILE]
Read Spec Format
Used by trace, collect, and related commands to specify what data to capture at function entry/exit.
Semicolon-separated fields. Each field:
| Syntax | Description | Example |
|---|
register | Register value as hex | ecx, eax, ebp |
[reg+OFFSET]:SIZE:TYPE | Read SIZE bytes from reg+OFFSET | [esp+4]:12:float32 |
*[reg+OFFSET]:SIZE:TYPE | Double-deref: follow pointer first | *[esp+4]:64:float32 |
st0 | FPU top-of-stack (best-effort) | st0 |
Types: hex (default), float32, float64, uint32, int32, uint16, int16, uint8, int8, ascii, utf16, ptr
Example read spec:
"ecx; [esp+4]:12:float32; *[ecx+0x10]:64:hex"
Filter Spec Format
Simple comparison on a readable field. Evaluated in-agent; non-matching calls are silently skipped.
[esp+8]==0x2
eax!=0
[ecx+0x54]:4:float32>0.5
Command Details
trace -- Non-blocking enter/leave function hook
Hooks a function's entry and exit without freezing the target. Reads specified data at each call, returns structured results.
python -m livetools trace 0x401000 --count 10 --read "ecx; [esp+4]:12:float32"
python -m livetools trace 0x402000 --count 5 --filter "[esp+8]==0x2" --read "[esp+c]:64:float32"
python -m livetools trace 0x403000 --count 20 --read "ecx; [esp+4]:12:float32" --read-leave "eax"
python -m livetools trace 0x401000 --count 100 --read "ecx" --output trace.jsonl
Output format:
=== TRACE 0x00401000 === 10 samples
#1 caller=00405ABC
ENTER ecx=10B457CC [esp+4]:12:float32=[-622.44, -278.34, 50.00]
LEAVE eax=00000001 retval=00000001
#2 caller=00405ABC
ENTER ecx=10DE6A74 [esp+4]:12:float32=[-236.48, -322.13, 50.00]
LEAVE eax=00000001 retval=00000001
steptrace -- Instruction-level execution recording via Stalker
Records every instruction executed from function entry through return. Uses Frida Stalker for real-time instruction tracing.
python -m livetools steptrace 0x401000 --max-insn 500 --call-depth 1 --detail full
python -m livetools steptrace 0x402000 --max-insn 1000 --detail branches
python -m livetools steptrace 0x403000 --max-insn 5000 --detail blocks
python -m livetools steptrace 0x401000 --max-insn 500 --output steptrace.jsonl
Detail levels:
- full: Every instruction, register snapshots at calls/rets. Expensive but complete.
- branches: All instructions recorded, register snapshots at branches. Good default.
- blocks: Only instruction addresses. Cheapest. Good for path mapping.
collect -- Long-running multi-function data collection
Streams data from one or more functions for a duration. Optionally partitions records into intervals via fence hooks.
python -m livetools collect 0x401000 0x402000 \
--duration 30 --output trace.jsonl \
--read "ecx; [esp+4]:12:float32" \
--fence 0x403000 \
--label 0x401000=FuncA --label 0x402000=FuncB
python -m livetools collect 0x401000 0x402000 \
--read@0x401000="ecx; [esp+4]:12:float32" \
--read@0x402000="ecx; [esp+4]:28:hex" \
--duration 15 --output multi.jsonl
The fence concept: Hook a boundary function (e.g. DX Present) that increments an interval counter. Every trace record includes the current interval ID. Enables per-frame analysis, per-N-calls partitioning, and cross-function correlation.
Output: JSONL in patches/<exe_name>/traces/ by default (gitignored).
modules -- List loaded DLLs
python -m livetools modules
python -m livetools modules --filter kernel
Returns name, base address, size, and full path for every loaded module. Essential for finding DLL bases for vtable hooks.
memwatch -- Hardware memory write watchpoint
Uses Frida's MemoryAccessMonitor to catch writes to an address range, capturing the writing instruction pointer and a backtrace per hit. The runtime complement to static datarefs.py โ catches writes through pointers computed at runtime.
python -m livetools memwatch start 0x7A0000 --size 48 --max-hits 20
python -m livetools memwatch read
python -m livetools memwatch stop
One active watchpoint at a time. Auto-stops after --max-hits (default 20).
dipcnt -- D3D9 DrawIndexedPrimitive counter
Hooks DIP through the device vtable, given the address of the game's global IDirect3DDevice9* pointer. Use to measure draw call volume or find render-loop callers without a full trace.
python -m livetools dipcnt on 0x7C5548
python -m livetools dipcnt read
python -m livetools dipcnt callers 200
python -m livetools dipcnt off
callers is the fast way to locate the main render loop: the dominant return address is the draw dispatcher.
gamectl -- Send input to the game window
Posts WM_KEYDOWN/WM_KEYUP/clicks directly to the target window handle โ no Frida, no focus stealing, works with the game in the background. Use to navigate menus or trigger in-game actions during long traces.
python -m livetools gamectl --exe game.exe info
python -m livetools gamectl --exe game.exe key RETURN [--hold-ms 50]
python -m livetools gamectl --exe game.exe keys "DOWN DOWN RETURN WAIT:1000 RETURN" [--delay-ms 200]
python -m livetools gamectl --exe game.exe click 400 300
python -m livetools gamectl --exe game.exe macro navigate_menu --macro-file patches/<game>/macros.json
Key names: RETURN, ESCAPE, SPACE, arrows, TAB, F1-F12, A-Z, 0-9, NUMPAD0-9, SHIFT, CTRL, ALT. Sequence tokens: WAIT:N (pause N ms), HOLD:KEY:N (hold key N ms). Window lookup: --exe (preferred) or --window <title substring>.
vishook -- Selective visibility override via code cave
Patches a jmp trampoline to route through a code cave that forces a "visible" result (st0 = 102400.0, optional byte out = 1) for callers at/above a threshold address, while callers below it run the original function. Designed for __thiscall visibility checks returning float on st(0) with ret 0x10. The --threshold value is parsed as hex: the default 500000 means 0x500000.
python -m livetools vishook on <jmp_site> <orig_target> [--threshold 500000]
python -m livetools vishook stats
python -m livetools vishook off
analyze -- Offline JSONL aggregation (no Frida needed)
Pure Python. Reads JSONL from collect or trace --output. Deterministic, non-hallucinated aggregation.
python -m livetools analyze trace.jsonl --summary
python -m livetools analyze trace.jsonl --group-by addr
python -m livetools analyze trace.jsonl --filter "addr==00401000" --group-by "leave.eax"
python -m livetools analyze trace.jsonl --filter "addr==00401000" --cross-tab caller leave.eax
python -m livetools analyze trace.jsonl --group-by interval --top 5
python -m livetools analyze trace.jsonl --interval 47
python -m livetools analyze trace.jsonl --compare-intervals 10 50
python -m livetools analyze trace.jsonl --filter "addr==00401000" --histogram "enter.reads.0.value.0"
python -m livetools analyze trace.jsonl --filter "addr==00401000" --export-csv output.csv
Field path syntax: dot-separated with array indices.
addr = hooked address
leave.eax = EAX at exit
enter.reads.0.value.0 = first read spec's first float value
interval = fence counter
caller = return address
JSONL Record Format
Each line in a JSONL file is a self-contained JSON object:
{"ts": 1710000000000, "interval": 47, "addr": "0x00401000", "label": "FuncA", "caller": "00405ABC", "enter": {"regs": {"ecx": "10B457CC", ...}, "reads": [{"spec": "[esp+4]:12:float32", "value": [-622.44, -278.34, 50.00]}]}, "leave": {"eax": "00000001", "retval": "00000001", "reads": []}}
Output File Location
JSONL files default to patches/<exe_name>/traces/ inside the workspace (gitignored). The exe name is auto-detected from the attached process. Override with --output /absolute/path.jsonl.
The Debugger Snapshot
Every watch (on hit) and step (after advancing) returns a unified snapshot:
=== BREAKPOINT HIT === 0x00401000 (bp#1, hit #3)
Registers:
EAX=00000001 EBX=00000000 ECX=008800A0 EDX=00405678
ESI=008900B4 EDI=007A0000 EBP=0019FE40 ESP=0019FD00
EIP=00401000
Stack [ESP=0019FD00]:
+00: 00405678 +04: 0019FE80 +08: 00000000 +0C: 00000000
Disasm @ EIP:
> 00401000 sub esp, 0x234
00401006 push ebp
Status line in every response:
[attached: target.exe (pid 1234) | FROZEN @ 00401000 | bps: 3]
Workflow Recipes
Recipe 1: Quick function behavior check (trace)
Non-blocking -- target keeps running. See what arguments a function receives and what it returns.
python -m livetools trace 0x401000 --count 10 --read "ecx; [esp+4]:12:float32"
Recipe 2: Understand a function's code path (steptrace)
Record the actual instructions executed through a single invocation:
python -m livetools steptrace 0x401000 --max-insn 500 --call-depth 1 --detail branches
Recipe 3: Per-frame analysis (collect + fence + analyze)
Collect data over many frames, then analyze offline:
python -m livetools collect 0x401000 0x402000 \
--duration 30 --fence 0x403000 \
--read "ecx; [esp+4]:12:float32" \
--label 0x401000=FuncA --label 0x402000=FuncB \
--output trace.jsonl
python -m livetools analyze trace.jsonl --summary
python -m livetools analyze trace.jsonl --group-by "leave.eax"
python -m livetools analyze trace.jsonl --compare-intervals 10 50
python -m livetools analyze trace.jsonl --histogram "enter.reads.0.value.0"
Recipe 4: Verify a hook address is correct
Before hooking, confirm the address contains real code in the live process:
python -m livetools modules --filter <game_name>
python -m livetools disasm <target_addr> -n 5
If disasm shows garbage or errors, the address is wrong at runtime. For DLLs, rebase from modules output. For game .exe code, most x86 games load at preferred base โ static addresses work directly.
Recipe 5: Register inspection at a breakpoint
python -m livetools bp add 0x401000
python -m livetools watch --timeout 60
python -m livetools resume
Recipe 6: Read struct fields from a register pointer
After a snapshot shows ECX=008800A0 and you suspect a float at offset +0x54:
python -m livetools mem read 0x008800F4 4 --as float32
Recipe 7: Step through a function
python -m livetools bp add <function_entry>
python -m livetools watch --timeout 60
python -m livetools step over
python -m livetools step over
python -m livetools mem read <addr> 32
python -m livetools resume
Recipe 8: Patch a byte and verify
python -m livetools mem write 0x00401000 "B0 01 C3"
python -m livetools disasm 0x00401000 -n 3
Recipe 9: Multi-function data-driven investigation
Collect raw data, then ask targeted questions offline:
python -m livetools collect 0x401000 0x402000 0x403000 \
--duration 60 --fence 0x404000 --output scene.jsonl \
--label 0x401000=FuncA --label 0x402000=FuncB --label 0x403000=FuncC
python -m livetools analyze scene.jsonl --summary
python -m livetools analyze scene.jsonl --group-by addr
python -m livetools analyze scene.jsonl --filter "addr==0x00401000" --group-by "leave.eax"
python -m livetools analyze scene.jsonl --filter "addr==0x00401000" --cross-tab caller leave.eax
python -m livetools analyze scene.jsonl --export-csv scene.csv
Thinking Patterns
-
Hypothesis first. Form a hypothesis from static analysis BEFORE tracing. Example: "I think ECX is the visibility struct pointer. Let me verify with trace."
-
State what you learned. After inspecting trace data or a snapshot, explicitly state what the values tell you and what question remains.
-
Use trace before breakpoints. Non-blocking trace is less disruptive than blocking bp+watch. Start with trace to understand call frequency and typical arguments, then use breakpoints only when you need to freeze and step.
-
Use collect for volume. When you need data from thousands of calls across many frames, collect with a fence gives you structured JSONL that analyze can slice and dice deterministically.
-
Use analyze for deterministic answers. Never hallucinate statistics. Always run analyze on real collected data to get ground-truth numbers about call counts, return value distributions, per-frame behavior, etc.
-
Cross-reference with static analysis. Match live register values and call sites against static disassembly from retools to identify struct offsets, vtable slots, and data pointers.
-
Verify addresses before hooking. Most 32-bit game .exe files load at their preferred base (no ASLR), so static addresses from retools work directly. For DLLs or ASLR binaries, run modules --filter <name> and rebase: runtime_addr = runtime_base + (static_addr - preferred_base). When in doubt, modules is one command โ run it.
-
Composable pipeline. trace captures raw records. collect streams them to disk. analyze aggregates offline. Chain them for any investigation.
-
Hook the game's CALL instruction, not the DLL function. To trace a D3D9 method (or any API call), find the call [reg+offset] or call <addr> instruction in the game's code via xrefs.py or vtable.py calls. Hook THAT address. Do NOT compute the target address inside d3d9.dll and hook there โ the arguments are arranged at the caller, and the DLL entry point is shared across all callers.
-
Zero hits means something is wrong โ diagnose, don't give up. If trace/collect returns 0 samples: (a) Ask the user: is the game window focused and actively rendering? (b) Verify the address: disasm <addr> in livetools โ confirm real code exists there. (c) Try a known-hot address: dipcnt callers 10 finds confirmed active call sites; trace one to prove the hook pipeline works. (d) Only after all three pass should you reconsider whether the original address is actually called during gameplay.
Anti-Patterns
Do NOT chase the "real" device pointer. When working with D3D9, do NOT: read a device pointer from a global, dereference its vtable, compute d3d9.dll_base + slot_offset, and hook that address. This hooks inside the DLL where arguments are not in the expected layout and proxy/wrapper DLLs break the vtable chain. Hook the game's CALL instruction instead (pattern #9).
Do NOT explain away zero data. If a trace returns 0 samples, the answer is "I got no data and need to troubleshoot" โ not "the game doesn't appear to use this code path." Follow the escalation in pattern #10.
Do NOT pass DLL-internal addresses to trace/bp. Addresses from a DLL's export table or vtable layout belong to the DLL's code. Hooking them gives you the wrong context. Always prefer hooking in the game's own .text section.