#!/usr/bin/env python3 """Scan firmware regions for consecutive function-pointer tables. Finds runs of >= min_run consecutive little-endian dwords that all fall in a given code range — the shape of MCU command-handler dispatch tables. Usage: scan_tables.py [min_run] [tolerance] tolerance = max absolute non-code dwords allowed inside a run (0/NULL counted). """ import struct import sys from pathlib import Path path = Path(sys.argv[1]) lo, hi = int(sys.argv[2], 0), int(sys.argv[3], 0) min_run = int(sys.argv[4]) if len(sys.argv) > 4 else 8 tol = int(sys.argv[5]) if len(sys.argv) > 5 else 0 d = path.read_bytes() base = 0 # If the region file corresponds to a load address, pass that as lo bound # via argv; region offsets here are file offsets, so also print file offset. words = struct.unpack_from('<%dI' % (len(d) // 4), d) run_start = None run = [] tables = [] def flush(end): global run, run_start code = sum(1 for w in run if lo <= w < hi) if len(run) >= min_run and code >= min_run: tables.append((run_start, end, len(run), code)) run = [] run_start = None for i, w in enumerate(words): ok = lo <= w < hi or w == 0 if ok: if run_start is None: run_start = i * 4 run.append(w) else: if run_start is not None and tol: run.append(w) # tolerate, next check trims if sum(1 for x in run[-3:] if not (lo <= x < hi or x == 0)) >= 2: flush(i * 4) else: flush(i * 4) for t in tables: start_off, end_off, n, code = t print('table @ file+0x%x..0x%x (%d words, %d code ptrs)' % (start_off, end_off, n, code)) vals = words[start_off // 4:end_off // 4] for j, w in enumerate(vals[:64]): mark = '' if lo <= w < hi else ('(0)' if w == 0 else '(?)') print(' [%2d] 0x%08x %s' % (j, w, mark))