flipperapps.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. from dataclasses import dataclass
  2. from typing import Optional, Tuple, Dict, ClassVar
  3. import struct
  4. import posixpath
  5. import os
  6. import zlib
  7. import gdb
  8. def get_file_crc32(filename):
  9. with open(filename, "rb") as f:
  10. return zlib.crc32(f.read())
  11. @dataclass
  12. class AppState:
  13. name: str
  14. text_address: int = 0
  15. entry_address: int = 0
  16. other_sections: Dict[str, int] = None
  17. debug_link_elf: str = ""
  18. debug_link_crc: int = 0
  19. DEBUG_ELF_ROOT: ClassVar[Optional[str]] = None
  20. def __post_init__(self):
  21. if self.other_sections is None:
  22. self.other_sections = {}
  23. def get_original_elf_path(self) -> str:
  24. if self.DEBUG_ELF_ROOT is None:
  25. raise ValueError("DEBUG_ELF_ROOT not set; call fap-set-debug-elf-root")
  26. return (
  27. posixpath.join(self.DEBUG_ELF_ROOT, self.debug_link_elf)
  28. if self.DEBUG_ELF_ROOT
  29. else self.debug_link_elf
  30. )
  31. def is_debug_available(self) -> bool:
  32. have_debug_info = bool(self.debug_link_elf and self.debug_link_crc)
  33. if not have_debug_info:
  34. print("No debug info available for this app")
  35. return False
  36. debug_elf_path = self.get_original_elf_path()
  37. debug_elf_crc32 = get_file_crc32(debug_elf_path)
  38. if self.debug_link_crc != debug_elf_crc32:
  39. print(
  40. f"Debug info ({debug_elf_path}) CRC mismatch: {self.debug_link_crc:08x} != {debug_elf_crc32:08x}, rebuild app"
  41. )
  42. return False
  43. return True
  44. def get_gdb_load_command(self) -> str:
  45. load_path = self.get_original_elf_path()
  46. print(f"Loading debug information from {load_path}")
  47. load_command = (
  48. f"add-symbol-file -readnow {load_path} 0x{self.text_address:08x} "
  49. )
  50. load_command += " ".join(
  51. f"-s {name} 0x{address:08x}"
  52. for name, address in self.other_sections.items()
  53. )
  54. return load_command
  55. def get_gdb_unload_command(self) -> str:
  56. return f"remove-symbol-file -a 0x{self.text_address:08x}"
  57. def is_loaded_in_gdb(self, gdb_app) -> bool:
  58. # Avoid constructing full app wrapper for comparison
  59. return self.entry_address == int(gdb_app["state"]["entry"])
  60. @staticmethod
  61. def parse_debug_link_data(section_data: bytes) -> Tuple[str, int]:
  62. # Debug link format: a null-terminated string with debuggable file name
  63. # Padded with 0's to multiple of 4 bytes
  64. # Followed by 4 bytes of CRC32 checksum of that file
  65. elf_name = section_data[:-4].decode("utf-8").split("\x00")[0]
  66. crc32 = struct.unpack("<I", section_data[-4:])[0]
  67. return (elf_name, crc32)
  68. @staticmethod
  69. def from_gdb(gdb_app: "AppState") -> "AppState":
  70. state = AppState(str(gdb_app["manifest"]["name"].string()))
  71. state.entry_address = int(gdb_app["state"]["entry"])
  72. app_state = gdb_app["state"]
  73. if debug_link_size := int(app_state["debug_link_info"]["debug_link_size"]):
  74. debug_link_data = (
  75. gdb.selected_inferior()
  76. .read_memory(
  77. int(app_state["debug_link_info"]["debug_link"]), debug_link_size
  78. )
  79. .tobytes()
  80. )
  81. state.debug_link_elf, state.debug_link_crc = AppState.parse_debug_link_data(
  82. debug_link_data
  83. )
  84. for idx in range(app_state["mmap_entry_count"]):
  85. mmap_entry = app_state["mmap_entries"][idx]
  86. section_name = mmap_entry["name"].string()
  87. section_addr = int(mmap_entry["address"])
  88. if section_name == ".text":
  89. state.text_address = section_addr
  90. else:
  91. state.other_sections[section_name] = section_addr
  92. return state
  93. class SetFapDebugElfRoot(gdb.Command):
  94. """Set path to original ELF files for debug info"""
  95. def __init__(self):
  96. super().__init__(
  97. "fap-set-debug-elf-root", gdb.COMMAND_FILES, gdb.COMPLETE_FILENAME
  98. )
  99. self.dont_repeat()
  100. def invoke(self, arg, from_tty):
  101. AppState.DEBUG_ELF_ROOT = arg
  102. try:
  103. global helper
  104. print(f"Set '{arg}' as debug info lookup path for Flipper external apps")
  105. helper.attach_fw()
  106. gdb.events.stop.connect(helper.handle_stop)
  107. except gdb.error as e:
  108. print(f"Support for Flipper external apps debug is not available: {e}")
  109. SetFapDebugElfRoot()
  110. class FlipperAppDebugHelper:
  111. def __init__(self):
  112. self.app_ptr = None
  113. self.app_type_ptr = None
  114. self.current_app: AppState = None
  115. def attach_fw(self) -> None:
  116. self.app_ptr = gdb.lookup_global_symbol("last_loaded_app")
  117. self.app_type_ptr = gdb.lookup_type("FlipperApplication").pointer()
  118. self._check_app_state()
  119. def _check_app_state(self) -> None:
  120. app_ptr_value = self.app_ptr.value()
  121. if not app_ptr_value and self.current_app:
  122. # There is an ELF loaded in GDB, but nothing is running on the device
  123. self._unload_debug_elf()
  124. elif app_ptr_value:
  125. # There is an app running on the device
  126. loaded_app = app_ptr_value.cast(self.app_type_ptr).dereference()
  127. if self.current_app and not self.current_app.is_loaded_in_gdb(loaded_app):
  128. # Currently loaded ELF is not the one running on the device
  129. self._unload_debug_elf()
  130. if not self.current_app:
  131. # Load ELF for the app running on the device
  132. self._load_debug_elf(loaded_app)
  133. def _unload_debug_elf(self) -> None:
  134. try:
  135. gdb.execute(self.current_app.get_gdb_unload_command())
  136. except gdb.error as e:
  137. print(f"Failed to unload debug ELF: {e} (might not be an error)")
  138. self.current_app = None
  139. def _load_debug_elf(self, app_object) -> None:
  140. self.current_app = AppState.from_gdb(app_object)
  141. if self.current_app.is_debug_available():
  142. gdb.execute(self.current_app.get_gdb_load_command())
  143. def handle_stop(self, event) -> None:
  144. self._check_app_state()
  145. helper = FlipperAppDebugHelper()
  146. print("Support for Flipper external apps debug is loaded")