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:
@@ -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-цикла (52b550–52b8bb)
|
||||
|
||||
### Резолвер
|
||||
`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; затем xmm12−xmm8) [ср. 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/2−1) деление; 52db50(&FIR[2049], xmm9, n/2−1)
|
||||
→ 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-прологов) — метрика только спектральная!
|
||||
|
||||
@@ -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 (52b550–52b8bb):
|
||||
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
@@ -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
|
||||
@@ -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
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user