from dataclasses import dataclass from typing import Optional, Tuple, Dict, ClassVar import struct import posixpath import zlib import gdb def get_file_crc32(filename): with open(filename, "rb") as f: return zlib.crc32(f.read()) @dataclass class AppState: name: str text_address: int = 0 entry_address: int = 0 other_sections: Dict[str, int] = None debug_link_elf: str = "" debug_link_crc: int = 0 DEBUG_ELF_ROOT: ClassVar[Optional[str]] = None def __post_init__(self): if self.other_sections is None: self.other_sections = {} def get_original_elf_path(self) -> str: if self.DEBUG_ELF_ROOT is None: raise ValueError("DEBUG_ELF_ROOT not set; call fap-set-debug-elf-root") return ( posixpath.join(self.DEBUG_ELF_ROOT, self.debug_link_elf) if self.DEBUG_ELF_ROOT else self.debug_link_elf ) def is_debug_available(self) -> bool: have_debug_info = bool(self.debug_link_elf and self.debug_link_crc) if not have_debug_info: print("No debug info available for this app") return False debug_elf_path = self.get_original_elf_path() debug_elf_crc32 = get_file_crc32(debug_elf_path) if self.debug_link_crc != debug_elf_crc32: print( f"Debug info ({debug_elf_path}) CRC mismatch: {self.debug_link_crc:08x} != {debug_elf_crc32:08x}, rebuild app" ) return False return True def get_gdb_load_command(self) -> str: load_path = self.get_original_elf_path() print(f"Loading debug information from {load_path}") load_command = ( f"add-symbol-file -readnow {load_path} 0x{self.text_address:08x} " ) load_command += " ".join( f"-s {name} 0x{address:08x}" for name, address in self.other_sections.items() ) return load_command def get_gdb_unload_command(self) -> str: return f"remove-symbol-file -a 0x{self.text_address:08x}" @staticmethod def get_gdb_app_ep(app) -> int: return int(app["state"]["entry"]) @staticmethod def parse_debug_link_data(section_data: bytes) -> Tuple[str, int]: # Debug link format: a null-terminated string with debuggable file name # Padded with 0's to multiple of 4 bytes # Followed by 4 bytes of CRC32 checksum of that file elf_name = section_data[:-4].decode("utf-8").split("\x00")[0] crc32 = struct.unpack(" "AppState": state = AppState(str(gdb_app["manifest"]["name"].string())) state.entry_address = cls.get_gdb_app_ep(gdb_app) app_state = gdb_app["state"] if debug_link_size := int(app_state["debug_link_info"]["debug_link_size"]): debug_link_data = ( gdb.selected_inferior() .read_memory( int(app_state["debug_link_info"]["debug_link"]), debug_link_size ) .tobytes() ) state.debug_link_elf, state.debug_link_crc = AppState.parse_debug_link_data( debug_link_data ) for idx in range(app_state["mmap_entry_count"]): mmap_entry = app_state["mmap_entries"][idx] section_name = mmap_entry["name"].string() section_addr = int(mmap_entry["address"]) if section_name == ".text": state.text_address = section_addr else: state.other_sections[section_name] = section_addr return state class SetFapDebugElfRoot(gdb.Command): """Set path to original ELF files for debug info""" def __init__(self): super().__init__( "fap-set-debug-elf-root", gdb.COMMAND_FILES, gdb.COMPLETE_FILENAME ) self.dont_repeat() def invoke(self, arg, from_tty): AppState.DEBUG_ELF_ROOT = arg try: global helper print(f"Set '{arg}' as debug info lookup path for Flipper external apps") helper.attach_to_fw() gdb.events.stop.connect(helper.handle_stop) gdb.events.gdb_exiting.connect(helper.handle_exit) except gdb.error as e: print(f"Support for Flipper external apps debug is not available: {e}") class FlipperAppStateHelper: def __init__(self): self.app_type_ptr = None self.app_list_ptr = None self.app_list_entry_type = None self._current_apps: list[AppState] = [] self.set_debug_mode(True) def _walk_app_list(self, list_head): while list_head: if app := list_head["data"]: yield app.dereference() list_head = list_head["next"] def _exec_gdb_command(self, command: str) -> bool: try: gdb.execute(command) return True except gdb.error as e: print(f"Failed to execute GDB command '{command}': {e}") return False def _sync_apps(self) -> None: self.set_debug_mode(True) if not (app_list := self.app_list_ptr.value()): print("Reset app loader state") for app in self._current_apps: self._exec_gdb_command(app.get_gdb_unload_command()) self._current_apps = [] return loaded_apps: dict[int, gdb.Value] = dict( (AppState.get_gdb_app_ep(app), app) for app in self._walk_app_list(app_list[0]) ) for app in self._current_apps.copy(): if app.entry_address not in loaded_apps: print(f"Application {app.name} is no longer loaded") if not self._exec_gdb_command(app.get_gdb_unload_command()): print(f"Failed to unload debug info for {app.name}") self._current_apps.remove(app) for entry_point, app in loaded_apps.items(): if entry_point not in set(app.entry_address for app in self._current_apps): new_app_state = AppState.from_gdb(app) print(f"New application loaded. Adding debug info") if self._exec_gdb_command(new_app_state.get_gdb_load_command()): self._current_apps.append(new_app_state) else: print(f"Failed to load debug info for {new_app_state}") def attach_to_fw(self) -> None: print("Attaching to Flipper firmware") self.app_list_ptr = gdb.lookup_global_symbol( "flipper_application_loaded_app_list" ) self.app_type_ptr = gdb.lookup_type("FlipperApplication").pointer() self.app_list_entry_type = gdb.lookup_type("struct FlipperApplicationList_s") self._sync_apps() def handle_stop(self, event) -> None: self._sync_apps() def handle_exit(self, event) -> None: self.set_debug_mode(False) def set_debug_mode(self, mode: bool) -> None: try: gdb.execute(f"set variable furi_hal_debug_gdb_session_active = {int(mode)}") except gdb.error as e: print(f"Failed to set debug mode: {e}") # Init additional 'fap-set-debug-elf-root' command and set up hooks SetFapDebugElfRoot() helper = FlipperAppStateHelper() print("Support for Flipper external apps debug is loaded")