23b: mask->FIR chain decoded — DESIGN body is vectorized LOG2 of band curve (poly fingerprinted), WIN_freq identified as periodic Hann(4096) falling half applied to FIR[n/2..n), full ILT stub->impl table resolved offline (ilt_resolve.py), live buffer catalog extended (SIMD lane masks at 0x540598, complex identity reset between callbacks, overlap buffer at 0x5406f8 non-zero), plugin output proven nondeterministic across renders (LCG dither) — spectral metrics only; ptrace lab scripts + lessons (TRACECLONE before CONT, sub-second host lifecycle under -renderproject)

This commit is contained in:
2026-08-23 20:59:54 +03:00
parent 07cd4b7dc0
commit b8d5f83fc5
39 changed files with 190417 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""firstop.py — catch the plugin INSIDE its audio callback by repeatedly
SIGSTOP-ing the yabridge host and reading only the ctx-referenced arrays.
Between callbacks the FIR work buffer (ctx+0x540668) is reset to the complex
identity (1,0)x2049. A snapshot where FIR != identity means we stopped after
the mask->FIR build stage; those snapshots are saved with the full pipeline
state (bands/scratch/f6f8/track/R curves).
Usage: python3 scripts/firstop.py [rpp] [nattempts]
"""
import os
import signal
import struct
import subprocess
import sys
import time
import numpy as np
SNAPDIR = '/tmp/opencode/firstop'
SLOTS = [0x540548, 0x540550, 0x540598, 0x540628, 0x540668, 0x540678, 0x540688,
0x540698, 0x5406a8, 0x5406b8, 0x5406c8, 0x5406d8, 0x5406e8, 0x5406f8,
0x540708, 0x540718, 0x540728, 0x540738, 0x540748, 0x540758,
0x540768, 0x540778, 0x540788, 0x540798, 0x5407a8, 0x5407b8,
0x5407c8, 0x5407d8, 0x5407e8, 0x5407f8, 0x540808, 0x540818,
0x540828, 0x540838, 0x540848]
NARR = 8194
def find_host():
import glob
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'
nattempts = int(sys.argv[2]) if len(sys.argv) > 2 else 120
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)
proc = subprocess.Popen(
['/usr/bin/reaper', '-nosplash', '-ignoreerrors', rpp,
'/home/m/re-tools/play_loop.lua'],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
t0 = time.time()
host = None
while time.time() - t0 < 60 and not host:
host = find_host()
time.sleep(0.2)
if not host:
print('NO HOST')
return 1
print('host', host, flush=True)
time.sleep(12)
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
def rd(addr, n):
try:
return os.pread(fd, n, addr)
except OSError:
return None
def get_ctx():
# cheap probe: sens scalar must be >100 at known offset
for base in (CTX.get('base'),):
pass
return CTX.get('base')
# locate ctx once (while running): marker scan over heap only is heavy;
# reuse known-good address from prior sessions, validate via sens scalar.
CTX = {}
ctx = None
b = rd(0x2370040 + 0x540870, 4)
if b and struct.unpack('<f', b)[0] > 100:
ctx = 0x2370040
else:
print('known ctx invalid; full scan needed')
return 1
print('ctx', hex(ctx), flush=True)
hits = 0
saved = []
rng = np.random.default_rng(7)
for k in range(nattempts):
os.kill(host, signal.SIGSTOP)
try:
fir_p = struct.unpack('<Q', rd(ctx + 0x540668, 8))[0]
fb = rd(fir_p, 64 * 4)
ident = False
if fb:
arr = np.frombuffer(fb[:256], dtype='<f4')
ident = bool(np.all(np.abs(arr[0::2] - 1.0) < 1e-6))
ident = ident and bool(np.all(arr[1::2] == 0))
if not ident:
hits += 1
store = {}
for off in SLOTS:
pb = rd(ctx + off, 8)
if not pb:
continue
p = struct.unpack('<Q', pb)[0]
if p < 0x10000:
continue
ab = rd(p, NARR * 4)
if not ab:
continue
store[hex(off)] = np.frombuffer(ab, dtype='<f4').astype(np.float32)
fn = f'{SNAPDIR}/hit{k:03d}.npz'
np.savez_compressed(fn, **store)
saved.append(fn)
sc = rd(ctx + 0x2404dc, 4)
print(f'[{k}] HIT fir!=identity -> {fn} '
f'(fir[0..5]={np.round(store["0x540668"][:6],4)})', flush=True)
finally:
os.kill(host, signal.SIGCONT)
time.sleep(float(rng.uniform(0.02, 0.09)))
os.close(fd)
if proc.poll() is None:
proc.kill()
print(f'done: {hits} hits / {nattempts} attempts -> {SNAPDIR}')
return 0
if __name__ == '__main__':
sys.exit(main())
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""firtrace.py — live capture of mask->FIR pipeline buffers during realtime
playback, with GUI-publish flag forced ON so bands[i] curves are copied into
the 0x540728+ slots inside the audio callback (decomp 529fe0:144-183).
Dumps full vector-object arrays for every ctx slot in [0x540500,0x540900):
reads {data_ptr, end_ptr, cap_ptr} (std::vector layout) when available,
falls back to fixed-length float view.
Usage: python3 scripts/firtrace.py [rpp] [nsnap]
"""
import json
import os
import struct
import subprocess
import sys
import time
import numpy as np
SNAPDIR = '/tmp/opencode/firtrace'
def find_host():
import glob
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 mem_read(fd, addr, n):
try:
return os.pread(fd, n, addr)
except OSError:
return None
def mem_write(fd, addr, data):
try:
os.pwrite(fd, data, addr)
return True
except OSError:
return False
def parse_snap(path):
data = open(path, 'rb').read()
regs = []
i = 0
while i + 16 <= len(data):
lo, sz = struct.unpack_from('<QQ', data, i)
regs.append((lo, data[i + 16:i + 16 + sz]))
i += 16 + sz
return regs
def readabs(regs, addr, n):
for lo, body in regs:
if lo <= addr < lo + len(body) and addr - lo + n <= len(body):
return body[addr - lo:addr - lo + n]
return None
def find_ctx(regs):
sig = struct.pack('<I', 0x473b8000)
cands = []
for lo, body in regs:
j = body.find(sig)
while j >= 0:
base = lo + j - 0x24
b = readabs(regs, base + 0x540870, 4)
if b and struct.unpack('<f', b)[0] > 100:
cands.append(base)
j = body.find(sig, j + 1)
return sorted(set(cands))
SLOTS = list(range(0x540600, 0x540900, 8))
EXTRA = [0x540548, 0x540550, 0x540558, 0x540560, 0x540568, 0x540570,
0x540578, 0x540580, 0x540588, 0x540590, 0x540598, 0x5405a0,
0x5405a8, 0x5405b0, 0x5405b8, 0x5405c0, 0x5405c8, 0x5405d0,
0x5405d8, 0x5405e0, 0x5405e8, 0x5405f0, 0x5405f8]
def dump_slot(regs, ctx, off, maxf=65536):
"""Read qword at ctx+off; if it looks like a heap array, dump floats."""
b = readabs(regs, ctx + off, 8)
if not b:
return None
ptr = struct.unpack('<Q', b)[0]
if ptr < 0x10000 or ptr > 0x7fffffffffff:
return None
body = readabs(regs, ptr, min(maxf, 262144) * 4)
if body is None or len(body) < 64:
return None
a = np.frombuffer(body, dtype='<f4').astype(np.float64)
# trim trailing zeros beyond a floor of 2049 samples
nz = np.nonzero(a != 0)[0]
keep = max(2049, (nz[-1] + 1) if len(nz) else 0)
return a[:min(len(a), ((keep + 63) // 64) * 64)]
def main():
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
nsnap = int(sys.argv[2]) if len(sys.argv) > 2 else 4
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)
proc = subprocess.Popen(
['/usr/bin/reaper', '-nosplash', '-ignoreerrors', rpp,
'/home/m/re-tools/play_loop.lua'],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
t0 = time.time()
host = None
while time.time() - t0 < 60 and not host:
host = find_host()
time.sleep(0.2)
if not host:
print('NO HOST')
return 1
print('host', host, flush=True)
time.sleep(12)
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
# locate ctx via one full snapshot
snap0 = f'{SNAPDIR}/probe.bin'
snapshot(fd, host, snap0)
regs = parse_snap(snap0)
cands = find_ctx(regs)
print('ctx candidates:', ['0x%x' % c for c in cands][:5], flush=True)
if not cands:
return 1
ctx = cands[0]
# force GUI publish flag (byte at ctx+0x2404bc)
ok = mem_write(fd, ctx + 0x2404bc, b'\x01')
print('GUI flag write:', ok, flush=True)
store = {}
meta = {}
for k in range(nsnap):
time.sleep(1.5)
# re-arm flag (callback consumes it)
mem_write(fd, ctx + 0x2404bc, b'\x01')
time.sleep(0.05)
p = f'{SNAPDIR}/s{k}.bin'
snapshot(fd, host, p)
rg = parse_snap(p)
cs = find_ctx(rg)
if not cs:
continue
c = cs[0]
tag = f's{k}'
scal = {}
for a in range(0x540860, 0x5408a8, 4):
bb = readabs(rg, c + a, 4)
scal[hex(a)] = struct.unpack('<f', bb)[0] if bb else None
store[f'{tag}_scalars'] = json.dumps(scal)
for off in SLOTS + EXTRA:
arr = dump_slot(rg, c, off)
if arr is not None:
store[f'{tag}_{hex(off)}'] = arr
meta.setdefault(hex(off), []).append(len(arr))
print(f'snap {k}: dumped {sum(1 for x in store if x.startswith(tag+"_") and x!=tag+"_scalars")} arrays',
flush=True)
os.close(fd)
if proc.poll() is None:
proc.kill()
json.dump(meta, open(f'{SNAPDIR}/meta.json', 'w'), indent=1)
np.savez_compressed(f'{SNAPDIR}/firtrace.npz', **store)
print('saved', f'{SNAPDIR}/firtrace.npz')
return 0
def snapshot(fd, host, path):
out = open(path, 'wb')
nreg = nbytes = 0
for line in open(f'/proc/{host}/maps').read().splitlines():
p = line.split()
if len(p) < 2 or 'r' not in p[1]:
continue
lo, hi = (int(x, 16) for x in p[0].split('-'))
a = lo
while a < hi:
n = min(hi - a, 8 * 1024 * 1024)
d = mem_read(fd, a, n)
if d:
out.write(struct.pack('<QQ', a, len(d)))
out.write(d)
nreg += 1
nbytes += len(d)
a += n
out.close()
return nreg, nbytes
if __name__ == '__main__':
sys.exit(main())
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""fnall.py — safe full-thread INT3 tracer.
Invariant: every thread is SEIZE+INTERRUPT-stopped BEFORE the breakpoint is
armed, so any later clone descends from a traced thread and its SIGTRAPs come
to us instead of killing the host.
Env: FN_ADDR, FN_DUR, FN_MAXHITS
"""
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)
DUR = float(os.environ.get('FN_DUR', '15'))
MAXHITS = int(os.environ.get('FN_MAXHITS', '50'))
PTRACE_CONT = 7
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_PEEKDATA = 2
PTRACE_POKETEXT = 4
PTRACE_SINGLESTEP = 9
PTRACE_SEIZE = 0x4206
PTRACE_INTERRUPT = 0x4207
PTRACE_O_TRACECLONE = 0x00000002
libc = ctypes.CDLL('libc.so.6', use_errno=True)
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')]
def pt(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:
return None, ctypes.get_errno()
return r, 0
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 drain_waits():
out = []
while True:
try:
pid, status = os.waitpid(-1, os.WSTOPPED | os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
if pid == 0:
break
out.append((pid, status))
return out
def main():
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; sleep 1", shell=True)
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
'-renderproject', '/home/m/soothe-bt/dual_b1q_0.5.rpp'],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
host = None
t0 = time.time()
while time.time() - t0 < 30 and not host:
host = find_host()
time.sleep(0.002)
if not host:
print('NO HOST')
return 1
print('host %d at %.3fs' % (host, time.time() - t0), flush=True)
# phase 1: seize main, interrupt, wait stop
seized = []
r, e = pt(PTRACE_SEIZE, host, 0, PTRACE_O_TRACECLONE)
if r is None:
print('seize fail', e)
return 1
seized.append(host)
pt(PTRACE_INTERRUPT, host)
# phase 2: pump events, seize newcomers until set stabilizes
stable_until = time.time() + 2.5
deadline = time.time() + 8.0
while time.time() < deadline:
for pid, st in drain_waits():
pass # they stay stopped; we hold them
grew = False
for tid_s in glob.glob(f'/proc/{host}/task/*'):
tid = int(os.path.basename(tid_s))
if tid not in seized:
r, e = pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE)
if r is not None or e != 3:
seized.append(tid)
pt(PTRACE_INTERRUPT, tid)
grew = True
if grew:
stable_until = time.time() + 0.5
elif time.time() > stable_until:
break
time.sleep(0.002)
print('stopped %d threads' % len(seized), flush=True)
orig, _ = pt(PTRACE_PEEKDATA, host, FN)
cc = (orig & ~0xFF) | 0xCC
r, e = pt(PTRACE_POKETEXT, host, FN, cc)
if r is None:
print('arm fail', e)
return 1
print('armed %#x' % FN, flush=True)
log = []
hits = 0
EVENTS = []
for tid in seized:
pt(PTRACE_CONT, tid, 0, 0)
t_end = time.time() + DUR
last_resweep = time.time()
while hits < MAXHITS and time.time() < t_end:
try:
pid, status = os.waitpid(-1, os.WSTOPPED | os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
EVENTS.append((time.time()-t0, pid, hex(status), status >> 16))
if pid == 0:
if time.time() - last_resweep > 0.05:
last_resweep = time.time()
for tid_s in glob.glob(f'/proc/{host}/task/*'):
tid = int(os.path.basename(tid_s))
if tid not in seized:
r, e = pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE)
if r is not None or e != 3:
seized.append(tid)
pt(PTRACE_INTERRUPT, tid)
drain_waits()
pt(PTRACE_CONT, tid, 0, 0)
time.sleep(0.0005)
continue
sig = status >> 8
ev = status >> 16
if os.WIFEXITED(status) or os.WIFSIGNALED(status):
EVENTS.append((time.time()-t0, pid, hex(status), ev))
continue
if ev == 3 or ev == 1:
pt(PTRACE_CONT, pid, 0, 0)
continue
if os.WIFSTOPPED(pid) and sig == signal.SIGTRAP and ev == 0:
regs = UserRegs()
r, _ = pt(PTRACE_GETREGS, pid, 0, ctypes.addressof(regs))
if r is None:
continue
if regs.rip - 1 == FN:
ret, _ = pt(PTRACE_PEEKDATA, pid, regs.rsp)
rec = dict(ctx=regs.rcx, a2=regs.rdx, cnt=regs.r8 & 0xffffffff,
r9=regs.r9 & 0xffffffff, ret=ret,
rbx=regs.rbx, r12=regs.r12, r13=regs.r13,
r14=regs.r14, r15=regs.r15, rsp=regs.rsp, tid=pid)
log.append(rec)
hits += 1
if hits <= 20:
print('HIT ctx=%#x a2=%#x cnt=%#x ret=%#x'
% (rec['ctx'], rec['a2'], rec['cnt'], rec['ret']), flush=True)
pt(PTRACE_POKETEXT, pid, FN, orig)
regs.rip = FN
pt(PTRACE_SETREGS, pid, 0, ctypes.addressof(regs))
pt(PTRACE_SINGLESTEP, pid, 0, 0)
os.waitpid(pid, os.WUNTRACED)
pt(PTRACE_POKETEXT, pid, FN, cc)
pt(PTRACE_CONT, pid, 0, 0)
else:
pt(PTRACE_CONT, pid, 0, 0)
elif os.WIFSTOPPED(pid):
pt(PTRACE_CONT, pid, 0, sig if 0 < sig < 32 else 0)
pt(PTRACE_POKETEXT, host, FN, orig)
print('total hits:', hits)
json.dump(dict(log=log, events=EVENTS[:8000], nseized=len(seized)),
open('/tmp/opencode/fntrace/allhits.json', 'w'), indent=1)
proc.kill()
return 0
if __name__ == '__main__':
sys.exit(main())
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""fnhw.py — hardware-breakpoint tracer (DR0) for the yabridge host.
Safe for un-traced threads (code is never patched). Auto-arms new threads.
Env: FN_ADDR (target VA), FN_DUR (seconds), FN_SKIP (skip first N hits)
"""
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)
DUR = float(os.environ.get('FN_DUR', '15'))
PTRACE_CONT = 7
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_PEEKUSER = 3
PTRACE_POKEUSER = 6
PTRACE_SINGLESTEP = 9
PTRACE_SEIZE = 0x4206
PTRACE_INTERRUPT = 0x4207
libc = ctypes.CDLL('libc.so.6', use_errno=True)
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')]
def pt(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:
return None, ctypes.get_errno()
return r, 0
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 find_dr0_off(pid):
"""Locate DR0 slot inside the USER area by probing."""
probe = 0x1234567890abcdef
for off in range(0x200, 0x400, 8):
r, e = pt(PTRACE_POKEUSER, pid, off, probe)
if r is None:
continue
v, _ = pt(PTRACE_PEEKUSER, pid, off)
if v == (probe & 0xFFFFFFFFFFFFFFFF):
# restore zero & sanity-check neighbours exist
pt(PTRACE_POKEUSER, pid, off, 0)
return off
return None
def arm(tid, dr0_off):
"""Arm DR0/DR7 - requires tid to be STOPPED; caller handles stop/cont."""
pt(PTRACE_POKEUSER, tid, dr0_off, FN)
# DR7 (index 7): L0=1, RW0=00 (exec), LEN0=00
pt(PTRACE_POKEUSER, tid, dr0_off + 7 * 8, 0x1)
v, _ = pt(PTRACE_PEEKUSER, tid, dr0_off + 7 * 8)
return v == 1
def disarm(tid, dr0_off):
pt(PTRACE_POKEUSER, tid, dr0_off + 7 * 8, 0)
pt(PTRACE_POKEUSER, tid, dr0_off, 0)
def arm_stopped(tid, dr0_off):
"""INTERRUPT -> (bounded) wait -> arm -> CONT. True if DR7 verified."""
pt(PTRACE_INTERRUPT, tid)
for _ in range(30):
try:
pid, st = os.waitpid(tid, os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
return False
if pid == tid:
break
time.sleep(0.002)
r0, e0 = pt(PTRACE_POKEUSER, tid, dr0_off, FN)
r7, e7 = pt(PTRACE_POKEUSER, tid, dr0_off + 7 * 8, 0x1)
v, _ = pt(PTRACE_PEEKUSER, tid, dr0_off + 7 * 8)
print(' arm tid=%d poke_dr0=%s(e%s) poke_dr7=%s(e%s) dr7_read=%s'
% (tid, 'ok' if r0 is not None else 'FAIL', e0,
'ok' if r7 is not None else 'FAIL', e7, hex(v or 0)), flush=True)
pt(PTRACE_CONT, tid, 0, 0)
return v == 1
def main():
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; sleep 1", shell=True)
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
'-renderproject', '/home/m/soothe-bt/dual_b1q_0.5.rpp'],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
host = None
t0 = time.time()
while time.time() - t0 < 30 and not host:
host = find_host()
time.sleep(0.002)
if not host:
print('NO HOST')
return 1
print('host %d at %.3fs' % (host, time.time() - t0), flush=True)
seized = []
for tid in [int(os.path.basename(p)) for p in glob.glob(f'/proc/{host}/task/*')] or [host]:
if tid not in seized:
r, e = pt(PTRACE_SEIZE, tid, 0, 0)
if r is not None or e != 3:
seized.append(tid)
# stop one thread briefly to locate DR0 offset
tgt = seized[0]
pt(PTRACE_INTERRUPT, tgt)
os.waitpid(tgt, os.WUNTRACED)
dr0_off = find_dr0_off(tgt)
print('DR0 user-offset:', hex(dr0_off) if dr0_off else 'NOT FOUND', flush=True)
if not dr0_off:
return 1
pt(PTRACE_CONT, tgt, 0, 0)
n_ok = 0
for tid in seized:
if arm_stopped(tid, dr0_off):
n_ok += 1
print('armed %#x on %d/%d tids (DR7 verified)' % (FN, n_ok, len(seized)), flush=True)
log = []
hits = 0
skip = int(os.environ.get('FN_SKIP', '0'))
t_end = time.time() + DUR
last_resweep = 0.0
while time.time() < t_end:
now = time.time()
if now - last_resweep > 0.03:
last_resweep = now
for tid_s in glob.glob(f'/proc/{host}/task/*'):
tid = int(os.path.basename(tid_s))
if tid not in seized:
r, e = pt(PTRACE_SEIZE, tid, 0, 0)
if r is not None or e != 3:
seized.append(tid)
if arm_stopped(tid, dr0_off):
print('+tid', tid, flush=True)
try:
pid, status = os.waitpid(-1, os.WSTOPPED | os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
if pid == 0:
time.sleep(0.0005)
continue
sig = status >> 8
ev = status >> 16
if os.WIFEXITED(status) or os.WIFSIGNALED(status):
continue
if ev == 3 or ev == 1:
pt(PTRACE_CONT, pid, 0, 0)
continue
if os.WIFSTOPPED(pid) and sig == signal.SIGTRAP:
regs = UserRegs()
r, _ = pt(PTRACE_GETREGS, pid, 0, ctypes.addressof(regs))
if r is None:
continue
if regs.rip == FN or regs.rip == FN + 1:
rip = FN
ret, _ = pt(PTRACE_PEEKDATA, pid, regs.rsp)
rec = dict(ctx=regs.rcx, a2=regs.rdx, cnt=regs.r8 & 0xffffffff,
r9=regs.r9 & 0xffffffff, ret=ret,
rbx=regs.rbx, r12=regs.r12, r13=regs.r13,
r14=regs.r14, r15=regs.r15, rsp=regs.rsp, tid=pid)
if hits >= skip:
log.append(rec)
print('HIT ctx=%#x a2=%#x cnt=%#x r9d=%#x ret=%#x'
% (rec['ctx'], rec['a2'], rec['cnt'], rec['r9'], rec['ret']),
flush=True)
hits += 1
# pass bp: clear DR0 temporarily, single-step, re-arm
pt(PTRACE_POKEUSER, pid, dr0_off + 7 * 8, 0)
regs.eflags |= 0x100 # TF
pt(PTRACE_SETREGS, pid, 0, ctypes.addressof(regs))
pt(PTRACE_SINGLESTEP, pid, 0, 0)
os.waitpid(pid, os.WUNTRACED)
regs2 = UserRegs()
pt(PTRACE_GETREGS, pid, 0, ctypes.addressof(regs2))
regs2.eflags &= ~0x100
pt(PTRACE_SETREGS, pid, 0, ctypes.addressof(regs2))
arm(pid, dr0_off)
pt(PTRACE_CONT, pid, 0, 0)
else:
# stray SIGTRAP (wine internal): forward
pt(PTRACE_CONT, pid, 0, signal.SIGTRAP)
elif os.WIFSTOPPED(pid):
pt(PTRACE_CONT, pid, 0, sig if 0 < sig < 32 else 0)
print('total hits:', hits, 'logged:', len(log))
json.dump(log, open('/tmp/opencode/fntrace/hwhits.json', 'w'), indent=1)
for tid in seized:
disarm(tid, dr0_off)
proc.kill()
return 0
if __name__ == '__main__':
sys.exit(main())
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""fntrace.py — ptrace INT3 tracer for FUN_180529fe0 (mask chain vtbl slot 6)
in the live yabridge host. The wine module is mapped at its preferred base,
so dump VAs == runtime addresses (verified: exec map 0x180001000-0x181baa000).
On each hit logs: RIP, RCX (ctx), RDX, R8D (count), R9D, [RSP] (return addr),
plus xmm0/xmm1 low scalars if available via GETFPREGS (skipped: not portable).
Usage: python3 scripts/fntrace.py [rpp] [nhits]
"""
import ctypes
import glob
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_TRACEME = 0
PTRACE_PEEKTEXT = 1
PTRACE_PEEKDATA = 2
PTRACE_POKETEXT = 4
PTRACE_CONT = 7
PTRACE_SINGLESTEP = 9
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_ATTACH = 16
PTRACE_DETACH = 17
PTRACE_SEIZE = 0x4206
PTRACE_INTERRUPT = 0x4207
PTRACE_O_TRACECLONE = 0x00000002
PTRACE_EVENT_CLONE = 3 # status >> 16 == 4 (event+1)? actually event = status>>16, CLONE==3 -> 4? use raw compare below
PTRACE_EVENT_FORK = 1
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_PEEKTEXT, 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 24
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)
t0 = time.time()
host = None
while time.time() - t0 < 60 and not host:
host = find_host()
time.sleep(0.05)
if not host:
print('NO HOST')
return 1
print('host', host, 'at %.1fs' % (time.time() - t0), flush=True)
tids = [int(os.path.basename(p)) for p in glob.glob(f'/proc/{host}/task/*')]
print('tids:', tids, flush=True)
seized = []
for tid in tids:
try:
ptrace(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE)
seized.append(tid)
except OSError as e:
print('seize fail', tid, e)
if not seized:
return 1
# stop everyone
stopped = []
for tid in seized:
try:
ptrace(PTRACE_INTERRUPT, tid)
os.waitpid(tid, os.WUNTRACED)
stopped.append(tid)
except (OSError, ChildProcessError):
pass
orig = ptrace(PTRACE_PEEKTEXT, stopped[0], FN)
cc = (orig & ~0xFF) | 0xCC
ptrace(PTRACE_POKETEXT, stopped[0], FN, cc)
print('breakpoint armed at %#x (orig=%#x)' % (FN, orig), flush=True)
for tid in stopped:
try:
ptrace(PTRACE_CONT, tid, 0, 0)
except OSError:
pass
hits = 0
log = []
import select
import time as _t
t_last = _t.time()
idle_deadline = float(os.environ.get('FNTRACE_IDLE', '20'))
while hits < nhits and _t.time() - t_last < idle_deadline:
try:
pid, status = os.waitpid(-1, os.WUNTRACED | os.WSTOPPED | os.WNOHANG)
except ChildProcessError:
break
if pid == 0:
_t.sleep(0.005)
continue
sig = status >> 8
if os.WIFEXITED(pid and status or status) or os.WIFSIGNALED(status):
# thread/process exited (normal in wine): forget it
if pid in seized:
seized.remove(pid)
if pid in stopped:
stopped.remove(pid)
continue
if not os.WIFSTOPPED(pid):
continue
t_last = _t.time()
ev = status >> 16
if ev == PTRACE_EVENT_CLONE or ev == PTRACE_EVENT_FORK:
if pid not in seized:
seized.append(pid)
ptrace(PTRACE_CONT, pid, 0, 0)
continue
if sig == signal.SIGTRAP:
regs = UserRegs()
try:
ptrace(PTRACE_GETREGS, pid, 0, ctypes.addressof(regs))
except OSError:
ptrace(PTRACE_CONT, pid, 0, 0)
continue
if regs.rip - 1 == FN:
ret = ptrace(PTRACE_PEEKDATA, pid, regs.rsp)
rec = dict(rip=regs.rip - 1, ctx=regs.rcx, a2=regs.rdx,
cnt=regs.r8 & 0xffffffff, r9=regs.r9 & 0xffffffff,
ret=ret, tid=pid,
rbx=regs.rbx, rbp_=regs.rbp, rsi=regs.rsi, rdi=regs.rdi)
log.append(rec)
hits += 1
print(f'hit {hits}: tid={pid} ctx={regs.rcx:#x} '
f'a2={regs.rdx:#x} cnt={regs.r8:#x} r9d={regs.r9:#x} '
f'ret={ret:#x}', flush=True)
# step over int3
lo = struct.unpack('<Q', struct.pack('<Q', orig ^ ((orig ^ cc) & 0xFF)))[0]
ptrace(PTRACE_POKETEXT, pid, FN, orig)
regs.rip = FN
ptrace(PTRACE_SETREGS, pid, 0, ctypes.addressof(regs))
ptrace(PTRACE_SINGLESTEP, pid, 0, 0)
os.waitpid(pid, os.WUNTRACED)
ptrace(PTRACE_POKETEXT, pid, FN, cc)
ptrace(PTRACE_CONT, pid, 0, 0)
else:
ptrace(PTRACE_CONT, pid, 0, 0)
else:
# other stop signals: deliver and continue
ptrace(PTRACE_CONT, pid, 0, sig if 0 < sig < 0x20 else 0)
# cleanup: remove breakpoint, detach
print('done hits=', hits, 'cleaning up...', flush=True)
for tid in seized:
try:
ptrace(PTRACE_INTERRUPT, tid)
os.waitpid(tid, os.WUNTRACED)
except (OSError, ChildProcessError):
continue
try:
ptrace(PTRACE_POKETEXT, stopped[0], FN, orig)
except Exception as e:
print('restore fail', e)
for tid in seized:
try:
ptrace(PTRACE_DETACH, tid, 0, 0)
except OSError:
pass
import json
json.dump(log, open(f'{SNAPDIR}/hits.json', 'w'), indent=1)
print('saved', f'{SNAPDIR}/hits.json')
if proc.poll() is None:
proc.kill()
return 0
if __name__ == '__main__':
sys.exit(main())
+195
View File
@@ -0,0 +1,195 @@
#!/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())
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""fntrace3.py — decisive INT3 experiment: seize ALL tids within milliseconds
of host spawn, arm breakpoint, log EVERY waitpid event for N seconds."""
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)
DUR = float(os.environ.get('FN_DUR', '25'))
PTRACE_CONT = 7
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_PEEKDATA = 2
PTRACE_POKETEXT = 4
PTRACE_SINGLESTEP = 9
PTRACE_SEIZE = 0x4206
PTRACE_INTERRUPT = 0x4207
PTRACE_LISTEN = 0x4208
PTRACE_O_TRACECLONE = 2
libc = ctypes.CDLL('libc.so.6', use_errno=True)
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')]
def pt(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:
return None, ctypes.get_errno()
return r, 0
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 = '/home/m/soothe-bt/dual_b1q_0.5.rpp'
subprocess.run('pkill -9 -x reaper; pkill -9 -f \'[y]abridge\'; sleep 1', shell=True)
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
'-renderproject', rpp],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
host = None
t0 = time.time()
while time.time() - t0 < 30 and not host:
host = find_host()
time.sleep(0.002)
if not host:
print('NO HOST')
return 1
print('host %d at %.3fs' % (host, time.time() - t0), flush=True)
seized = []
for tid in [host] + [int(os.path.basename(p)) for p in glob.glob(f'/proc/{host}/task/*')]:
if tid in seized:
continue
r, e = pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE)
if r is None and e == 3: # ESRCH - gone
continue
seized.append(tid)
# interrupt to allow poke below (poke needs SOME stopped thread)
print('seized:', seized, flush=True)
# stop one thread to enable POKETEXT
tgt = seized[0]
pt(PTRACE_INTERRUPT, tgt)
os.waitpid(tgt, os.WUNTRACED)
orig, _ = pt(PTRACE_PEEKDATA, tgt, FN)
cc = (orig & ~0xFF) | 0xCC
pt(PTRACE_POKETEXT, tgt, FN, cc)
print('armed orig=%#x' % orig, flush=True)
# seize any tids spawned meanwhile
for tid in [int(os.path.basename(p)) for p in glob.glob(f'/proc/{host}/task/*')]:
if tid not in seized:
r, e = pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE)
if r is not None or e != 3:
seized.append(tid)
for tid in seized:
try:
pt(PTRACE_CONT, tid, 0, 0)
except Exception:
pass
log = []
events = []
hits = 0
t_end = time.time() + DUR
last_resweep = 0.0
while time.time() < t_end:
now = time.time()
if now - last_resweep > 0.05:
last_resweep = now
for tid_s in glob.glob(f'/proc/{host}/task/*'):
tid = int(os.path.basename(tid_s))
if tid not in seized:
r, e = pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE)
if r is not None or e != 3:
seized.append(tid)
print('+tid', tid, flush=True)
pid, status = os.waitpid(-1, os.WSTOPPED | os.WUNTRACED | os.WNOHANG)
if pid == 0:
time.sleep(0.001)
continue
sig = status >> 8
ev = status >> 16
events.append((time.time() - t0, pid, hex(status), ev))
if pid not in seized:
r, e = pt(PTRACE_SEIZE, pid, 0, PTRACE_O_TRACECLONE)
if r is not None or e != 3:
seized.append(pid)
if ev == 3 or ev == 1:
pt(PTRACE_CONT, pid, 0, 0)
continue
if os.WIFSTOPPED(pid) and sig == signal.SIGTRAP:
regs = UserRegs()
r, _ = pt(PTRACE_GETREGS, pid, 0, ctypes.addressof(regs))
if r is None:
continue
if regs.rip - 1 == FN:
ret, _ = pt(PTRACE_PEEKDATA, pid, regs.rsp)
rec = dict(ctx=regs.rcx, a2=regs.rdx, cnt=regs.r8 & 0xffffffff,
r9=regs.r9 & 0xffffffff, ret=ret, rbx=regs.rbx,
r12=regs.r12, r13=regs.r13, r14=regs.r14,
r15=regs.r15, rsp=regs.rsp, tid=pid)
log.append(rec)
hits += 1
print('HIT ctx=%#x a2=%#x cnt=%#x ret=%#x'
% (regs.rcx, regs.rdx, regs.r8 & 0xffffffff, ret), flush=True)
pt(PTRACE_POKETEXT, pid, FN, orig)
regs.rip = FN
pt(PTRACE_SETREGS, pid, 0, ctypes.addressof(regs))
pt(PTRACE_SINGLESTEP, pid, 0, 0)
os.waitpid(pid, os.WUNTRACED)
pt(PTRACE_POKETEXT, pid, FN, cc)
pt(PTRACE_CONT, pid, 0, 0)
else:
pt(PTRACE_CONT, pid, 0, 0)
elif os.WIFEXITED(status) or os.WIFSIGNALED(status):
continue
else:
pt(PTRACE_CONT, pid, 0, sig if 0 < sig < 32 else 0)
print('total hits:', hits, 'events:', len(events))
json.dump(dict(log=log, events=events[:400]),
open('/tmp/opencode/fntrace/hits3.json', 'w'), indent=1)
proc.kill()
return 0
if __name__ == '__main__':
sys.exit(main())
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""hotips.py — sample thread RIPs aggressively during the render burst to find
which module code actually executes (the real DSP path)."""
import collections
import glob
import os
import subprocess
import sys
import time
MOD_LO, MOD_HI = 0x180001000, 0x181baa000
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():
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; sleep 1", shell=True)
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
'-renderproject', '/home/m/soothe-bt/dual_b1q_0.5.rpp'],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
host = None
t0 = time.time()
while time.time() - t0 < 30 and not host:
host = find_host()
time.sleep(0.002)
print('host', host, 'at %.3fs' % (time.time() - t0))
ips = collections.Counter()
t_end = time.time() + float(os.environ.get('HOT_DUR', '6'))
n_ok = n_fail = 0
while time.time() < t_end:
for tid_s in glob.glob(f'/proc/{host}/task/*'):
try:
parts = open(f'{tid_s}/syscall').read().split()
ip = int(parts[-1], 16)
n_ok += 1
if MOD_LO <= ip < MOD_HI:
ips[ip] += 1
except Exception:
n_fail += 1
proc.kill()
print('samples ok=%d fail=%d, in-module=%d' % (n_ok, n_fail, sum(ips.values())))
# bucket to 64-byte lines, aggregate by function-ish regions
agg = collections.Counter()
for ip, c in ips.items():
agg[(ip >> 6) << 6] += c
for addr, c in agg.most_common(40):
print('%#x %d' % (addr, c))
if __name__ == '__main__':
main()
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""ilt_resolve.py — resolve ILT dispatch stubs of soothe_mem.bin (raw dump, base 0x180000000).
Stub pattern (7+7+4 bytes):
48 63 05 rel32 mov eax, [rip+rel32] ; slot index (runtime-fixed after reloc)
4c 8d 15 rel32 lea r10, [rip+rel32] ; pointer table base
41 ff 24 d2 jmp qword ptr [r10+rax*8]
Resolution (all offline from the dump, post-relocation values):
idx_addr = stub + 7 + rel32_a
tbl_addr = stub + 14 + rel32_b
impl = u64[tbl_addr + idx * 8]
Usage: ilt_resolve.py [VA ...] (default: FIR-loop + main-loop stub set)
"""
import struct
import sys
BASE = 0x180000000
BIN = '/home/m/re-tools/soothe_mem.bin'
_data = open(BIN, 'rb').read()
def rd(va, n):
off = va - BASE
return _data[off:off + n]
def u32(va):
return struct.unpack('<I', rd(va, 4))[0]
def u64(va):
return struct.unpack('<Q', rd(va, 8))[0]
def resolve_stub(va):
"""Return dict describing the ILT stub at va, or None if pattern mismatch."""
b = rd(va, 18)
if len(b) < 18 or b[0] != 0x48 or b[1] != 0x63 or b[2] != 0x05:
return None
rel_a = struct.unpack_from('<i', b, 3)[0]
if b[7] != 0x4C or b[8] != 0x8D or b[9] != 0x15:
return None
rel_b = struct.unpack_from('<i', b, 10)[0]
idx_addr = va + 7 + rel_a
tbl_addr = va + 14 + rel_b
idx = u32(idx_addr)
impl = u64(tbl_addr + idx * 8)
return dict(stub=va, idx_addr=idx_addr, idx=idx,
tbl_addr=tbl_addr, impl=impl)
DEFAULT_STUBS = [
# FIR-build loop 52b550..52b8bb
0x180002210, # copy scratch->FIR ?
0x180002180, # complex-op A/C (float)
0x180001bb0, # complex-op A/C (double)
0x180001a90, # complex-op B/D (float)
0x1800019d0, # complex-op B/D (double)
0x180001880, # paired-scalar op 1 (flag branch)
0x180001ca0, # paired-scalar op 2 (flag branch)
0x180001df0, # final op (float)
0x180001f70, # final op (double)
# import thunks of 535a70 dispatcher
0x180140a10,
0x180140a70,
# main-loop transforms referenced by BLOCKMAP
0x180002030, 0x180001d30, 0x180002270, 0x1800022a0,
0x180001970, 0x180001a60, 0x180001850, 0x180002000,
0x180001c40, 0x180001fa0, 0x180001940, 0x180001f10,
0x180001c70, 0x180001d60, 0x1800019a0, 0x180001a00,
]
def main():
args = [int(a, 16) if not a.startswith('0x') else int(a, 0)
for a in sys.argv[1:]] or DEFAULT_STUBS
print(f'{"stub":>12} {"idx@":>12} {"idx":>4} {"table":>12} {"impl":>12}')
rows = []
for va in args:
r = resolve_stub(va)
if r is None:
print(f'{va:#12x} PATTERN MISMATCH')
continue
rows.append(r)
print(f'{r["stub"]:#12x} {r["idx_addr"]:#12x} {r["idx"]:4d} '
f'{r["tbl_addr"]:#12x} {r["impl"]:#12x}')
return rows
if __name__ == '__main__':
main()