Add DM analysis tool
tools/analyze_dm.py scans 16-bit DM dumps for: - Null regions (BSS, uninitialized memory) - Q15 coefficient tables with range and average - Periodic waveforms (sine/cosine lookup tables) - ASCII strings - Unclassified data regions with hex preview Supports big-endian (default) and little-endian byte order. Outputs DM word addresses for cross-reference with PM code.
This commit is contained in:
251
tools/analyze_dm.py
Executable file
251
tools/analyze_dm.py
Executable file
@@ -0,0 +1,251 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""analyze_dm.py -- Analyze ADSP-219x DM (Data Memory) dumps.
|
||||||
|
|
||||||
|
Scans a 16-bit DM binary for:
|
||||||
|
- Null regions (uninitialized / BSS)
|
||||||
|
- Coefficient tables (Q15 fixed-point patterns)
|
||||||
|
- Lookup tables (periodic waveforms)
|
||||||
|
- ASCII strings
|
||||||
|
- Non-zero data regions
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 analyze_dm.py firmware_dm.bin
|
||||||
|
python3 analyze_dm.py firmware_dm.bin --endian little
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_words(path, endian="big"):
|
||||||
|
"""Read 16-bit words from a binary file."""
|
||||||
|
fmt = ">H" if endian == "big" else "<H"
|
||||||
|
data = open(path, "rb").read()
|
||||||
|
if len(data) % 2:
|
||||||
|
data += b"\x00"
|
||||||
|
return [struct.unpack_from(fmt, data, i)[0]
|
||||||
|
for i in range(0, len(data), 2)]
|
||||||
|
|
||||||
|
|
||||||
|
def signed16(v):
|
||||||
|
"""Convert unsigned 16-bit to signed."""
|
||||||
|
return v - 0x10000 if v >= 0x8000 else v
|
||||||
|
|
||||||
|
|
||||||
|
def find_null_regions(words, min_run=8):
|
||||||
|
"""Find contiguous runs of zero words."""
|
||||||
|
regions = []
|
||||||
|
start = None
|
||||||
|
for i, w in enumerate(words):
|
||||||
|
if w == 0:
|
||||||
|
if start is None:
|
||||||
|
start = i
|
||||||
|
else:
|
||||||
|
if start is not None and (i - start) >= min_run:
|
||||||
|
regions.append((start, i - start))
|
||||||
|
start = None
|
||||||
|
if start is not None and (len(words) - start) >= min_run:
|
||||||
|
regions.append((start, len(words) - start))
|
||||||
|
return regions
|
||||||
|
|
||||||
|
|
||||||
|
def find_coeff_tables(words, min_len=8, max_magnitude=0x7FFF):
|
||||||
|
"""Find potential Q15 coefficient tables.
|
||||||
|
|
||||||
|
Looks for runs of non-zero values where all values are
|
||||||
|
within the Q15 range and show some variance (not constant).
|
||||||
|
"""
|
||||||
|
tables = []
|
||||||
|
start = None
|
||||||
|
run = []
|
||||||
|
|
||||||
|
for i, w in enumerate(words):
|
||||||
|
sw = signed16(w)
|
||||||
|
if w != 0 and abs(sw) <= max_magnitude:
|
||||||
|
if start is None:
|
||||||
|
start = i
|
||||||
|
run = []
|
||||||
|
run.append(sw)
|
||||||
|
else:
|
||||||
|
if start is not None and len(run) >= min_len:
|
||||||
|
mn = min(run)
|
||||||
|
mx = max(run)
|
||||||
|
if mx != mn: # not constant
|
||||||
|
tables.append((start, len(run), mn, mx,
|
||||||
|
sum(run) / len(run)))
|
||||||
|
start = None
|
||||||
|
run = []
|
||||||
|
|
||||||
|
if start is not None and len(run) >= min_len:
|
||||||
|
mn = min(run)
|
||||||
|
mx = max(run)
|
||||||
|
if mx != mn:
|
||||||
|
tables.append((start, len(run), mn, mx,
|
||||||
|
sum(run) / len(run)))
|
||||||
|
return tables
|
||||||
|
|
||||||
|
|
||||||
|
def detect_periodicity(words, start, length):
|
||||||
|
"""Check if a region has periodic content (sine/cosine table)."""
|
||||||
|
if length < 16:
|
||||||
|
return None
|
||||||
|
vals = [signed16(words[start + i]) for i in range(length)]
|
||||||
|
|
||||||
|
# Try periods from 16 to length/2
|
||||||
|
best_period = None
|
||||||
|
best_err = float("inf")
|
||||||
|
for period in range(16, min(length // 2 + 1, 1025)):
|
||||||
|
if length < period * 2:
|
||||||
|
continue
|
||||||
|
err = 0.0
|
||||||
|
count = 0
|
||||||
|
for i in range(period, min(length, period * 4)):
|
||||||
|
diff = vals[i] - vals[i % period]
|
||||||
|
err += diff * diff
|
||||||
|
count += 1
|
||||||
|
if count > 0:
|
||||||
|
rms = math.sqrt(err / count)
|
||||||
|
if rms < best_err:
|
||||||
|
best_err = rms
|
||||||
|
best_period = period
|
||||||
|
|
||||||
|
if best_period and best_err < 500:
|
||||||
|
return best_period
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_strings(data, min_len=4):
|
||||||
|
"""Find ASCII strings in raw bytes."""
|
||||||
|
strings = []
|
||||||
|
current = []
|
||||||
|
start = None
|
||||||
|
for i, byte in enumerate(data):
|
||||||
|
if 0x20 <= byte < 0x7F:
|
||||||
|
if start is None:
|
||||||
|
start = i
|
||||||
|
current.append(chr(byte))
|
||||||
|
else:
|
||||||
|
if current and len(current) >= min_len:
|
||||||
|
strings.append((start, "".join(current)))
|
||||||
|
current = []
|
||||||
|
start = None
|
||||||
|
if current and len(current) >= min_len:
|
||||||
|
strings.append((start, "".join(current)))
|
||||||
|
return strings
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(path, endian="big"):
|
||||||
|
"""Run full analysis on a DM dump."""
|
||||||
|
data = open(path, "rb").read()
|
||||||
|
size = len(data)
|
||||||
|
words = read_words(path, endian)
|
||||||
|
n_words = len(words)
|
||||||
|
|
||||||
|
print(f"File: {path}")
|
||||||
|
print(f"Size: {size} bytes ({n_words} words, 16-bit {endian}-endian)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Null regions
|
||||||
|
nulls = find_null_regions(words)
|
||||||
|
if nulls:
|
||||||
|
total_null = sum(n for _, n in nulls)
|
||||||
|
print(f"=== Null Regions ({len(nulls)} found, "
|
||||||
|
f"{total_null} words = {total_null*100//n_words}%) ===")
|
||||||
|
for addr, length in nulls:
|
||||||
|
print(f" DM 0x{addr:04X} - 0x{addr+length-1:04X}"
|
||||||
|
f" ({length} words, {length*2} bytes)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Coefficient tables
|
||||||
|
coeffs = find_coeff_tables(words)
|
||||||
|
if coeffs:
|
||||||
|
print(f"=== Potential Coefficient Tables ({len(coeffs)} found) ===")
|
||||||
|
for addr, length, mn, mx, avg in coeffs:
|
||||||
|
q15_min = mn / 32768.0
|
||||||
|
q15_max = mx / 32768.0
|
||||||
|
period = detect_periodicity(words, addr, length)
|
||||||
|
pstr = f", period~{period}" if period else ""
|
||||||
|
print(f" DM 0x{addr:04X} - 0x{addr+length-1:04X}"
|
||||||
|
f" ({length} words)")
|
||||||
|
print(f" Range: {mn}..{mx} (Q15: {q15_min:.4f}"
|
||||||
|
f"..{q15_max:.4f}), avg={avg:.1f}{pstr}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Strings
|
||||||
|
strings = find_strings(data)
|
||||||
|
if strings:
|
||||||
|
print(f"=== ASCII Strings ({len(strings)} found) ===")
|
||||||
|
for offset, s in strings:
|
||||||
|
dm_addr = offset // 2
|
||||||
|
print(f" Byte 0x{offset:04X} (DM ~0x{dm_addr:04X}):"
|
||||||
|
f" \"{s}\"")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Non-null, non-coefficient data regions
|
||||||
|
covered = set()
|
||||||
|
for addr, length in nulls:
|
||||||
|
for i in range(addr, addr + length):
|
||||||
|
covered.add(i)
|
||||||
|
for addr, length, _, _, _ in coeffs:
|
||||||
|
for i in range(addr, addr + length):
|
||||||
|
covered.add(i)
|
||||||
|
|
||||||
|
data_regions = []
|
||||||
|
start = None
|
||||||
|
for i in range(n_words):
|
||||||
|
if i not in covered and words[i] != 0:
|
||||||
|
if start is None:
|
||||||
|
start = i
|
||||||
|
else:
|
||||||
|
if start is not None:
|
||||||
|
length = i - start
|
||||||
|
if length >= 4:
|
||||||
|
data_regions.append((start, length))
|
||||||
|
start = None
|
||||||
|
if start is not None and (n_words - start) >= 4:
|
||||||
|
data_regions.append((start, n_words - start))
|
||||||
|
|
||||||
|
if data_regions:
|
||||||
|
print(f"=== Other Data Regions ({len(data_regions)} found) ===")
|
||||||
|
for addr, length in data_regions:
|
||||||
|
sample = [f"0x{words[addr+j]:04X}"
|
||||||
|
for j in range(min(length, 8))]
|
||||||
|
dots = " ..." if length > 8 else ""
|
||||||
|
print(f" DM 0x{addr:04X} - 0x{addr+length-1:04X}"
|
||||||
|
f" ({length} words)")
|
||||||
|
print(f" [{', '.join(sample)}{dots}]")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
total_nonzero = sum(1 for w in words if w != 0)
|
||||||
|
print(f"=== Summary ===")
|
||||||
|
print(f" Total words: {n_words}")
|
||||||
|
print(f" Non-zero words: {total_nonzero}"
|
||||||
|
f" ({total_nonzero*100//max(n_words,1)}%)")
|
||||||
|
print(f" Null regions: {len(nulls)}")
|
||||||
|
print(f" Coeff tables: {len(coeffs)}")
|
||||||
|
print(f" Strings: {len(strings)}")
|
||||||
|
print(f" Other data: {len(data_regions)}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Analyze ADSP-219x DM (Data Memory) dumps.")
|
||||||
|
parser.add_argument("file", help="DM binary file")
|
||||||
|
parser.add_argument("--endian", choices=["big", "little"],
|
||||||
|
default="big",
|
||||||
|
help="Byte order (default: big)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.isfile(args.file):
|
||||||
|
print(f"Error: {args.file} not found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
analyze(args.file, args.endian)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user