Files
viktor 5f4d207aa5 Мост к DMR-модулю, разбор спектра REFV DualTachyon, карта запчастей прошивки
Баузбенд FM100B:
- найден прозрачный мост ПК<->модуль (включение с зажатой МЕНЮ), проверен живьём
- расшифрована контрольная сумма кадров (one's complement, BE), сверена с ответом рации
- инструмент tools/fm100b.py (ping/send/raw/scan)
- cmd 0x25 = запрос версии, модуль отвечает V1.2.0.32 (совпало с офиц. образом)
- разобран диспетчер входящих кадров: 193 записи, реальный код у 10 команд
- cmd 0x59 = индикация приёма, она же управляет гейтом звука PA14
- исправлено: boot-handshake это cmd 0x84, а не 0x64 (research/re/dmr.md)

Спектроанализатор REFV DualTachyon (docs/refw-spectrum.md):
- вызывается как функция горячей клавиши №22 Analog Spectrum
- вход 0x08009CAA -> обычный 0x080139E4 / по зоне 0x08015318
- спектр это экран №11; тик 0x08013BA4 (автомат на 4 состояния), клавиши 0x08013BFC

Декомпозиция (docs/firmware-parts.md):
- два процессора + внешний SPI = три канала внедрения
- карта ресурсов SPI: шрифты, пиньинь, голос, таблица Unicode
- найден штатный загрузчик ресурсов FontVoicePicture (шрифты/голос/картинки)
- дерево меню целиком: MIC/SPK Gain, RX/TX Limit, SMS Format уже в стоке
- аудио двухступенчатое: PA2 питание УНЧ, PA14 гейт от DMR-модуля

Прочее:
- везде исправлен режим прошивки: тангента PTT вместо клавиши "*"
- устаревший Ru-4D_Flasher.exe удалён из репозитория
- добавлены инструменты реверса: xref, refs, gpiomap, gpioscan, schem
2026-07-22 21:17:20 +09:00

120 строки
3.7 KiB
Python

#!/usr/bin/env python3
"""RT-4D firmware xref/disasm helper.
python xref.py dis <vaddr> [n] disassemble n bytes at vaddr
python xref.py callers <vaddr> who bl/blx's this address
python xref.py lit <value> where this 32-bit literal appears (pool refs)
python xref.py imm <value> movw/movt pairs building this constant
python xref.py find <hexbytes> raw byte search
"""
import sys, struct, capstone
BASE = 0x08002800
import os
_CANDS = [
"/home/viktor/claude/rt-4d/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin",
"/home/viktor/claude/rt-4d-repo/firmware/rt4d_stock_v3.25_abs_0x08002800.bin",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "firmware",
"rt4d_stock_v3.25_abs_0x08002800.bin"),
]
IMG = next(open(p, "rb").read() for p in _CANDS if os.path.exists(p))
md = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)
md.detail = True
def _word(va):
o = va - BASE
if 0 <= o <= len(IMG) - 4:
return struct.unpack_from("<I", IMG, o)[0]
return None
def dis(va, n=0x60):
"""disassemble, annotating PC-relative literal loads with the resolved value"""
off = va - BASE
import re as _re
for i in md.disasm(IMG[off:off + n], va):
ann = ""
m = _re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", i.op_str)
if m and i.mnemonic.startswith("ldr"):
pcv = ((i.address + 4) & ~3) + int(m.group(1), 16)
w = _word(pcv)
if w is not None:
ann = f" ; @0x{pcv:08X} = 0x{w:08X}"
print(f" 0x{i.address:08X}: {i.bytes.hex():<10} {i.mnemonic:8} {i.op_str}{ann}")
def _all_calls():
"""Linear sweep collecting bl/blx targets -> {target: [sites]}."""
xr = {}
for start in (0, 2): # both alignments, dedupe by site
for i in md.disasm(IMG[start:], BASE + start):
if i.mnemonic in ("bl", "blx") and i.op_str.startswith("#"):
t = int(i.op_str[1:], 16)
xr.setdefault(t, set()).add(i.address)
return {k: sorted(v) for k, v in xr.items()}
def callers(va):
xr = _all_calls()
hits = xr.get(va, []) + xr.get(va | 1, []) + xr.get(va & ~1, [])
hits = sorted(set(hits))
print(f"callers of 0x{va:08X}: {len(hits)}")
for h in hits:
print(f" 0x{h:08X}")
return hits
def lit(val):
hits = []
for off in range(0, len(IMG) - 4, 4):
if struct.unpack_from("<I", IMG, off)[0] == val:
hits.append(BASE + off)
print(f"literal 0x{val:08X}: {len(hits)} hit(s)")
for h in hits[:60]:
print(f" 0x{h:08X}")
return hits
def imm(val):
"""find movw/movt pairs that build `val` (constant loaded into a reg)."""
lo, hi = val & 0xFFFF, (val >> 16) & 0xFFFF
out = []
for i in md.disasm(IMG, BASE):
if i.mnemonic == "movw" and i.op_str.endswith(f"#{hex(lo)}"):
out.append(i.address)
print(f"movw #{hex(lo)} (for 0x{val:08X}): {len(out)} site(s)")
for a in out[:60]:
print(f" 0x{a:08X}")
return out
def find(hexs):
pat = bytes.fromhex(hexs)
off = 0
hits = []
while True:
j = IMG.find(pat, off)
if j < 0:
break
hits.append(BASE + j); off = j + 1
print(f"bytes {hexs}: {len(hits)} hit(s)")
for h in hits[:60]:
print(f" 0x{h:08X}")
return hits
if __name__ == "__main__":
cmd = sys.argv[1]
if cmd == "dis":
dis(int(sys.argv[2], 16), int(sys.argv[3], 0) if len(sys.argv) > 3 else 0x60)
elif cmd == "callers":
callers(int(sys.argv[2], 16))
elif cmd == "lit":
lit(int(sys.argv[2], 16))
elif cmd == "imm":
imm(int(sys.argv[2], 16))
elif cmd == "find":
find(sys.argv[2])