diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index 709fe52..8851618 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -4236,3 +4236,54 @@ lvl_raw → track_i (буфер [rsp+0x138][band]) и пост-обработк 2. Найти консюмера track/FIR после fn529fe0 (FFT-conv движок) — как track_i превращается в применённую маску аудио. 3. C++ порт RT_CASCADE=1 (Этап C) — цепь до df0 уже можно транскрибировать. + +## ============ 24mm12: НАЙДЕН ДЕТЕКТОРНЫЙ КАСКАД = СТАДИЯ vt+0x28 = 180529c60; КАРТА VTABLE ПАЙПЛАЙНА ============ + +### Оркестратор (0x1805300f0..0x1805305e7) +fn529fe0 вызывается ВИРТУАЛЬНО: `call [ctx_vtable+0x30]` (сайт 530371), +аргументы (rcx=ctx, rdx=ТАБЛИЦА TRACK = ctx+0x380, r8d=4096=[ctx+0x1a8], +r9d=nbands=[ctx+0x30]). До него в том же оркестраторе — серия виртуальных +вызовов других стадий. Полоса-цикл после fn: cmp [rsi+0x30],r13; +r15=ctx+0x360, r14=ctx+0x2e0 — ещё таблицы-указатели в малых оффсетах ctx. + +### Карта vtable ctx (live, comb_b1234) +``` ++0x08 0x18052cfb0 +0x10 0x180529550 +0x18 0x1804714b0 (чужой модуль?) ++0x20 0x18052b940 +0x28 0x180529c60 ← ДЕТЕКТОР +0x30 0x180529fe0 (fn) ++0x38 0x180529ef0 (prep) +0x40 0x18052bbf0 ++0xe8 0x1804731c0 (чужой модуль?) +0x218 0x180481210 +``` +Трасса кадра: [e8,e8]? → 38 → 28(×nbands) → 30(fn). Писец track — ТОЛЬКО +vt+0x28 (430/430 изменений md5). + +### 180529c60 — детекторный каскад, ВСЯ функция 0x281 байт +``` +lock bts [ctx+0x2404dc] ; тот же лок что в fn +r9 = rdx (СТАРЫЙ track всей таблицы? базовый буфер) +r12 = band*2; rcx = [ctx + r12*8 + 0x540678] ; bands_curve полосы +n2 = ([ctx+0x64]±)/2+1 +call 0x1805355d0(bands, rdx, n2) ; ?? первичное смешение +loop по [ctx+0x1b0]: + rbp = [ctx+r12*8+0x540678] + call 0x180530080(rbp, rbp+4, n2-1) ; ?? + call 0x18052d920(rbp, xmm1=0.5f, n2-1) ; масштаб ×0.5 ! + r15 = [ctx+0x5406f8] ; vec6f8 + call 0x1800020f0 / 0x180001850(rsi=rbp+4, rdx=rbp, r8=r15, r9=n2-1) + call 0x180001a00 / ...(rcx=r15, rdx=rsi, xmm=0.5f/0.5d) +``` +То есть «шаги 9–17» из BLOCKMAP 24mm5 на самом деле живут ЗДЕСЬ (+хелперы), +а НЕ в fn529fe0: fn529fe0 получает ГОТОВЫЕ track-буферы (arg2) и строит +из них ядро (df0: T⊗F), не вычисляя детектор! + +### Статус Этапа B +Осталось декодировать: хелперы 5355d0 / 530080 / 20f0 / 1850 / 1a00 / +52d920-семантику и роль vec6f8@6f8 — десятки инструкций, вся рекуррентия +track_{t+1} замкнётся офлайн. Затем γ=1.760561 выводится аналитически. + +### Инструменты раунда +scripts/wine_stage_trace.py (vtable-stage трассировщик c md5-атрибуцией +записей), scripts/wine_ptrace_trace.py (+TRACKSAVE/DIV/DC40/EXPVAR/FN, +mapped-фильтр, строгая валидация ctx по trk==exp(scr)). +Грабли: адреса-константы в патчах скрипта молча не применялись при чужом +отступе — проверять installed-list; DC40=0x1800dc40 (не 0x18000dc40); +DIV/DC40/EXPVAR/FN могут быть вне маппинга процесса (пропускать). diff --git a/scripts/wine_ptrace_trace.py b/scripts/wine_ptrace_trace.py index 569332c..612f2d1 100644 --- a/scripts/wine_ptrace_trace.py +++ b/scripts/wine_ptrace_trace.py @@ -28,6 +28,11 @@ BP_EXP = 0x1803831c0 BP_DF0 = 0x18000b3c0 BP_COPY = 0x1800136e0 BP_DF0RET = 0x18052b898 +BP_TRACKSAVE = 0x18052b574 +BP_DIV = 0x1803a06a0 +BP_DC40 = 0x1800dc40 +BP_EXPVAR = 0x1802dc0e0 +BP_FN = 0x180529fe0 CTX_SLOTS = {'scr': 0x540628, 'trk': 0x540688, 'cur': 0x540678, 'fir_ptr': 0x540668} @@ -256,18 +261,40 @@ def main(): # Фаза 3: int3 и запуск bps = {} + # проверка маппенности по /proc/pid/maps + maps_txt = open(f'/proc/{host}/maps').read() + + def mapped(a): + for ln in maps_txt.splitlines(): + rng = ln.split()[0] + lo, hi = (int(x, 16) for x in rng.split('-')) + if lo <= a < hi: + return True + return False + for name, addr in (('COPY', BP_COPY), ('EXP', BP_EXP), ('DF0', BP_DF0), - ('DF0RET', BP_DF0RET)): + ('DF0RET', BP_DF0RET), ('TRACKSAVE', BP_TRACKSAVE), + ('DIV', BP_DIV), ('DC40', BP_DC40), + ('EXPVAR', BP_EXPVAR), ('FN', BP_FN)): + if not mapped(addr): + print('!! %s@%#x не смапплен — пропуск' % (nm_ := name, addr), flush=True) + continue orig = peek(host, addr) poke(host, addr, (orig & ~0xFF) | 0xCC) bps[addr] = (name, orig & 0xFF) print('int3 installed:', {hex(a): n for a, (n, _) in bps.items()}, flush=True) + for addr, (nm, _) in bps.items(): + rb = peek(host, addr) & 0xFF + if rb != 0xCC: + print('!! %s@%#x НЕ 0xCC: %#02x' % (nm, addr, rb), flush=True) for tid in attached: pt(PTRACE_CONT, tid, 0, 0) samples = [] - hits = {'COPY': 0, 'EXP': 0, 'DF0': 0, 'DF0RET': 0} + hits = {'COPY': 0, 'EXP': 0, 'DF0': 0, 'DF0RET': 0, 'TRACKSAVE': 0, + 'DIV': 0, 'DC40': 0, 'EXPVAR': 0, 'FN': 0} track_by_tid = {} + track_dumps = [] t_start = time.time() def snapshot_slots(rec): @@ -309,6 +336,70 @@ def main(): pt(PTRACE_CONT, pid, 0, 0) continue kind, obyte = info + if kind == 'TRACKSAVE': + # rax = track-ptr текущей полосы, r12 = индекс полосы, + # [rsp+0x138] = база таблицы указателей (arg2 fn529fe0) + tbl = rd_q(regs.rsp + 0x138) if regs.rsp else 0 + rec_t = {'kind': 'TRACKSAVE', 'tid': pid, 'band': regs.r12, + 'track_ptr': regs.rax, 'tbl': tbl, + 't': round(time.time()-t_start, 4)} + if len(track_dumps) < 48: + try: + rec_t['tbl_entries'] = [rd_q(tbl+8*i) for i in range(16)] + rec_t['trk_curve'] = rd_f32(regs.rax, 2049*2) + except OSError as e: + rec_t['err'] = str(e) + track_dumps.append(rec_t) + samples.append(rec_t) + hits['TRACKSAVE'] += 1 + poke(pid, site, (peek(pid, site) & ~0xFF) | obyte) + regs.rip = site + setregs(pid, regs) + pt(PTRACE_SINGLESTEP, pid, 0, 0) + os.waitpid(pid, __WALL) + poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC) + pt(PTRACE_CONT, pid, 0, 0) + continue + if kind == 'FN': + ra = rd_q(regs.rsp) + rec_f = {'kind':'FN','tid':pid, + 'rcx':regs.rcx,'rdx':regs.rdx,'r8':regs.r8,'r9':regs.r9, + 'ret':ra,'t':round(time.time()-t_start,4)} + samples.append(rec_f); hits['FN'] += 1 + if hits['FN'] <= 3: + print('FN: rcx=%#x rdx=%#x r8=%#x r9=%#x ret=%#x'%( + regs.rcx,regs.rdx,regs.r8,regs.r9,ra), flush=True) + poke(pid, site, (peek(pid, site) & ~0xFF) | obyte) + regs.rip = site + setregs(pid, regs) + pt(PTRACE_SINGLESTEP, pid, 0, 0) + os.waitpid(pid, __WALL) + poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC) + pt(PTRACE_CONT, pid, 0, 0) + continue + if kind in ('DIV','DC40','EXPVAR'): + rec_a = {'kind': kind, 'tid': pid, + 't': round(time.time()-t_start, 4), + 'rcx': regs.rcx, 'rdx': regs.rdx, + 'r8': regs.r8, 'r9': regs.r9} + try: + for nm, p, cnt in (('a', regs.rcx, 2050), + ('b', regs.rdx, 2050), + ('c', regs.r8, 2050)): + if p > 0x10000: + rec_a[nm] = rd_f32(p, cnt) + except OSError as e: + rec_a['err'] = str(e) + samples.append(rec_a) + hits[kind] += 1 + poke(pid, site, (peek(pid, site) & ~0xFF) | obyte) + regs.rip = site + setregs(pid, regs) + pt(PTRACE_SINGLESTEP, pid, 0, 0) + os.waitpid(pid, __WALL) + poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC) + pt(PTRACE_CONT, pid, 0, 0) + continue if kind == 'DF0': track_by_tid[pid] = regs.rdx if kind == 'DF0RET': @@ -387,6 +478,46 @@ def main(): flush=True) if good: ctx = good + if kind == 'FN': + ra = rd_q(regs.rsp) + rec_f = {'kind':'FN','tid':pid, + 'rcx':regs.rcx,'rdx':regs.rdx,'r8':regs.r8,'r9':regs.r9, + 'ret':ra,'t':round(time.time()-t_start,4)} + samples.append(rec_f); hits['FN'] += 1 + if hits['FN'] <= 3: + print('FN: rcx=%#x rdx=%#x r8=%#x r9=%#x ret=%#x'%( + regs.rcx,regs.rdx,regs.r8,regs.r9,ra), flush=True) + poke(pid, site, (peek(pid, site) & ~0xFF) | obyte) + regs.rip = site + setregs(pid, regs) + pt(PTRACE_SINGLESTEP, pid, 0, 0) + os.waitpid(pid, __WALL) + poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC) + pt(PTRACE_CONT, pid, 0, 0) + continue + if kind in ('DIV','DC40','EXPVAR'): + rec_a = {'kind': kind, 'tid': pid, + 't': round(time.time()-t_start, 4), + 'rcx': regs.rcx, 'rdx': regs.rdx, + 'r8': regs.r8, 'r9': regs.r9} + try: + for nm, p, cnt in (('a', regs.rcx, 2050), + ('b', regs.rdx, 2050), + ('c', regs.r8, 2050)): + if p > 0x10000: + rec_a[nm] = rd_f32(p, cnt) + except OSError as e: + rec_a['err'] = str(e) + samples.append(rec_a) + hits[kind] += 1 + poke(pid, site, (peek(pid, site) & ~0xFF) | obyte) + regs.rip = site + setregs(pid, regs) + pt(PTRACE_SINGLESTEP, pid, 0, 0) + os.waitpid(pid, __WALL) + poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC) + pt(PTRACE_CONT, pid, 0, 0) + continue if kind == 'DF0': track_by_tid[pid] = regs.rdx rec = {'kind': kind, 'tid': pid, diff --git a/scripts/wine_stage_trace.py b/scripts/wine_stage_trace.py new file mode 100644 index 0000000..9f06bd1 --- /dev/null +++ b/scripts/wine_stage_trace.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +"""wine_stage_trace.py — трассировка СТАДИЙ пайплайна через vtable ctx. + +На входе fn529fe0: читает vtable=[ctx], ставит int3 на таргеты слотов +{8,0x18,0x20,0x28,0x30,0x48,0xe8,0x218,0x220,0x228,0x230}, снапшотит +track-буферы (таблица @arg2, count=r9). На каждом хите стадии: md5 +track-буферов + аргументы. Разница md5 между стадиями = кто пишет track. +""" +import ctypes +import hashlib +import os +import pickle +import signal +import struct +import subprocess +import sys +import time + +import numpy as np + +from wine_ptrace_trace import ( # noqa + pt, getregs, setregs, peek, poke, find_host, find_ctx, + PTRACE_ATTACH, PTRACE_DETACH, PTRACE_CONT, PTRACE_SINGLESTEP, + PTRACE_SETOPTIONS, PTRACE_O_TRACECLONE, __WALL) + +BP_FN = 0x180529fe0 +SLOTS = [0x8, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38, 0x40, 0x48, + 0xe8, 0x218, 0x220, 0x228, 0x230] + + +def main(): + rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/comb_b1234.rpp' + n_frames = int(sys.argv[2]) if len(sys.argv) > 2 else 6 + outdir = sys.argv[3] if len(sys.argv) > 3 else '/tmp/opencode/winetrace_casc' + os.makedirs(outdir, exist_ok=True) + + wav = None + for ln in open(rpp, errors='replace'): + if 'RENDER_FILE' in ln and '"' in ln: + wav = ln.split('"')[1] + break + + 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) + t0 = time.time() + host = None + while time.time() - t0 < 25: + host = find_host() + if host: + break + time.sleep(0.001) + if not host: + print('NO HOST') + return 1 + fd = os.open(f'/proc/{host}/mem', os.O_RDONLY) + ctx = None + while ctx is None and time.time() - t0 < 25: + try: + ctx = find_ctx(fd, host) + except (ProcessLookupError, OSError): + return 1 + if not ctx: + time.sleep(0.002) + print('host %d ctx %#x (+%.2fs)' % (host, ctx, time.time()-t0), flush=True) + + def rd(a, n): + return os.pread(fd, n, a) + + def rd_f32(a, n): + return np.frombuffer(rd(a, 4*n), dtype=' 0x10000: + out[i] = hashlib.md5(rd(p, 4098*4)).hexdigest() + return out + + try: + while frames_done < n_frames and time.time() - t_start < 240: + try: + pid, status = os.waitpid(-1, __WALL | os.WNOHANG) + except ChildProcessError: + break + if (pid, status) == (0, 0): + time.sleep(0.0005) + continue + if not os.WIFSTOPPED(status): + if pid in attached: + attached.remove(pid) + continue + if os.WSTOPSIG(status) != signal.SIGTRAP: + pt(PTRACE_CONT, pid, 0, sig if False else 0) + continue + try: + regs = getregs(pid) + except OSError: + continue + site = regs.rip - 1 + info = bps.get(site) + if info is None: + pt(PTRACE_CONT, pid, 0, 0) + continue + kind, obyte = info + + def restore_and_go(): + poke(pid, site, (peek(pid, site) & ~0xFF) | obyte) + regs.rip = site + setregs(pid, regs) + pt(PTRACE_SINGLESTEP, pid, 0, 0) + os.waitpid(pid, __WALL) + poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC) + pt(PTRACE_CONT, pid, 0, 0) + + if kind == 'FN': + table = regs.rdx + nb = regs.r9 & 0xFFFFFFFF + cur_frame = {'t': round(time.time()-t_start, 4), + 'ctx': regs.rcx, 'table': table, 'nbands': nb, + 'md5_before': track_snapshot(table, nb), + 'stages': []} + rec = dict(kind='FN', **{k: v for k, v in cur_frame.items() + if k != 'md5_before'}) + samples.append(rec) + else: + if cur_frame is not None: + ent = {'stage': kind, 'site': hex(site), + 'rcx': regs.rcx, 'rdx': regs.rdx, + 'r8': regs.r8, 'r9': regs.r9, + 'md5_after': track_snapshot(cur_frame['table'], + cur_frame['nbands'])} + cur_frame['stages'].append(ent) + if kind.startswith('vt') and frames_done < 2: + args = {} + for nm, p in (('rcx', regs.rcx), ('rdx', regs.rdx), + ('r8', regs.r8)): + if p > 0x10000: + try: + args[nm] = rd_f32(p, 2050)[:64].tolist() + except OSError: + pass + samples.append({'kind': 'ARG:' + kind, 'site': hex(site), + 'args64': str(args)[:400]}) + if kind == 'vt+0x30': + # fn529fe0 завершился: финальный md5 + if cur_frame is not None: + cur_frame['md5_after_fn'] = track_snapshot( + cur_frame['table'], cur_frame['nbands']) + frames_done += 1 + samples.append({'kind': 'FRAME_END', + 'frame': cur_frame}) + cur_frame = None + + restore_and_go() + finally: + for addr, (nm, obyte) in bps.items(): + try: + poke(host, addr, (peek(host, addr) & ~0xFF) | obyte) + except OSError: + pass + for tid in list(attached): + try: + pt(PTRACE_DETACH, tid, 0, 0) + except OSError: + pass + + with open(os.path.join(outdir, 'stage_samples.pkl'), 'wb') as f: + pickle.dump(samples, f) + fr = [s for s in samples if s['kind'] == 'FRAME_END'] + print('кадров собрано:', len(fr), flush=True) + for f_ in fr[:3]: + fr_ = f_['frame'] + print('--- кадр t=%.2f bands=%d' % (fr_['t'], fr_['nbands'])) + prev = fr_['md5_before'] + for st in fr_['stages']: + ch = '' if st['md5_after'] == prev else ' <<< TRACK ИЗМЕНИЛСЯ' + print(' %-8s rcx=%#x rdx=%#x%s' % (st['stage'], st['rcx'], + st['rdx'], ch)) + prev = st['md5_after'] + print(' после fn:', fr_.get('md5_after_fn')) + print('reaper_rc=%s' % proc.poll(), flush=True) + return 0 + + +if __name__ == '__main__': + sys.exit(main())