diff --git a/firmware/tools/guru_lookup.py b/firmware/tools/guru_lookup.py new file mode 100755 index 000000000..a5517a6cf --- /dev/null +++ b/firmware/tools/guru_lookup.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 + +# +# copyleft 2026 zxkmm co author with AI +# +# This file is part of PortaPack. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +"""Translate a guru meditation pc/lr into a symbol, source line and disassembly. + +Plain `arm-none-eabi-gdb -ex "x/3i 0xADDR" application.elf` only works for +addresses in the main M0 firmware. External apps are linked at a placeholder +0xADxxxxxx address but *run* from ~0x1008xxxx, so the guru shows a runtime +address that does not exist in the ELF. This script does the translation: + + link_addr = section_vma + (runtime_addr - memory_location) + +where memory_location is the first word of the app's .ppma header. + +Usage: + guru_lookup.py 0x10085CF9 0x0FF36284 + guru_lookup.py --app waterfall_designer 0x10085CF9 + guru_lookup.py --baseband adsbrx 0x10081234 +""" + +import argparse +import os +import re +import struct +import subprocess +import sys + +REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + + +def find_toolchain(explicit): + """Locate the arm-none-eabi- prefix: --toolchain, $ARM_TOOLCHAIN, ./armbin/bin, PATH.""" + candidates = [] + if explicit: + candidates.append(explicit) + if os.environ.get("ARM_TOOLCHAIN"): + candidates.append(os.environ["ARM_TOOLCHAIN"]) + candidates.append(os.path.join(REPO, "armbin", "bin", "arm-none-eabi-")) + candidates.append("arm-none-eabi-") + + for prefix in candidates: + try: + subprocess.run([prefix + "addr2line", "--version"], + capture_output=True, check=True) + return prefix + except (OSError, subprocess.CalledProcessError): + continue + sys.exit("error: no arm-none-eabi toolchain found (try --toolchain /path/to/arm-none-eabi-)") + + +def run(*cmd): + return subprocess.run(cmd, capture_output=True, text=True).stdout + + +def section_vmas(tc, elf): + """section name -> (vma, size) for every .external_app_* section.""" + out = run(tc + "objdump", "-h", elf) + vmas = {} + for m in re.finditer(r'\.external_app_(\S+)\s+([0-9a-f]{8})\s+([0-9a-f]{8})', out): + vmas[m.group(1)] = (int(m.group(3), 16), int(m.group(2), 16)) + return vmas + + +def app_load_addresses(build_dir): + """app name -> memory_location, read from the first word of each .ppma.""" + app_dir = os.path.join(build_dir, "firmware", "application") + loads = {} + if not os.path.isdir(app_dir): + return loads + for name in os.listdir(app_dir): + if not name.endswith(".ppma"): + continue + path = os.path.join(app_dir, name) + try: + with open(path, "rb") as f: + (mem,) = struct.unpack("> " if here else " ", l.strip())) + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("addresses", nargs="+", help="pc / lr values from the guru screen") + p.add_argument("--build", default=os.path.join(REPO, "build"), help="build directory") + p.add_argument("--app", help="external app name, if auto-detection is ambiguous") + p.add_argument("--baseband", help="resolve against firmware/baseband/baseband_.elf") + p.add_argument("--toolchain", help="arm-none-eabi- prefix") + args = p.parse_args() + + tc = find_toolchain(args.toolchain) + app_elf = os.path.join(args.build, "firmware", "application", "application.elf") + + if args.baseband: + elf = os.path.join(args.build, "firmware", "baseband", + "baseband_%s.elf" % args.baseband) + if not os.path.exists(elf): + sys.exit("error: no such baseband image: " + elf) + for a in args.addresses: + addr = int(a, 16) & ~1 + print("\n%s (M4 baseband: %s)" % (a, args.baseband)) + describe(tc, elf, addr) + return + + if not os.path.exists(app_elf): + sys.exit("error: %s not found (pass --build)" % app_elf) + + vmas = section_vmas(tc, app_elf) + loads = app_load_addresses(args.build) + + for a in args.addresses: + addr = int(a, 16) & ~1 # drop the Thumb bit + print("\n=== %s ===" % a) + + # Main M0 firmware is linked at 0 (SPIFI shadow), so it resolves directly. + if addr < 0x00100000: + print(" main M0 firmware") + describe(tc, app_elf, addr) + continue + + # Otherwise it is an external app running from local SRAM. Every app has + # its own memory_location, so several can plausibly contain the address; + # list each candidate and let the caller pick the app they actually ran. + names = [args.app] if args.app else sorted(loads) + hits = 0 + for name in names: + if name not in loads or name not in vmas: + continue + off = addr - loads[name] + vma, size = vmas[name] + if not (0 <= off < size): + continue + hits += 1 + print(" candidate: %s load=0x%08X offset=0x%X" % (name, loads[name], off)) + describe(tc, app_elf, vma + off, "link ") + + if hits == 0: + print(" no external app contains this address.") + print(" If the guru header said M4, re-run with --baseband .") + print(" A wild pc with a sane lr usually means an indirect call through") + print(" a bad pointer - look up the lr instead, that is the caller.") + elif hits > 1 and not args.app: + print(" (%d candidates - narrow it with --app )" % hits) + + +if __name__ == "__main__": + main()