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
+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()