runtime capture pt2: vst3 loads AT ImageBase 0x180000000; 0x540658 = field [ctx+0x540658] not RVA; beacon->ctx script (racy)

This commit is contained in:
2026-08-18 21:11:29 +03:00
parent 41fc402b05
commit 1a57c48318
4 changed files with 398 additions and 62 deletions
+28
View File
@@ -349,3 +349,31 @@ getFunctionContaining(0x52ac64) and has the complete per-band loop + FFT-conv).
## опять +1.4 dB). ## опять +1.4 dB).
## => мультибанд: сумма displacements -> sat-кривая. Каждая полоса отдельно калибруется single- ## => мультибанд: сумма displacements -> sat-кривая. Каждая полоса отдельно калибруется single-
## band рендерами; комбинация закрывается runtime или sat-подгонкой на b1on12_b2on12. ## band рендерами; комбинация закрывается runtime или sat-подгонкой на b1on12_b2on12.
## ============ 2026-08-18h2: RUNTIME CAPTURE PARTIAL (base 0x180000000, 0x540658 = FIELD NOT RVA) ============
### Runtime capture infra (rtcapture2.py, WORKS):
- Launch: `reaper -nosplash -renderproject X.rpp`; host pid found by scanning /proc/*/maps for 'soothe2'
(lowest map addr with soothe2 in path = PE base). **vst3 loads AT ImageBase 0x180000000** (no reloc).
- Reading /proc/pid/mem (+0x180000000) works DURING render; process dies right after render finishes.
- Section table (Authoritative): .text rva 0x1000 raw 0x600 vsize 0x1a52000; IPPCODE 0x1a53000/0x1a51e00;
.rdata 0x1baa000/0x1ba8400 vsize 0xa60000; .data 0x260a000/0x2608200 vsize 0x72000; .pdata 0x267c000.
- FILE->RVA mapping: raw off F maps to rva = F - raw_sec + va_sec (e.g. .text: F=0x53F658<->rva=0x541058).
### CRITICAL: 0x540658 (and 0x540678/0x5407c8/0x5408b0/etc) are FIELD OFFSETS, NOT RVAs!
- Disasm at rva 0x52b771: `mov rax,[rdi+0x540658]` -> window read is [ctx+0x540658] (ctx = DSP object).
- Static soothe_mem.bin is RVA-linear; reading at 0x540658 gives .text CODE bytes (garbage floats).
- So the "window" (FIR*=WINDOW in FFT-conv step 5) lives in the HEAP DSP object at offset 0x540658,
NOT in module image. Runtime must locate `ctx` = base of DSP object.
- Beacon idea: phase_table_1024 (twiddle doubles 0x182615608 in .data) is the FFT-plan table;
scan host heap for u64==0x182615608, then the pointer owner is plan (+0x18/+0x20/+0x68 fields)
-> ctx = byte_addr_of_(pointer_field) - field_offset; window at ctx+0x540658.
(rtdeep2.py implements this; currently racy - host may exit before numpy import + first scan.)
### Notes for next session:
- Render lifetime is short (~2-19s); to win the race: import numpy BEFORE Popen, scan /proc instantly,
and re-run if host missed. Could also pre-load module into gdb for direct ctx inspection.
- Alternative: find ctx via 'consumers_out' alloc chain (note :119-135) if heap layout known.
- Verified render outputs: out_dual300.wav etc; run_sweep works; pkill -9 -x reaper hangs shell -
use `pkill -9 -f "reap[e]r"` style to avoid killing own bash.
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""rtcapture.py — runtime-захват soothe2 из yabridge-host во время offline-рендера.
Запускает reaper -renderproject <rpp>, находит yabridge-host.exe.so с soothe2_x64.vst3,
вычисляет runtime-базу модуля (по MZ/PE) и непрерывно сэмплирует целевую RAM плагина
(RVA = VA - 0x180000000). Печатает последние стабильные значения по окончании рендера.
Цели (RVA от базы модуля):
0x540658 окно/FIR (размер окна из plan) [искомое 0.540658]
0x540678 per-bin уровень-трекеры
0x5407c8 аккумулятор маски (C)
0x540698 freq-axis значение
0x540534 N/2 (FFT size)
0x5408b0 узлы кривой 0x188 (20 doubles)
0x540888 0x540880 ctor-константы
0x540530..0x5405a0 план-таблицы FFT
"""
import os, sys, glob, time, struct, subprocess
RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/render_long.rpp'
BASE = 0x180000000
TARGETS = {
'window_0x540658': (0x540658, 0x1000),
'levels_0x540678': (0x540678, 0x400),
'acc_0x5407c8': (0x5407c8, 0x40),
'freqaxis_0x540698': (0x540698, 0x20),
'n2_0x540534': (0x540534, 0x20),
'curve_0x5408b0': (0x5408b0, 0x200),
'ctor_0x540870': (0x540870, 0x60),
'plan_0x540530': (0x540530, 0x80),
}
def find_base(pid):
maps = open(f'/proc/{pid}/maps').read()
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
seen = set()
for line in maps.splitlines():
p = line.split()
lo = int(p[0].split('-')[0], 16)
if lo in seen:
continue
seen.add(lo)
try:
hdr = os.pread(mem, 0x1000, lo)
except Exception:
continue
if hdr[:2] != b'MZ':
continue
try:
x = struct.unpack('<I', hdr[0x3c:0x40])[0]
if hdr[x:x + 4] != b'PE\x00\x00':
continue
magic = struct.unpack('<H', hdr[x + 24:x + 26])[0]
if magic == 0x20b:
ib = struct.unpack('<Q', hdr[x + 24 + 24:x + 24 + 32])[0] # PE32+ ImageBase
elif magic == 0x10b:
ib = struct.unpack('<I', hdr[x + 24 + 28:x + 24 + 32])[0]
else:
continue
return lo, ib
except Exception:
continue
os.close(mem)
return None, 0
def read(pid, addr, n):
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
try:
return os.pread(mem, n, addr)
except Exception:
return None
finally:
os.close(mem)
def main():
proc = subprocess.Popen(['reaper', '-nosplash', '-renderproject', RPP],
stdout=open('/tmp/rtcap.log', 'w'), stderr=subprocess.STDOUT)
host = None
t0 = time.time()
while time.time() - t0 < 120:
for pid in glob.glob('/proc/[0-9]*'):
pid = int(os.path.basename(pid))
try:
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().decode('utf8', 'replace')
maps = open(f'/proc/{pid}/maps').read()
except Exception:
continue
if 'yabridge-host' in cmd and ('soothe2' in maps or 'vst3' in maps):
host = pid
break
if host:
break
time.sleep(0.2)
if not host:
print('HOST NOT FOUND')
proc.kill()
return
base, ib = find_base(host)
print(f'host pid={host} module_base={hex(base)} PE_ImageBase={hex(ib)} delta={hex(base - ib)}', flush=True)
if not base:
proc.kill()
return
# sample until reaper exits
last = {}
ts = time.time()
while proc.poll() is None and time.time() - ts < 150:
for name, (rva, n) in TARGETS.items():
d = read(host, base + rva, n)
if d is not None:
last[name] = d
time.sleep(0.05)
proc.wait(timeout=10)
print('--- captured (render done) ---')
for name, (rva, n) in TARGETS.items():
d = last.get(name)
if not d:
print(f'{name:20s} MISSING')
continue
vals = struct.unpack('<%dI' % (min(n, len(d)) // 4), d[:min(n, len(d))])
if name == 'window_0x540658':
fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4])
nz = [(i, round(v, 6)) for i, v in enumerate(fl) if abs(v) > 1e-9]
print(f'{name:20s} nonzero={len(nz)} first20={nz[:20]}')
elif name == 'acc_0x5407c8':
print(f'{name:20s} ints={[hex(v) for v in vals[:8]]}')
elif name == 'curve_0x5408b0':
dl = struct.unpack('<%dd' % (len(d) // 8), d[:len(d) // 8 * 8])
print(f'{name:20s} doubles={[round(v, 6) for v in dl[:20]]}')
elif name == 'levels_0x540678':
fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4])
print(f'{name:20s} floats={[round(v, 5) for v in fl[:40]]}')
elif name in ('ctor_0x540870',):
fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4])
print(f'{name:20s} floats={[round(v, 5) for v in fl[:16]]}')
elif name in ('plan_0x540530',):
print(f'{name:20s} ints={[hex(v) for v in vals[:24]]}')
else:
print(f'{name:20s} ints={[hex(v) for v in vals[:8]]}')
print('done')
if __name__ == '__main__':
main()
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""rtcapture2.py — robust runtime capture for a LONG render (host lives minutes).
Dumps sections + raw target buffers at module_base+rva (PE ImageBase=0x180000000).
"""
import subprocess, time, glob, os, struct, sys
RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual300.rpp'
OUT = sys.argv[2] if len(sys.argv) > 2 else '/tmp/rtcap2.log'
RVAS = [0x541134, 0x541258, 0x541278, 0x5413c8, 0x541298, 0x541470, 0x541480, 0x541488, 0x5414b0]
def find_pe(pid):
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
seen = set()
for line in open(f'/proc/{pid}/maps').read().splitlines():
lo = int(line.split()[0].split('-')[0], 16)
if lo in seen:
continue
seen.add(lo)
try:
hdr = os.pread(mem, 0x1000, lo)
except Exception:
continue
if hdr[:2] != b'MZ':
continue
try:
x = struct.unpack('<I', hdr[0x3c:0x40])[0]
if hdr[x:x + 4] != b'PE\x00\x00':
continue
magic = struct.unpack('<H', hdr[x + 24:x + 26])[0]
ib = struct.unpack('<Q', hdr[x + 48:x + 56])[0] if magic == 0x20b else struct.unpack('<I', hdr[x + 52:x + 56])[0]
return lo, ib
except Exception:
continue
os.close(mem)
return None, 0
def sections(pid, base):
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
hdr = os.pread(mem, 0x1000, base)
x = struct.unpack('<I', hdr[0x3c:0x40])[0]
nsec = struct.unpack('<H', hdr[x + 6:x + 8])[0]
opt = struct.unpack('<H', hdr[x + 20:x + 22])[0]
out = []
for i in range(nsec):
sh = os.pread(mem, 40, base + x + 24 + opt + i * 40)
nm = sh[:8].rstrip(b'\0').decode('utf8', 'replace')
vsize, va, rsize, roc = struct.unpack('<IIII', sh[8:24])
ch = struct.unpack('<I', sh[36:40])[0]
out.append((nm, va, vsize, ch))
os.close(mem)
return out
def vst_base(pid):
lo = None
for line in open(f'/proc/{pid}/maps').read().splitlines():
if 'soothe2' not in line:
continue
a = int(line.split()[0].split('-')[0], 16)
lo = a if lo is None else min(lo, a)
if lo is None:
return None
try:
mem0 = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
hdr = os.pread(mem0, 0x1000, lo)
os.close(mem0)
except Exception:
return None
return lo if hdr[:2] == b'MZ' else None
def main():
out = open(OUT, 'w')
proc = subprocess.Popen(['reaper', '-nosplash', '-renderproject', RPP], stdout=out, stderr=subprocess.STDOUT)
t0 = time.time()
base = None
host = None
while time.time() - t0 < 90:
for pid in glob.glob('/proc/[0-9]*'):
pid = int(os.path.basename(pid))
try:
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().decode('utf8', 'replace')
maps = open(f'/proc/{pid}/maps').read()
except Exception:
continue
if 'yabridge-host' not in cmd and 'soothe2' not in maps:
continue
b = vst_base(pid)
if b:
host, base = pid, b
print(f'pid={pid} vst_base={hex(b)}', flush=True)
break
if base:
break
time.sleep(0.05)
if not base:
print('NO PE FOUND', flush=True)
proc.kill()
return
for nm, va, vs, ch in sections(host, base):
print(f'sec {nm:8s} rva={hex(va):>9} vsize={hex(vs):>9} chars={hex(ch)}', flush=True)
mem = os.open(f'/proc/{host}/mem', os.O_RDONLY)
t3 = time.time()
last = 0
while proc.poll() is None and time.time() - t3 < 60:
try:
d = os.pread(mem, 0x20, base + 0x541258)
win = struct.unpack('<8f', d[:32]) if len(d) >= 32 else None
except Exception:
win = None
now = time.time() - t3
if now - last >= 2.0 or win is None:
print(f'sample t={now:.1f}s win[0:8]={[round(v,5) for v in win] if win else None}', flush=True)
for rva in RVAS:
d = os.pread(mem, 128, base + rva)
fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4]) if len(d) >= 4 else []
print(f' {hex(rva)} floats={[round(v,5) for v in fl[:4]]} raw={d[:24].hex()}', flush=True)
last = now
time.sleep(0.05)
os.close(mem)
print('render done', flush=True)
proc.wait(timeout=10)
print('done', flush=True)
if __name__ == '__main__':
main()
+95 -62
View File
@@ -1,76 +1,109 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import subprocess, os, glob, sys, time, struct """rtdeep2.py — find DSP ctx object via phase-table pointer beacon, then dump window 0x540658 & neighbors."""
import subprocess, time, glob, os, struct, sys
import numpy as np
RPP = "/home/m/soothe-bt/render_long.rpp" RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual300.rpp'
proc = subprocess.Popen(['reaper', '-nosplash', '-renderproject', RPP], BEACON = 0x182615608 # VA of phase_table_1024 (static .data)
stdout=open('/tmp/rtdeep2.log', 'w'), stderr=subprocess.STDOUT)
print('reaper', proc.pid, flush=True)
def find_hosts(): def vst_base(pid):
lo = None
for line in open(f'/proc/{pid}/maps').read().splitlines():
if 'soothe2' not in line:
continue
a = int(line.split()[0].split('-')[0], 16)
lo = a if lo is None else min(lo, a)
return lo
def readable_regions(pid):
out = [] out = []
for p in glob.glob('/proc/[0-9]*'): for line in open(f'/proc/{pid}/maps').read().splitlines():
try: p = line.split()[0]
pid = int(os.path.basename(p)) lo, hi = int(p.split('-')[0], 16), int(p.split('-')[1], 16)
m = open(f'/proc/{pid}/maps').read() if 'vst3' in line:
c = open(f'/proc/{pid}/cmdline', 'rb').read().decode('utf8', 'replace') continue # skip module itself
if 'soothe2' in m and 'yabridge-host.exe' in c: out.append((lo, hi))
out.append(pid)
except Exception:
pass
return out return out
def scan(pid): def find_beacon(pid):
# strategy: enumerate every readable chunk; build set of img addresses whose content is 0x18052xxxx try:
# then find heap words equal to any such address mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) except Exception:
img_sp = set() # vtbl-slot candidates: addrs in module image holding 0x18052xxxx return []
heap_addrs = [] # (va, chunk) heap chunks res = []
maps = open(f'/proc/{pid}/maps').read() for lo, hi in readable_regions(pid):
for line in maps.splitlines(): sz = (hi - lo) & ~7
p = line.split() if sz <= 0 or sz > 0x200000000:
lo, hi = (int(x, 16) for x in p[0].split('-'))
if 'r' not in p[1]:
continue continue
try: try:
d = os.pread(mem, hi - lo, lo) buf = os.pread(mem, sz, lo)
except Exception: except Exception:
continue continue
if lo < 0x180000000 < hi or (0x180000000 <= lo < 0x183000000): arr = np.frombuffer(buf, dtype='<u8')
for off in range(0, len(d) - 7, 8): idx = np.where(arr == BEACON)[0]
q = struct.unpack_from('<Q', d, off)[0] for i in idx:
if 0x180520000 <= q < 0x180555000: res.append(lo + int(i) * 8)
img_sp.add(lo + off)
else:
heap_addrs.append((lo, d))
os.close(mem) os.close(mem)
print('img SP-slot addrs found:', len(img_sp), flush=True) return res
# find heap objects pointing into img_sp
found = []
for lo, d in heap_addrs:
for s in img_sp:
t = struct.pack('<Q', s)
i = 0
while True:
i = d.find(t, i)
if i < 0:
break
found.append((s, lo + i))
i += 1
return img_sp, found
res = {}
t0 = time.time() def main():
while time.time() - t0 < 80: proc = subprocess.Popen(['reaper', '-nosplash', '-renderproject', RPP],
for pid in find_hosts(): stdout=open('/tmp/rtdeep2.log', 'w'), stderr=subprocess.STDOUT)
if pid in res: t0 = time.time()
continue host = None
img_sp, found = scan(pid) while time.time() - t0 < 60:
res[pid] = (img_sp, found) for p in glob.glob('/proc/[0-9]*'):
print('PID', pid, 'objects pointing to SP-method vtbl:', len(found), flush=True) try:
for s, a in found[:30]: maps = open(f'/proc/{p}/maps').read()
print(f' slotslot {s:#x} object@{a:#x}', flush=True) except Exception:
time.sleep(0.4) continue
proc.kill() if 'soothe2' not in maps:
print('done', flush=True) continue
host = int(os.path.basename(p))
break
if host:
break
time.sleep(0.05)
print(f'host={host}')
if not host:
proc.kill()
return
base = vst_base(host)
# allow module load: plugin init first
mem = os.open(f'/proc/{host}/mem', os.O_RDONLY)
t1 = time.time()
while time.time() - t1 < 50:
hits = find_beacon(host)
ctxs = set()
for p in hits:
for d in (0x540548, 0x540550, 0x540598):
ctxc = p - d
if 0x10000000 < ctxc < 0x7fff00000000:
ctxs.add(ctxc)
if ctxs:
print(f't={time.time()-t1:.1f}s hits={len(hits)} ctx_cands={sorted(hex(c) for c in ctxs)}', flush=True)
for ctx in sorted(ctxs):
d = os.pread(mem, 4096, ctx + 0x540658)
if len(d) < 4096:
continue
fl = np.frombuffer(d, dtype='<f4')
if np.all(np.isfinite(fl)) and fl.min() >= -1 and fl.max() <= 2:
print(f' ctx={hex(ctx)} WIN@+0x540658 head={[round(float(x),4) for x in fl[:8]]} mid={[round(float(x),4) for x in fl[1024:1032]]}', flush=True)
blk = os.pread(mem, 0x8000, ctx + 0x540000)
open(f'/tmp/ctx_{ctx:x}_win.bin', 'wb').write(blk)
break
time.sleep(0.2)
os.close(mem)
try:
proc.wait(timeout=5)
except Exception:
proc.kill()
print('done', flush=True)
if __name__ == '__main__':
main()