Мост к 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
Этот коммит содержится в:
@@ -95,7 +95,7 @@ def main():
|
||||
print("[i] handshake (0xFF probe)...")
|
||||
if not handshake(p):
|
||||
raise SystemExit("[E] no bootloader (radio not in flash mode). "
|
||||
"Power off, hold * (or the flash-mode key) and power on.")
|
||||
"Power off, hold PTT and power on.")
|
||||
print("[i] bootloader ready")
|
||||
print("[i] erasing application region...")
|
||||
erase(p)
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""FM100B (DMR baseband) console — talk to the RT-4D's DMR chip from the PC.
|
||||
|
||||
Требуется: рация включена с зажатой клавишей МЕНЮ (режим «Обновление DMR чипа»),
|
||||
в котором прошивка работает прозрачным мостом USB ↔ FM100B (USART3).
|
||||
|
||||
Формат кадра: 68 | cmd | b | sub | CKh CKl | lenH lenL | payload… | 10
|
||||
CRC — «интернетная» one's-complement сумма 16-битных BE-слов по всему кадру,
|
||||
причём во время расчёта поле CRC = 0xFFFF; результат кладётся BE в [4..5].
|
||||
|
||||
python3 fm100b.py ping # штатный handshake (cmd 0x84)
|
||||
python3 fm100b.py send 84 --sub 1 --payload 00
|
||||
python3 fm100b.py raw 6884010100000001 0010
|
||||
python3 fm100b.py scan 01 20 # опрос диапазона команд (только чтение статуса)
|
||||
|
||||
Порт задаётся через --port (по умолчанию берётся первый CH343).
|
||||
Под WSL доступ к COM идёт через powershell.exe (WSL2 не пробрасывает COM напрямую).
|
||||
"""
|
||||
import argparse, subprocess, sys, time
|
||||
|
||||
DEFAULT_BAUD = 115200
|
||||
|
||||
|
||||
# ---------------- frame helpers ----------------
|
||||
def cksum(frame: bytes) -> int:
|
||||
"""one's complement sum of 16-bit big-endian words (CRC field must be 0xFFFF)"""
|
||||
s = 0
|
||||
i = 0
|
||||
while len(frame) - i > 1:
|
||||
s += (frame[i] << 8) | frame[i + 1]
|
||||
i += 2
|
||||
if len(frame) - i:
|
||||
s += frame[i] << 8
|
||||
while s >> 16:
|
||||
s = (s & 0xFFFF) + (s >> 16)
|
||||
return (~s) & 0xFFFF
|
||||
|
||||
|
||||
def build(cmd: int, b: int = 1, sub: int = 1, payload: bytes = b"") -> bytes:
|
||||
f = bytearray([0x68, cmd, b, sub, 0xFF, 0xFF,
|
||||
(len(payload) >> 8) & 0xFF, len(payload) & 0xFF])
|
||||
f += payload
|
||||
f.append(0x10)
|
||||
ck = cksum(bytes(f))
|
||||
f[4], f[5] = (ck >> 8) & 0xFF, ck & 0xFF
|
||||
return bytes(f)
|
||||
|
||||
|
||||
def parse(resp: bytes):
|
||||
"""decode a response frame -> dict (or None)"""
|
||||
if len(resp) < 9 or resp[0] != 0x68:
|
||||
return None
|
||||
cmd, b, status = resp[1], resp[2], resp[3]
|
||||
ck = (resp[4] << 8) | resp[5]
|
||||
ln = (resp[6] << 8) | resp[7]
|
||||
payload = resp[8:8 + ln]
|
||||
chk = bytearray(resp[:9 + ln])
|
||||
chk[4] = chk[5] = 0xFF
|
||||
ok = cksum(bytes(chk)) == ck
|
||||
return dict(cmd=cmd, b=b, status=status, length=ln,
|
||||
payload=bytes(payload), crc_ok=ok, raw=resp)
|
||||
|
||||
|
||||
# ---------------- serial via powershell (WSL2) ----------------
|
||||
PS = r'''
|
||||
$p = New-Object System.IO.Ports.SerialPort "{port}",{baud},"None",8,"one"
|
||||
$p.ReadTimeout = 120
|
||||
try {{ $p.Open() }} catch {{ Write-Output "ERR:$($_.Exception.Message)"; exit }}
|
||||
$p.DiscardInBuffer()
|
||||
$tx = [byte[]]@({txlist})
|
||||
if ($tx.Length -gt 0) {{ $p.Write($tx,0,$tx.Length) }}
|
||||
$buf = New-Object System.Collections.Generic.List[byte]
|
||||
$sw = [Diagnostics.Stopwatch]::StartNew()
|
||||
$last = 0
|
||||
while ($sw.ElapsedMilliseconds -lt {tmo}) {{
|
||||
try {{ $buf.Add($p.ReadByte()); $last = $sw.ElapsedMilliseconds }} catch {{}}
|
||||
if ($buf.Count -gt 0 -and ($sw.ElapsedMilliseconds - $last) -gt {idle}) {{ break }}
|
||||
}}
|
||||
$p.Close()
|
||||
Write-Output ("OK:" + (($buf | ForEach-Object {{ "{{0:x2}}" -f $_ }}) -join ""))
|
||||
'''
|
||||
|
||||
|
||||
def find_port() -> str:
|
||||
out = subprocess.run(["powershell.exe", "-NoProfile", "-Command",
|
||||
"[System.IO.Ports.SerialPort]::GetPortNames() -join ','"],
|
||||
capture_output=True, text=True).stdout.strip().replace("\r", "")
|
||||
ports = [p for p in out.split(",") if p]
|
||||
if not ports:
|
||||
sys.exit("COM-порт не найден. Подключите рацию.")
|
||||
return ports[0]
|
||||
|
||||
|
||||
def xfer(port: str, tx: bytes, timeout_ms: int = 1200, idle_ms: int = 250) -> bytes:
|
||||
txlist = ",".join(str(x) for x in tx) if tx else ""
|
||||
script = PS.format(port=port, baud=DEFAULT_BAUD, txlist=txlist,
|
||||
tmo=timeout_ms, idle=idle_ms)
|
||||
r = subprocess.run(["powershell.exe", "-NoProfile", "-Command", script],
|
||||
capture_output=True, text=True)
|
||||
line = [l for l in r.stdout.replace("\r", "").split("\n") if l.startswith(("OK:", "ERR:"))]
|
||||
if not line:
|
||||
return b""
|
||||
if line[0].startswith("ERR:"):
|
||||
sys.exit(line[0])
|
||||
return bytes.fromhex(line[0][3:])
|
||||
|
||||
|
||||
def show(tx: bytes, rx: bytes):
|
||||
print(f" TX: {tx.hex(' ')}")
|
||||
if not rx:
|
||||
print(" RX: (нет ответа)")
|
||||
return None
|
||||
print(f" RX: {rx.hex(' ')}")
|
||||
p = parse(rx)
|
||||
if p:
|
||||
print(f" cmd=0x{p['cmd']:02X} status=0x{p['status']:02X} "
|
||||
f"len={p['length']} crc={'ok' if p['crc_ok'] else 'BAD'}"
|
||||
+ (f" payload={p['payload'].hex(' ')}" if p['payload'] else ""))
|
||||
return p
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--port")
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
sub.add_parser("ping")
|
||||
s = sub.add_parser("send"); s.add_argument("code"); s.add_argument("--b", default="1")
|
||||
s.add_argument("--sub", default="1"); s.add_argument("--payload", default="")
|
||||
r = sub.add_parser("raw"); r.add_argument("hex", nargs="+")
|
||||
sc = sub.add_parser("scan"); sc.add_argument("first"); sc.add_argument("last")
|
||||
a = ap.parse_args()
|
||||
port = a.port or find_port()
|
||||
print(f"[порт {port} @ {DEFAULT_BAUD}]")
|
||||
|
||||
if a.cmd == "ping":
|
||||
tx = bytes([0x68, 0x84, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10])
|
||||
show(tx, xfer(port, tx))
|
||||
elif a.cmd == "send":
|
||||
tx = build(int(a.code, 16), int(a.b, 0), int(a.sub, 0), bytes.fromhex(a.payload))
|
||||
show(tx, xfer(port, tx))
|
||||
elif a.cmd == "raw":
|
||||
tx = bytes.fromhex("".join(a.hex))
|
||||
show(tx, xfer(port, tx))
|
||||
elif a.cmd == "scan":
|
||||
lo, hi = int(a.first, 16), int(a.last, 16)
|
||||
print(f"опрос команд 0x{lo:02X}..0x{hi:02X} (b=1 sub=1, пустой payload)\n")
|
||||
for c in range(lo, hi + 1):
|
||||
tx = build(c)
|
||||
rx = xfer(port, tx, timeout_ms=600, idle_ms=150)
|
||||
p = parse(rx) if rx else None
|
||||
if p:
|
||||
print(f" cmd 0x{c:02X}: status=0x{p['status']:02X} len={p['length']}"
|
||||
+ (f" payload={p['payload'].hex(' ')}" if p['payload'] else ""))
|
||||
elif rx:
|
||||
print(f" cmd 0x{c:02X}: неразобранный ответ {rx.hex(' ')}")
|
||||
time.sleep(0.03)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Map every GPIO write in the RT-4D app: find calls to the low-level GPIO
|
||||
helpers and back-resolve (port, mask, state) from the preceding instructions."""
|
||||
import struct, re, capstone, os
|
||||
|
||||
BASE = 0x08002800
|
||||
P = "/home/viktor/claude/rt-4d/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin"
|
||||
IMG = open(P, "rb").read()
|
||||
md = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)
|
||||
|
||||
PORTS = {0x40020000: "GPIOA", 0x40020400: "GPIOB", 0x40020800: "GPIOC",
|
||||
0x40020C00: "GPIOD", 0x40021000: "GPIOE", 0x40021400: "GPIOF",
|
||||
0x40021800: "GPIOG"}
|
||||
# low-level writers discovered by disassembly
|
||||
HELPERS = {0x08021C6E: "gpio_set_state(port,mask,state)",
|
||||
0x08021C56: "gpio_bsrr_write(port,mask)",
|
||||
0x08021C4C: "set_bit0(reg,val)"}
|
||||
|
||||
|
||||
def word(va):
|
||||
o = va - BASE
|
||||
return struct.unpack_from("<I", IMG, o)[0] if 0 <= o <= len(IMG) - 4 else None
|
||||
|
||||
|
||||
def lit_of(ins):
|
||||
m = re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", ins.op_str)
|
||||
if m and ins.mnemonic.startswith("ldr"):
|
||||
return word(((ins.address + 4) & ~3) + int(m.group(1), 16))
|
||||
return None
|
||||
|
||||
|
||||
# linear sweep, keep a sliding window to resolve register values
|
||||
def scan():
|
||||
hits = []
|
||||
win = []
|
||||
for ins in md.disasm(IMG, BASE):
|
||||
win.append(ins)
|
||||
if len(win) > 14:
|
||||
win.pop(0)
|
||||
if ins.mnemonic == "bl" and ins.op_str.startswith("#"):
|
||||
tgt = int(ins.op_str[1:], 16)
|
||||
if tgt in HELPERS:
|
||||
regs = {}
|
||||
for p in win[:-1]:
|
||||
mm = re.match(r"(r\d+)", p.op_str)
|
||||
dst = mm.group(1) if mm else None
|
||||
if not dst:
|
||||
continue
|
||||
v = lit_of(p)
|
||||
if v is not None:
|
||||
regs[dst] = v
|
||||
elif p.mnemonic in ("movs", "mov.w", "mov") and "#" in p.op_str:
|
||||
try:
|
||||
regs[dst] = int(p.op_str.split("#")[1], 0)
|
||||
except Exception:
|
||||
pass
|
||||
elif p.mnemonic == "movw" and "#" in p.op_str:
|
||||
try:
|
||||
regs[dst] = int(p.op_str.split("#")[1], 0)
|
||||
except Exception:
|
||||
pass
|
||||
hits.append((ins.address, HELPERS[tgt], regs.get("r0"), regs.get("r1"), regs.get("r2")))
|
||||
return hits
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rows = scan()
|
||||
print(f"{len(rows)} GPIO-helper call sites\n")
|
||||
print(f"{'site':<12}{'helper':<32}{'r0(port/reg)':<22}{'r1(mask/val)':<14}{'r2'}")
|
||||
for site, h, r0, r1, r2 in rows:
|
||||
pn = PORTS.get(r0, f"0x{r0:08X}" if r0 is not None else "?")
|
||||
mask = f"0x{r1:X}" if r1 is not None else "?"
|
||||
bit = ""
|
||||
if r1 and r1 and (r1 & (r1 - 1)) == 0:
|
||||
bit = f" (bit{r1.bit_length()-1})"
|
||||
print(f"0x{site:08X} {h:<32}{pn:<22}{mask+bit:<14}{r2 if r2 is not None else ''}")
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Find every GPIO-base literal load in the RT-4D app and show the surrounding
|
||||
code, so each (port, register-offset, bit-mask) access can be identified."""
|
||||
import struct, re, sys, capstone
|
||||
|
||||
BASE = 0x08002800
|
||||
IMG = open("/home/viktor/claude/rt-4d/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin", "rb").read()
|
||||
md = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)
|
||||
|
||||
PORTS = {0x40020000: "GPIOA", 0x40020400: "GPIOB", 0x40020800: "GPIOC",
|
||||
0x40020C00: "GPIOD", 0x40021000: "GPIOE", 0x40021400: "GPIOF",
|
||||
0x40021800: "GPIOG", 0x40021C00: "GPIOH"}
|
||||
|
||||
|
||||
def word(va):
|
||||
o = va - BASE
|
||||
return struct.unpack_from("<I", IMG, o)[0] if 0 <= o <= len(IMG) - 4 else None
|
||||
|
||||
|
||||
def litval(ins):
|
||||
m = re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", ins.op_str)
|
||||
if m and ins.mnemonic.startswith("ldr"):
|
||||
return word(((ins.address + 4) & ~3) + int(m.group(1), 16))
|
||||
return None
|
||||
|
||||
|
||||
def main(ctx=6, only=None):
|
||||
ins_list = list(md.disasm(IMG, BASE))
|
||||
idx = {i.address: n for n, i in enumerate(ins_list)}
|
||||
hits = []
|
||||
for n, i in enumerate(ins_list):
|
||||
v = litval(i)
|
||||
if v in PORTS and (only is None or v == only):
|
||||
hits.append((n, i, v))
|
||||
print(f"{len(hits)} GPIO-base literal loads\n")
|
||||
for n, i, v in hits:
|
||||
print(f"=== {PORTS[v]} (0x{v:08X}) referenced at 0x{i.address:08X} ===")
|
||||
for k in range(n, min(n + ctx, len(ins_list))):
|
||||
j = ins_list[k]
|
||||
extra = ""
|
||||
lv = litval(j)
|
||||
if lv is not None:
|
||||
extra = f" ; =0x{lv:08X}"
|
||||
print(f" 0x{j.address:08X}: {j.mnemonic:8} {j.op_str}{extra}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
only = int(sys.argv[1], 16) if len(sys.argv) > 1 else None
|
||||
main(only=only)
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exhaustive reference finder for the RT-4D app image.
|
||||
|
||||
Decodes one instruction at EVERY 2-byte alignment (linear sweep desyncs on
|
||||
Thumb), so nothing is missed.
|
||||
|
||||
python refs.py addr <0xADDR> [ctx] who pc-relative-loads this address/constant
|
||||
python refs.py gpio full GPIO map: (port, register, mask) per site
|
||||
"""
|
||||
import sys, struct, re, capstone
|
||||
|
||||
BASE = 0x08002800
|
||||
IMG = open("/home/viktor/claude/rt-4d/stock-fw/rt4d_stock_v3.25_abs_0x08002800.bin", "rb").read()
|
||||
md = capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_THUMB)
|
||||
|
||||
PORTS = {0x40020000: "GPIOA", 0x40020400: "GPIOB", 0x40020800: "GPIOC",
|
||||
0x40020C00: "GPIOD", 0x40021000: "GPIOE", 0x40021400: "GPIOF"}
|
||||
# AT32F43x GPIO layout
|
||||
REG = {0x00: "CFGR", 0x10: "IDT", 0x14: "ODT", 0x18: "SCR(set)",
|
||||
0x1C: "CLR16", 0x28: "CLR(clear)", 0x2C: "TOGR(toggle)"}
|
||||
|
||||
|
||||
def word(va):
|
||||
o = va - BASE
|
||||
return struct.unpack_from("<I", IMG, o)[0] if 0 <= o <= len(IMG) - 4 else None
|
||||
|
||||
|
||||
def decode_at(va):
|
||||
o = va - BASE
|
||||
for i in md.disasm(IMG[o:o + 4], va):
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def litval(i):
|
||||
if i is None or not i.mnemonic.startswith("ldr"):
|
||||
return None
|
||||
m = re.search(r"\[pc, #(?:0x)?([0-9a-fA-F]+)\]", i.op_str)
|
||||
if not m:
|
||||
return None
|
||||
return word(((i.address + 4) & ~3) + int(m.group(1), 16))
|
||||
|
||||
|
||||
def all_lit_loads():
|
||||
"""[(va, ins, value)] for every pc-relative ldr at any 2-byte alignment"""
|
||||
out = []
|
||||
for off in range(0, len(IMG) - 4, 2):
|
||||
va = BASE + off
|
||||
i = decode_at(va)
|
||||
v = litval(i)
|
||||
if v is not None:
|
||||
out.append((va, i, v))
|
||||
return out
|
||||
|
||||
|
||||
def cmd_addr(target, ctx=8):
|
||||
hits = [(va, i) for va, i, v in all_lit_loads() if v == target]
|
||||
print(f"pc-relative loads of 0x{target:08X}: {len(hits)}")
|
||||
for va, i in hits:
|
||||
print(f"\n--- 0x{va:08X} ({i.mnemonic} {i.op_str}) ---")
|
||||
a = va
|
||||
for _ in range(ctx):
|
||||
j = decode_at(a)
|
||||
if j is None:
|
||||
break
|
||||
v = litval(j)
|
||||
e = f" ; =0x{v:08X}" if v is not None else ""
|
||||
print(f" 0x{j.address:08X}: {j.mnemonic:8} {j.op_str}{e}")
|
||||
a += j.size
|
||||
|
||||
|
||||
def cmd_gpio():
|
||||
loads = all_lit_loads()
|
||||
rows = []
|
||||
for va, i, v in loads:
|
||||
if v not in PORTS:
|
||||
continue
|
||||
reg = i.op_str.split(",")[0].strip()
|
||||
a = va + i.size
|
||||
mask = None
|
||||
# small forward window: find store to [reg,#off]
|
||||
for _ in range(6):
|
||||
j = decode_at(a)
|
||||
if j is None:
|
||||
break
|
||||
m = re.match(r"(r\d+), \[" + reg + r", #(0x[0-9a-fA-F]+|\d+)\]$", j.op_str)
|
||||
if j.mnemonic.startswith("str") and m:
|
||||
src, off = m.group(1), int(m.group(2), 0)
|
||||
# back-window for the immediate loaded into src
|
||||
b = va
|
||||
for _ in range(10):
|
||||
b -= 2
|
||||
p = decode_at(b)
|
||||
if p and p.op_str.startswith(src + ", #"):
|
||||
try:
|
||||
mask = int(p.op_str.split("#")[1], 0)
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
rows.append((va, PORTS[v], off, mask))
|
||||
break
|
||||
a += j.size
|
||||
print(f"{len(rows)} GPIO store sites\n")
|
||||
print(f"{'site':<13}{'port':<7}{'register':<14}{'mask':<10}bit")
|
||||
for va, p, off, m in rows:
|
||||
bit = f"bit{m.bit_length()-1}" if m and (m & (m - 1)) == 0 else ("?" if m is None else "multi")
|
||||
print(f"0x{va:08X} {p:<7}{REG.get(off, hex(off)):<14}{(hex(m) if m is not None else '?'):<10}{bit}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1] == "addr":
|
||||
cmd_addr(int(sys.argv[2], 16), int(sys.argv[3]) if len(sys.argv) > 3 else 8)
|
||||
elif sys.argv[1] == "gpio":
|
||||
cmd_gpio()
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a clipped region of the RT-4D schematic PDF at high DPI.
|
||||
python schem.py <x0> <y0> <x1> <y1> <dpi> <out.png> (coords in PDF points)
|
||||
Sheets (approx, points): 1: y 12-582 | 2: y 600-1170 | 3: y 1200-1764 | 4: y 1794-2370
|
||||
Page is 864 x 2400 pt.
|
||||
"""
|
||||
import sys, fitz
|
||||
|
||||
PDF = "/mnt/c/Users/vikto/Downloads/Telegram Desktop/RT4DDLT01.pdf"
|
||||
|
||||
def render(x0, y0, x1, y1, dpi, out):
|
||||
doc = fitz.open(PDF)
|
||||
p = doc[0]
|
||||
clip = fitz.Rect(x0, y0, x1, y1)
|
||||
pm = p.get_pixmap(dpi=dpi, clip=clip)
|
||||
pm.save(out)
|
||||
print(f"{out}: {pm.width}x{pm.height} clip=({x0},{y0})-({x1},{y1}) dpi={dpi}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1:]
|
||||
render(float(a[0]), float(a[1]), float(a[2]), float(a[3]), int(a[4]), a[5])
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/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])
|
||||
Ссылка в новой задаче
Block a user