flipperapps.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. from dataclasses import dataclass
  2. from typing import Optional, Tuple, Dict, ClassVar
  3. import struct
  4. import posixpath
  5. import zlib
  6. import gdb
  7. def get_file_crc32(filename):
  8. with open(filename, "rb") as f:
  9. return zlib.crc32(f.read())
  10. @dataclass
  11. class AppState:
  12. name: str
  13. text_address: int = 0
  14. entry_address: int = 0
  15. other_sections: Dict[str, int] = None
  16. debug_link_elf: str = ""
  17. debug_link_crc: int = 0
  18. DEBUG_ELF_ROOT: ClassVar[Optional[str]] = None
  19. def __post_init__(self):
  20. if self.other_sections is None:
  21. self.other_sections = {}
  22. def get_original_elf_path(self) -> str:
  23. if self.DEBUG_ELF_ROOT is None:
  24. raise ValueError("DEBUG_ELF_ROOT not set; call fap-set-debug-elf-root")
  25. return (
  26. posixpath.join(self.DEBUG_ELF_ROOT, self.debug_link_elf)
  27. if self.DEBUG_ELF_ROOT
  28. else self.debug_link_elf
  29. )
  30. def is_debug_available(self) -> bool:
  31. have_debug_info = bool(self.debug_link_elf and self.debug_link_crc)
  32. if not have_debug_info:
  33. print("No debug info available for this app")
  34. return False
  35. debug_elf_path = self.get_original_elf_path()
  36. debug_elf_crc32 = get_file_crc32(debug_elf_path)
  37. if self.debug_link_crc != debug_elf_crc32:
  38. print(
  39. f"Debug info ({debug_elf_path}) CRC mismatch: {self.debug_link_crc:08x} != {debug_elf_crc32:08x}, rebuild app"
  40. )
  41. return False
  42. return True
  43. def get_gdb_load_command(self) -> str:
  44. load_path = self.get_original_elf_path()
  45. print(f"Loading debug information from {load_path}")
  46. load_command = (
  47. f"add-symbol-file -readnow {load_path} 0x{self.text_address:08x} "
  48. )
  49. load_command += " ".join(
  50. f"-s {name} 0x{address:08x}"
  51. for name, address in self.other_sections.items()
  52. )
  53. return load_command
  54. def get_gdb_unload_command(self) -> str:
  55. return f"remove-symbol-file -a 0x{self.text_address:08x}"
  56. @staticmethod
  57. def get_gdb_app_ep(app) -> int:
  58. return int(app["state"]["entry"])
  59. @staticmethod
  60. def parse_debug_link_data(section_data: bytes) -> Tuple[str, int]:
  61. # Debug link format: a null-terminated string with debuggable file name
  62. # Padded with 0's to multiple of 4 bytes
  63. # Followed by 4 bytes of CRC32 checksum of that file
  64. elf_name = section_data[:-4].decode("utf-8").split("\x00")[0]
  65. crc32 = struct.unpack("<I", section_data[-4:])[0]
  66. return (elf_name, crc32)
  67. @classmethod
  68. def from_gdb(cls, gdb_app: "AppState") -> "AppState":
  69. state = AppState(str(gdb_app["manifest"]["name"].string()))
  70. state.entry_address = cls.get_gdb_app_ep(gdb_app)
  71. app_state = gdb_app["state"]
  72. if debug_link_size := int(app_state["debug_link_info"]["debug_link_size"]):
  73. debug_link_data = (
  74. gdb.selected_inferior()
  75. .read_memory(
  76. int(app_state["debug_link_info"]["debug_link"]), debug_link_size
  77. )
  78. .tobytes()
  79. )
  80. state.debug_link_elf, state.debug_link_crc = AppState.parse_debug_link_data(
  81. debug_link_data
  82. )
  83. for idx in range(app_state["mmap_entry_count"]):
  84. mmap_entry = app_state["mmap_entries"][idx]
  85. section_name = mmap_entry["name"].string()
  86. section_addr = int(mmap_entry["address"])
  87. if section_name == ".text":
  88. state.text_address = section_addr
  89. else:
  90. state.other_sections[section_name] = section_addr
  91. return state
  92. class SetFapDebugElfRoot(gdb.Command):
  93. """Set path to original ELF files for debug info"""
  94. def __init__(self):
  95. super().__init__(
  96. "fap-set-debug-elf-root", gdb.COMMAND_FILES, gdb.COMPLETE_FILENAME
  97. )
  98. self.dont_repeat()
  99. def invoke(self, arg, from_tty):
  100. AppState.DEBUG_ELF_ROOT = arg
  101. try:
  102. global helper
  103. print(f"Set '{arg}' as debug info lookup path for Flipper external apps")
  104. helper.attach_to_fw()
  105. gdb.events.stop.connect(helper.handle_stop)
  106. gdb.events.exited.connect(helper.handle_exit)
  107. except gdb.error as e:
  108. print(f"Support for Flipper external apps debug is not available: {e}")
  109. class FlipperAppStateHelper:
  110. def __init__(self):
  111. self.app_type_ptr = None
  112. self.app_list_ptr = None
  113. self.app_list_entry_type = None
  114. self._current_apps: list[AppState] = []
  115. self.set_debug_mode(True)
  116. def _walk_app_list(self, list_head):
  117. while list_head:
  118. if app := list_head["data"]:
  119. yield app.dereference()
  120. list_head = list_head["next"]
  121. def _exec_gdb_command(self, command: str) -> bool:
  122. try:
  123. gdb.execute(command)
  124. return True
  125. except gdb.error as e:
  126. print(f"Failed to execute GDB command '{command}': {e}")
  127. return False
  128. def _sync_apps(self) -> None:
  129. self.set_debug_mode(True)
  130. if not (app_list := self.app_list_ptr.value()):
  131. print("Reset app loader state")
  132. for app in self._current_apps:
  133. self._exec_gdb_command(app.get_gdb_unload_command())
  134. self._current_apps = []
  135. return
  136. loaded_apps: dict[int, gdb.Value] = dict(
  137. (AppState.get_gdb_app_ep(app), app)
  138. for app in self._walk_app_list(app_list[0])
  139. )
  140. for app in self._current_apps.copy():
  141. if app.entry_address not in loaded_apps:
  142. print(f"Application {app.name} is no longer loaded")
  143. if not self._exec_gdb_command(app.get_gdb_unload_command()):
  144. print(f"Failed to unload debug info for {app.name}")
  145. self._current_apps.remove(app)
  146. for entry_point, app in loaded_apps.items():
  147. if entry_point not in set(app.entry_address for app in self._current_apps):
  148. new_app_state = AppState.from_gdb(app)
  149. print(f"New application loaded. Adding debug info")
  150. if self._exec_gdb_command(new_app_state.get_gdb_load_command()):
  151. self._current_apps.append(new_app_state)
  152. else:
  153. print(f"Failed to load debug info for {new_app_state}")
  154. def attach_to_fw(self) -> None:
  155. print("Attaching to Flipper firmware")
  156. self.app_list_ptr = gdb.lookup_global_symbol(
  157. "flipper_application_loaded_app_list"
  158. )
  159. self.app_type_ptr = gdb.lookup_type("FlipperApplication").pointer()
  160. self.app_list_entry_type = gdb.lookup_type("struct FlipperApplicationList_s")
  161. def handle_stop(self, event) -> None:
  162. self._sync_apps()
  163. def handle_exit(self, event) -> None:
  164. self.set_debug_mode(False)
  165. def set_debug_mode(self, mode: bool) -> None:
  166. try:
  167. gdb.execute(f"set variable furi_hal_debug_gdb_session_active = {int(mode)}")
  168. except gdb.error as e:
  169. print(f"Failed to set debug mode: {e}")
  170. # Init additional 'fap-set-debug-elf-root' command and set up hooks
  171. SetFapDebugElfRoot()
  172. helper = FlipperAppStateHelper()
  173. print("Support for Flipper external apps debug is loaded")