flipperapps.py 5.4 KB

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