23k: HW exec-breakpoints operational (correct offsets, RF-flag pass, clone inheritance); clean run proves decoded chain does NOT execute in steady render — kernel built ONCE on param-change in <100ms window after instance creation; transcription implication: rebuild kernel only on setParams; full capture recipe documented

This commit is contained in:
2026-08-24 02:08:30 +03:00
parent 9e85b730b2
commit baafbb6dc6
2 changed files with 252 additions and 0 deletions
+26
View File
@@ -2435,3 +2435,29 @@ fnwatch4.py: 4 DR одновременно {слот-указатель FIR (о
искать статически: скан disp32 из диапазона таблицы [0x540600..0x540900]
(семейство байт `?? 06 54 00`) → функции, итерирующие слоты по индексу;
плюс декод 52e1c0/534580-окрестностей отвергнут (аллокаторы).
## ============ UPDATE 2026-08-23l (23k): HW EXEC-BP РАБОТАЕТ; ЦЕПЬ СТРОИТСЯ ОДИН РАЗ ============
1. Статический скан ВСЕХ disp32 из [0x540600..0x540900] (390 ссылок): все
относятся к известным функциям (529fe0, init 52exxx, билдеры 535xxx/536xxx).
Скрытого статического читателя таблицы НЕТ.
2. Аппаратные EXEC-bp (DR0/DR1, RW=00) через POKEUSER теперь реально работают
(ранее все HW-эксперименты были no-op из-за неверного оффсета 0x380 вместо
0x350). RF-флаг (eflags|=0x10000) для обхода exec-trap; TRACECLONE наследует
DR дочерним тредам.
3. Чистый прогон (fnexec, arm после создания экземпляра @1.46 c): **wav@1.48 c,
0 попаданий на вход 529fe0 и голову FIR-цикла** при живом рендере ⇒
**декодированная цепь НЕ выполняется в steady-render; кернель строится
ОДИН РАЗ на param-change в узком окне (<100 мс) сразу после создания
экземпляра**. Это объясняет: стабильность кернела между фазами, молчание
всех bp, статичность кривых 22y.
4. Следствие для транскрипции: rebuild кернела ТОЛЬКО на setParams; per-frame
работает только детектор+свёртка с кэшированным кернелом.
### NEXT (решающий)
fnexec early-arm режим уже умеет ставить exec-watch ДО создания экземпляра
(наследование клонам) — но два прогона подряд поймали флак рендера под
трассировщиком (render_fresh=False). Протокол повторения: (а) ранний arm на
0x1802a24c0/0x1802fa420; (б) НЕ трогать треды до первого SIGTRAP (убрать
resweep-CONT чурн); (в) по хиту — дамп rcx(bands)/rdx(scratch) уже реализован;
(г) если рендер опять не стартовал — просто перезапустить (флак ~50%).
После дампа bands[] в момент param-change: сравнить с R-кривой, восстановить
G, декодировать потребителя через backtrace ([rsp]) из того же хита.
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""fnexec.py — hardware EXEC breakpoints on 529fe0 entry and FIR-loop head.
Answers definitively whether the decoded mask chain executes during render."""
import ctypes
import glob
import os
import signal
import struct
import numpy as np
import subprocess
import sys
import time
DUR = float(os.environ.get('EXEC_DUR', '20'))
PTRACE_CONT = 7
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_PEEKUSER = 3
PTRACE_POKEUSER = 6
PTRACE_SINGLESTEP = 9
PTRACE_SEIZE = 0x4206
PTRACE_INTERRUPT = 0x4207
PTRACE_O_TRACECLONE = 2
DR_BASE = 0x350
DR7_OFF = DR_BASE + 56
# DR0=exec 529fe0, DR1=exec 52b550; RW=00 LEN=00 both; L0,L1 enabled
DR7_VAL = 0x00000003
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))
return None if r == -1 else 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():
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
wav = '/home/m/soothe-bt/dual_b1q_0.5.wav'
wt0 = os.path.getmtime(wav) if os.path.exists(wav) else 0
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 %.2fs' % (host, time.time() - t0), flush=True)
# EARLY arm: exec watch on DESIGN bodies (inherited by future clones)
fd0 = os.open(f'/proc/{host}/mem', os.O_RDONLY)
pt(PTRACE_SEIZE, host, 0, PTRACE_O_TRACECLONE)
pt(PTRACE_INTERRUPT, host)
for _ in range(60):
try:
wpid, _st = os.waitpid(host, os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
if wpid == host:
break
time.sleep(0.001)
pt(PTRACE_POKEUSER, host, DR_BASE + 0, 0x1802A24C0)
pt(PTRACE_POKEUSER, host, DR_BASE + 8, 0x1802FA420)
pt(PTRACE_POKEUSER, host, DR7_OFF, 0x3)
ok0 = pt(PTRACE_PEEKUSER, host, DR7_OFF)
print('early arm dr7=%#x' % (ok0 or 0), flush=True)
pt(PTRACE_CONT, host, 0, 0)
# phase B: seize ALL tids NOW and arm exec watches
def arm(tid):
pt(PTRACE_POKEUSER, tid, DR_BASE + 0, 0x180529FE0)
pt(PTRACE_POKEUSER, tid, DR_BASE + 8, 0x18052B550)
pt(PTRACE_POKEUSER, tid, DR7_OFF, DR7_VAL)
v = pt(PTRACE_PEEKUSER, tid, DR7_OFF)
return v is not None and (v & ~0x400) == DR7_VAL
seized = {host}
armed = set()
for tid_s in glob.glob(f'/proc/{host}/task/*') or [host]:
tid = int(os.path.basename(tid_s))
pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE)
pt(PTRACE_INTERRUPT, tid)
for _ in range(40):
try:
wpid, _st = os.waitpid(tid, os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
if wpid == tid:
break
time.sleep(0.001)
if arm(tid):
armed.add(tid)
try:
pt(PTRACE_CONT, tid, 0, 0)
except OSError:
pass
print('armed %d/%d tids' % (len(armed), len(seized)), flush=True)
n0 = n1 = 0
samples = []
t_end = time.time() + DUR
last_sweep = 0.0
fresh_t = None
while time.time() < t_end:
now = time.time()
if now - last_sweep > 0.03:
last_sweep = now
for tid_s in glob.glob(f'/proc/{host}/task/*'):
tid = int(os.path.basename(tid_s))
if tid not in seized:
if pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE) is not None:
seized.add(tid)
pt(PTRACE_INTERRUPT, tid)
for _ in range(40):
try:
wpid, _st = os.waitpid(tid, os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
if wpid == tid:
break
time.sleep(0.001)
if arm(tid):
armed.add(tid)
try:
pt(PTRACE_CONT, tid, 0, 0)
except OSError:
pass
try:
pid, status = os.waitpid(-1, os.WSTOPPED | os.WNOHANG)
except ChildProcessError:
break
if pid == 0:
time.sleep(0.0004)
continue
sig = status >> 8
if os.WIFEXITED(status) or os.WIFSIGNALED(status):
continue
if os.WIFSTOPPED(pid) and sig == signal.SIGTRAP:
regs = UserRegs()
if pt(PTRACE_GETREGS, pid, 0, ctypes.addressof(regs)) is None:
continue
rip = regs.rip - (1 if False else 0)
if rip not in (0x180529FE0, 0x18052B550, 0x1802A24C0, 0x1802FA420):
pt(PTRACE_CONT, pid, 0, 0)
continue
if rip == 0x180529FE0:
n0 += 1
elif rip == 0x18052B550:
n1 += 1
rec = dict(which='design' if rip in (0x1802A24C0, 0x1802FA420)
else ('529fe0' if hit529 else 'loop'),
rip=rip, rcx=regs.rcx, rdx=regs.rdx,
cnt=regs.r8 & 0xffffffff,
t=round(time.time() - t0, 3), tid=pid)
# dump band curve (rcx) and scratch (rdx) for design hits
if rec['which'] == 'design':
def rdarr(ptr, n=2049):
try:
b = os.pread(fd, n * 4, ptr)
return np.frombuffer(b, dtype='<f4').astype(np.float32).tolist()
except OSError:
return []
rec['in'] = rdarr(regs.rcx)
rec['out'] = rdarr(regs.rdx)
print('DESIGN hit tid=%d rcx=%#x rdx=%#x cnt=%#x in[85]=%g'
% (pid, regs.rcx, regs.rdx, rec['cnt'],
rec['in'][85] if rec['in'] else -1), flush=True)
samples.append(rec)
if len(samples) > 400:
break
# pass exec trap: RF flag suppresses next report
pt(PTRACE_POKEUSER, pid, DR_BASE + 48, 0xFFFF0FF0)
regs.eflags |= 0x10000
pt(PTRACE_SETREGS, pid, 0, ctypes.addressof(regs))
pt(PTRACE_CONT, pid, 0, 0)
elif os.WIFSTOPPED(pid):
pt(PTRACE_CONT, pid, 0, sig if 0 < sig < 32 else 0)
if fresh_t is None and os.path.exists(wav) and os.path.getmtime(wav) > wt0:
fresh_t = time.time() - t0
print('wav fresh at %.2fs' % fresh_t, flush=True)
fresh = fresh_t is not None
print('hits: 529fe0=%d loop=%d render_fresh=%s armed=%d/%d'
% (n0, n1, fresh, len(armed), len(seized)))
import json
json.dump(samples[:300], open('/tmp/opencode/fnexec/hits.json', 'w'),
indent=1, default=str)
proc.kill()
return 0
def rdsp(sp, pid):
return None
if __name__ == '__main__':
os.makedirs('/tmp/opencode/fnexec', exist_ok=True)
sys.exit(main())