Files
soothe2-re/scripts/fntrace2.py
T

196 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""fntrace2.py — fast single-point INT3 tracer for the live yabridge host.
Flow: poll for host spawn (5ms), immediately PTRACE_SEIZE the main thread,
poke INT3 at FN (no stop required - word write is atomic), then serve
waitpid events (clone children are auto-traced and continued). Logs args at
each hit. Works best against `reaper -renderproject` where all DSP work
happens in a burst right after host spawn.
Usage: python3 scripts/fntrace2.py [rpp] [nhits] [--render]
Env: FN_ADDR (default 0x180529fe0)
"""
import ctypes
import glob
import json
import os
import signal
import struct
import subprocess
import sys
import time
FN = int(os.environ.get('FN_ADDR', '0x180529FE0'), 16)
SNAPDIR = '/tmp/opencode/fntrace'
PTRACE_CONT = 7
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_PEEKDATA = 2
PTRACE_POKETEXT = 4
PTRACE_SINGLESTEP = 9
PTRACE_DETACH = 17
PTRACE_SEIZE = 0x4206
PTRACE_INTERRUPT = 0x4207
PTRACE_O_TRACECLONE = 0x00000002
class UserRegs(ctypes.Structure):
_fields_ = [(n, ctypes.c_ulonglong) for n in (
'r15', 'r14', 'r13', 'r12', 'rbp', 'rbx', 'r11', 'r10',
'r9', 'r8', 'rax', 'rcx', 'rdx', 'rsi', 'rdi', 'orig_rax',
'rip', 'cs', 'eflags', 'rsp', 'ss', 'fs_base', 'gs_base',
'ds', 'es', 'fs', 'gs')]
libc = ctypes.CDLL('libc.so.6', use_errno=True)
def ptrace(req, pid, addr=0, data=0):
libc.ptrace.restype = ctypes.c_long
r = libc.ptrace(req, pid, ctypes.c_void_p(addr), ctypes.c_void_p(data))
if r == -1:
e = ctypes.get_errno()
if req not in (PTRACE_PEEKDATA,):
raise OSError(e, f'ptrace({req:#x},{pid}) failed')
return None
return r
def find_host():
for p in glob.glob('/proc/[0-9]*'):
pid = int(os.path.basename(p))
try:
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace')
maps = open(f'/proc/{pid}/maps').read()
except Exception:
continue
if 'soothe2' in maps and 'reaper' not in cmd:
return pid
return None
def main():
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
nhits = int(sys.argv[2]) if len(sys.argv) > 2 else 12
render = '--render' in sys.argv
os.makedirs(SNAPDIR, exist_ok=True)
subprocess.run('pkill -9 -x reaser 2>/dev/null; pkill -9 -x reaper 2>/dev/null; '
"pkill -9 -f '[y]abridge' 2>/dev/null; sleep 1", shell=True)
if render:
proc = subprocess.Popen(
['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
'-renderproject', rpp],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
else:
proc = subprocess.Popen(
['/usr/bin/reaper', '-nosplash', '-ignoreerrors', rpp,
'/home/m/re-tools/play_loop.lua'],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
host = None
t0 = time.time()
while time.time() - t0 < 60 and not host:
host = find_host()
time.sleep(0.005)
if not host:
print('NO HOST')
return 1
print('host %d at %.2fs' % (host, time.time() - t0), flush=True)
ptrace(PTRACE_SEIZE, host, 0, PTRACE_O_TRACECLONE)
try:
ptrace(PTRACE_INTERRUPT, host)
os.waitpid(host, os.WUNTRACED)
except (OSError, ChildProcessError) as e:
print('interrupt fail', e)
orig = ptrace(PTRACE_PEEKDATA, host, FN)
cc = (orig & ~0xFF) | 0xCC
ptrace(PTRACE_POKETEXT, host, FN, cc)
print('armed %#x orig=%#x' % (FN, orig), flush=True)
try:
ptrace(PTRACE_CONT, host, 0, 0)
except OSError:
pass
known = {host}
log = []
hits = 0
t_last = time.time()
deadline_idle = float(os.environ.get('FNTRACE_IDLE', '25'))
while hits < nhits and time.time() - t_last < deadline_idle:
try:
pid, status = os.waitpid(-1, os.WSTOPPED | os.WUNTRACED)
except ChildProcessError:
break
sig = status >> 8
ev = status >> 16
known.add(pid)
t_last = time.time()
if ev == 3 or ev == 1: # CLONE/FORK event stop
try:
ptrace(PTRACE_CONT, pid, 0, 0)
except OSError:
pass
continue
if not os.WIFSTOPPED(pid):
continue
if sig == signal.SIGTRAP:
regs = UserRegs()
try:
ptrace(PTRACE_GETREGS, pid, 0, ctypes.addressof(regs))
except OSError:
continue
if regs.rip - 1 == FN:
ret = ptrace(PTRACE_PEEKDATA, pid, regs.rsp)
rec = dict(ctx=regs.rcx, a2=regs.rdx, cnt=regs.r8 & 0xffffffff,
r9=regs.r9 & 0xffffffff, ret=ret, rsp=regs.rsp,
rbx=regs.rbx, r12=regs.r12, r13=regs.r13,
r14=regs.r14, r15=regs.r15, rsi=regs.rsi, rdi=regs.rdi,
rip=regs.rip - 1, tid=pid)
log.append(rec)
hits += 1
print('hit %d tid=%d ctx=%#x a2=%#x cnt=%#x r9d=%#x ret=%#x'
% (hits, pid, regs.rcx, regs.rdx,
regs.r8 & 0xffffffff, regs.r9 & 0xffffffff, ret),
flush=True)
# step over
ptrace(PTRACE_POKETEXT, pid, FN, orig)
regs.rip = FN
ptrace(PTRACE_SETREGS, pid, 0, ctypes.addressof(regs))
ptrace(PTRACE_SINGLESTEP, pid, 0, 0)
try:
os.waitpid(pid, os.WUNTRACED)
except ChildProcessError:
pass
ptrace(PTRACE_POKETEXT, pid, FN, cc)
try:
ptrace(PTRACE_CONT, pid, 0, 0)
except OSError:
pass
else:
try:
ptrace(PTRACE_CONT, pid, 0, 0)
except OSError:
pass
else:
try:
ptrace(PTRACE_CONT, pid, 0, sig if 0 < sig < 0x20 else 0)
except OSError:
pass
print('hits:', hits, flush=True)
json.dump(log, open(f'{SNAPDIR}/hits.json', 'w'), indent=1)
for pid in list(known):
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
proc.kill()
print('saved', f'{SNAPDIR}/hits.json')
return 0
if __name__ == '__main__':
sys.exit(main())