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
+74
View File
@@ -153,3 +153,77 @@ for i in 1..n-1:
Незакоммичено: f529fe0_full.dis, BLOCKMAP_529fe0.md, thunk-резолвер,
правки NOTES_LEVEL (22v будет добавлена при транскрипции). Следующий шаг —
транскрипция process_frame_faithful по этой карте.
## ДОПОЛНЕНИЕ 23b: резолв ILT + декод FIR-цикла (52b55052b8bb)
### Резолвер
`scripts/ilt_resolve.py`: стаб = `mov eax,[rip+idx]; lea r10,[rip+tbl]; jmp [r10+rax*8]`;
live idx=4 у всех; таблицы @0x1826xxxx. Полная таблица «стаб→impl»:
| стаб | impl | роль (уточн.) |
|---|---|---|
| 0x180002210 | 0x136e0 | copy scratch→FIR? (валидатор → 39de0/3a040/3a220) |
| 0x180002180 | 0x125e0 | complex-op A/C float (воркер 3a4a0; r9 НЕ передаётся ⇒ длина из объекта) |
| 0x180001bb0 | 0x6a40 | complex-op A/C double |
| 0x180001a90 | 0x5560 | complex-op B/D float — ЧИСТЫЙ FMA (fma=28, mul/add=0!) |
| 0x1800019d0 | 0x4340 | complex-op B/D double |
| 0x180001880 | 0x2c60 | paired-scalar op 1 (флаг-ветка) |
| 0x180001ca0 | 0x9380 | paired-scalar op 2 |
| 0x180001df0 | 0xb3c0 | final op float |
| 0x180001f70 | 0xe360 | final op double |
| 0x180140a10 | 0x141400→[1826181d8]=**0x1802a24c0** | дизайн float |
| 0x180140a70 | 0x141580→[1826183c8]=**0x1802fa420** | дизайн double |
| 2030/1d30 | ffe0 / 9be0 | scalar-transform |
| 2270/22a0 | 14c40 / 15060 | scalar-transform-2 |
| 2000/1c40 | fb60 / 8700 | axpy-класс |
| остальное | dc40,8d60,5a20,4720,4200,3f40,5160,25e0,ee20,3c40 | как в BLOCKMAP ✓ |
### Дизайн-тело 0x1802a24c0 (float, вход=rcx bands[i], выход=rdx scratch@0x540628)
Функция входа 0..0x675 (~1.6КБ), далее сиблинги. Алгоритм = **ВЕКТОРНЫЙ LOG2**
(range reduction бит-трюком с 0.6666667, полином Хорнера на YMM-константах
0.5, 0.3333656, 0.2500466, 0.198225, 0.1646246, 0.1696488, 0.1517721,
реконструкция ×ln2=0.6931472; константы @FN+0x1cdfac0..d20). Маскированный
хвост через vmaskmovps/popcnt-таблицы (@0x181F8xxxx). ИТОГ: **scratch = log(bands[i])**.
Диспетчер 535a70 делает swap аргументов (rcx↔rdx) перед прыжком!
### Структура FIR-цикла (полностью залочена)
На полосу: 2×scalar-transform(bands[i], xmm8·[540888] при флаге 540890==0, иначе
xmm8; затем xmm12xmm8) [ср. decomp 184-185: A=[0x540874]expf(K), wet=[0x540888]]
→ DESIGN=log(bands[i])→scratch → copy(scratch→FIR@0x540668) [2210]
→ opA(FIR,buf548,buf598) [2180/1bb0] → FIR[n]=0 (n=0x540534=4096!)
→ 52d920(&FIR[1], xmm13, n/21) деление; 52db50(&FIR[2049], xmm9, n/21)
→ opB [1a90/19d0] → bigkernel in-place 140b30/140aa0 (n/2+1)
→ opC [2180/1bb0] → FIR[n]=0 → 52d990(FIR, WIN_freq+n/2, n/2) УМНОЖЕНИЕ на
**падающую половину Hann** (WIN_freq=wperiodicHann4096: w[1024]=0.5,w[2048]=1.0!)
→ 52db50(&FIR[2048], xmm9, n/2) → opD [1a90/19d0] → FIR[0]=1.0, FIR[1]=0
→ флаг-ветка: парно-скалярные 1880/ca0 → dry/wet scale ×[0x540888] над **2n флоатов**
→ final(df0/f70) с сохранённым track_i ([rsp+0x138][i]).
### Живые буферы (firtrace.py по s1/s2 + свежие захваты dual_b1q_0.5)
- 0x540658 WIN_freq = периодический Hann(4096), пик 1.0 @bin2048 (НЕ рамп 0.5→0.8!)
- 0x540758 freqaxis = линейный, шаг ≈11.713 Гц (≈48000/4096=11.71875, лёгкое
занижение — похоже на накопительную ошибку f32 при построении суммой)
- 0x540598 = SIMD-маски: 8×1.0 / 8×0.0 периодом 16 флоатов, ровно 1024 шт
(YMM lane-select для комплексных кернелов)
- 0x540550 NULL (float-путь активен, флаг 0x5408b8=0); 0x540628 scratch нулевой
между колбеками; 0x540668 FIR = комплексная единица (1,0)×2049 бинов МЕЖДУ
колбеками — кернел потребляется и сбрасывается внутри колбека
- 0x5406f8 хранит аудио-масштабный сигнал (~±0.1) между колбеками — overlap/STFT
буфер conv-движка (не «нулевой acc»!)
- 0x540788 vs 0x5407f8 различаются (max 1.108; @171: 1.751 vs 1.409) — две РАЗНЫЕ
кривые; ни одна не достигает нужных 3.89×@171 (11.82 дБ) ⇒ применённый фильтр
≠ поточечная копия любой из живых кривых (подтверждение разрыва 22y)
### Динамика (попытки перехвата; окружение)
- realtime-playback НЕ тикает DSP (нет аудио-девайса; треды спят в futex);
кривые 22y = результат инициализации при загрузке проекта
- `-renderproject`: полный цикл init+render+exit хоста занимает ~0.8 c
(host@1.2s, workers@1.4s, wav@1.6s, exit@2.0s); обработка идёт в окне ~0.5 c
- INT3-ptrace: SEIZE+TRACECLONE обязательны до CONT (иначе untraced thread
ловит SIGTRAP и убивает процесс — подтверждено); DR-брейкпоинты: DR0@user+0x380,
но POKEUSER DR7 даёт EIO. Скрипты: fntrace*.py, fnhw.py, fnall.py, firstop.py,
firtrace.py, hotips.py (см. scripts/)
- Вывод: FUN_180529fe0 и 52d650/536300/52e260 НЕ ловятся в рендер-окне —
маск-цепь выполняется при ЗАГРУЗКЕ/изменении параметров, стационарный рендер
использует закэшированный кернел; либо трассировать надо момент инициализации
- Выход плагина НЕдетерминирован: md5 двух свободных рендеров различен при
одинаковом rms (PRNG-дизеринг из LCG-прологов) — метрика только спектральная!
+47
View File
@@ -2224,3 +2224,50 @@ dual НЕ в пост-обработке кривой R.
0x1802a24c0 (3КБ AVX2) и complex-op шагов с буферами 0x540548/550/598 — следующий
статический шаг; альтернатива — трассировка промежуточных буферов живьём
(снапшоты в момент аудио-колбека, не между кадрами).
## ============ UPDATE 2026-08-23g (23b): ДИЗАЙН=LOG(band), Hann-окно, PRNG-дизеринг ============
### Что доказано декодом (детали в BLOCKMAP 23b)
1. Полная цепь построения FIR на полосу залочена по raw asm (52b55052b8bb):
scalar-shape ×2 → **DESIGN: scratch=log(bands[i])** (векторный log2, полином
идентифицирован по константам, тело 0x1802a24c0) → copy→FIR@0x540668
4 комплекс-op (A..D) на буферах 0x540548(SIMD/twiddle-workspace)/0x540598
(YMM-маски 8×1/8×0×64) вокруг нормировки &FIR[1]÷K и bigkernel →
**FIR[2048..4095] ×= Hann_freq[2048..4095]** (падающая половина периодического
Hann(4096), пик ровно на стыке!) → FIR[0]=1,FIR[1]=0 → dry/wet над 2n флоатами.
Структура log→ops→exp-класс + окно = классический frequency-sampling /
минимально-фазовый дизайн кернела. Гипотеза механизма скайрта: геометрия
задаётся ОКНОМ и комплекс-ops на log-спектре, а не постобработкой R.
2. WIN_freq опознан численно (w[1024]=0.5,w[2048]=1.0) — прежняя трактовка
«0.5→0.8» относилась к другому слоту (0x540698).
3. freqaxis: шаг 11.71303 vs точные 11.71875 — накопительная f32-ошибка построения;
сетка внутренняя 48k/4096 подтверждена независимо.
4. Выход плагина недетерминирован от рендера к рендеру (md5 разные, rms равные) —
LCG-прологи реально дизерят; честная метрика только спектральная.
### Динамика: почему живые кривые «статичны»
Realtime-playback без аудио-девайса НЕ вызывает processing (все треды спят);
кривые 22y — продукт инициализации проекта. `-renderproject` прожимает весь
цикл за ~0.8 c (init+render+exit); перехват требует SEIZE+TRACECLONE ДО первого
CONT главного треда. FUN_180529fe0/52d650/536300/52e260 не ловятся в рендер-окне
⇒ маск-цепь выполняется при загрузке/param-change, рендер использует кэш.
Следующий динамический шаг (если понадобится): bp на входе process-callback vtbl
или трасса окна init (поймать DESIGN в момент param-change).
### Инструментарий этого раунда (закommичено)
scripts/ilt_resolve.py (резолв стабов офлайн), scripts/firtrace.py (полный каталог
буферов + GUI-флаг 0x2404bc), scripts/firstop.py (SIGSTOP-семплирование),
fntrace*.py/fnhw.py/fnall.py (ptrace-лаборатория: INT3/DR, уроки — см. BLOCKMAP),
hotips.py. .dis тела: conv_float_a24c0, conv_double_fa420, imp_*, th_* (19 impl).
### Контрольные числа dual (цель раунда, неизменны)
cut@500=10.32 дБ const ∀q; @2000=11.82 @q=0.5; Δcut≈2.25·(ln res).
Модель сейчас: −0.18/@500-ок + провал скайрта.
### NEXT (пересмотр после декода)
1. numpy-реплика цепи log→opsA-D→window с КАЛИБРОВКОЙ ops по измеренным H(f)
(корпус референсов уже есть); критерий — воспроизведение Δcut(res) геометрии.
2. Если ops A-D ≈ exp-раундтрип со сглаживанием: транскрипция за RT_FIRCONV,
гейт corpus --vs-bridge + контрольные числа dual.
3. ptrace-эскалация ТОЛЬКО если численная репликация не сойдётся: цель —
брейкпоинт в момент param-change (init-окно хоста), а не рендер.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
; imp_141400: VA 0x180141400-0x180141420 (32 bytes)
/tmp/opencode/dis/imp_141400.bin: file format binary
Disassembly of section .data:
0000000000000000 <.data>:
0: 48 8b 05 d1 6d 4d 02 mov 0x24d6dd1(%rip),%rax # 0x24d6dd8
7: ff e0 jmp *%rax
9: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
10: 00
11: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
18: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
1f: 00
+15
View File
@@ -0,0 +1,15 @@
; imp_141580: VA 0x180141580-0x1801415a0 (32 bytes)
/tmp/opencode/dis/imp_141580.bin: file format binary
Disassembly of section .data:
0000000000000000 <.data>:
0: 48 8b 05 41 6e 4d 02 mov 0x24d6e41(%rip),%rax # 0x24d6e48
7: ff e0 jmp *%rax
9: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
10: 00
11: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
18: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
1f: 00
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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()