diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index 271cfcf..ee9dd84 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -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]) из того же хита. diff --git a/scripts/fnexec.py b/scripts/fnexec.py new file mode 100644 index 0000000..4f82168 --- /dev/null +++ b/scripts/fnexec.py @@ -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=' 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())