From 60ac2e98810fa9638f9955ea7fa6a553d1cedaa9 Mon Sep 17 00:00:00 2001 From: hedger Date: Mon, 20 Mar 2023 19:03:55 +0400 Subject: [PATCH] [FL-3161] Improved debugging experience for external apps (#2507) * debug: automated support for multiple debug symbol files * faploader: extra checks for app list state * debug: trigger BP before fap's EP if under debugger * faploader, debug: better naming * docs: info on load breakpoint * faploader: header cleanup * faploader: naming fixes * debug: less verbose; setting debug flag more often * typo fix --- applications/main/fap_loader/fap_loader_app.c | 19 ++- debug/flipperapps.py | 113 +++++++++++------- documentation/AppsOnSDCard.md | 2 + lib/flipper_application/elf/elf_file.c | 5 +- lib/flipper_application/flipper_application.c | 47 ++++++-- 5 files changed, 127 insertions(+), 59 deletions(-) diff --git a/applications/main/fap_loader/fap_loader_app.c b/applications/main/fap_loader/fap_loader_app.c index dcbad8e1..f5c7af02 100644 --- a/applications/main/fap_loader/fap_loader_app.c +++ b/applications/main/fap_loader/fap_loader_app.c @@ -1,16 +1,17 @@ +#include "fap_loader_app.h" + #include -#include + #include +#include #include -#include #include #include #include #include #include -#include "fap_loader_app.h" -#define TAG "fap_loader_app" +#define TAG "FapLoader" struct FapLoader { FlipperApplication* app; @@ -22,6 +23,8 @@ struct FapLoader { Loading* loading; }; +volatile bool fap_loader_debug_active = false; + bool fap_loader_load_name_and_icon( FuriString* path, Storage* storage, @@ -107,6 +110,14 @@ static bool fap_loader_run_selected_app(FapLoader* loader) { FuriThread* thread = flipper_application_spawn(loader->app, NULL); + /* This flag is set by the debugger - to break on app start */ + if(fap_loader_debug_active) { + FURI_LOG_W(TAG, "Triggering BP for debugger"); + /* After hitting this, you can set breakpoints in your .fap's code + * Note that you have to toggle breakpoints that were set before */ + __asm volatile("bkpt 0"); + } + FuriString* app_name = furi_string_alloc(); path_extract_filename_no_ext(furi_string_get_cstr(loader->fap_path), app_name); furi_thread_set_appid(thread, furi_string_get_cstr(app_name)); diff --git a/debug/flipperapps.py b/debug/flipperapps.py index e815e40b..1dc5ebd0 100644 --- a/debug/flipperapps.py +++ b/debug/flipperapps.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from typing import Optional, Tuple, Dict, ClassVar import struct import posixpath -import os import zlib import gdb @@ -66,9 +65,9 @@ class AppState: def get_gdb_unload_command(self) -> str: return f"remove-symbol-file -a 0x{self.text_address:08x}" - def is_loaded_in_gdb(self, gdb_app) -> bool: - # Avoid constructing full app wrapper for comparison - return self.entry_address == int(gdb_app["state"]["entry"]) + @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]: @@ -79,10 +78,10 @@ class AppState: crc32 = struct.unpack(" "AppState": + @classmethod + def from_gdb(cls, gdb_app: "AppState") -> "AppState": state = AppState(str(gdb_app["manifest"]["name"].string())) - state.entry_address = int(gdb_app["state"]["entry"]) + 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"]): @@ -123,59 +122,83 @@ class SetFapDebugElfRoot(gdb.Command): try: global helper print(f"Set '{arg}' as debug info lookup path for Flipper external apps") - helper.attach_fw() + helper.attach_to_fw() gdb.events.stop.connect(helper.handle_stop) + gdb.events.exited.connect(helper.handle_exit) except gdb.error as e: print(f"Support for Flipper external apps debug is not available: {e}") -SetFapDebugElfRoot() - - -class FlipperAppDebugHelper: +class FlipperAppStateHelper: def __init__(self): - self.app_ptr = None self.app_type_ptr = None - self.current_app: AppState = None + self.app_list_ptr = None + self.app_list_entry_type = None + self._current_apps: list[AppState] = [] - def attach_fw(self) -> None: - self.app_ptr = gdb.lookup_global_symbol("last_loaded_app") - self.app_type_ptr = gdb.lookup_type("FlipperApplication").pointer() - self._check_app_state() + def _walk_app_list(self, list_head): + while list_head: + if app := list_head["data"]: + yield app.dereference() + list_head = list_head["next"] - def _check_app_state(self) -> None: - app_ptr_value = self.app_ptr.value() - if not app_ptr_value and self.current_app: - # There is an ELF loaded in GDB, but nothing is running on the device - self._unload_debug_elf() - elif app_ptr_value: - # There is an app running on the device - loaded_app = app_ptr_value.cast(self.app_type_ptr).dereference() - - if self.current_app and not self.current_app.is_loaded_in_gdb(loaded_app): - # Currently loaded ELF is not the one running on the device - self._unload_debug_elf() - - if not self.current_app: - # Load ELF for the app running on the device - self._load_debug_elf(loaded_app) - - def _unload_debug_elf(self) -> None: + def _exec_gdb_command(self, command: str) -> bool: try: - gdb.execute(self.current_app.get_gdb_unload_command()) + gdb.execute(command) + return True except gdb.error as e: - print(f"Failed to unload debug ELF: {e} (might not be an error)") - self.current_app = None + print(f"Failed to execute GDB command '{command}': {e}") + return False - def _load_debug_elf(self, app_object) -> None: - self.current_app = AppState.from_gdb(app_object) + 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 - if self.current_app.is_debug_available(): - gdb.execute(self.current_app.get_gdb_load_command()) + 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") def handle_stop(self, event) -> None: - self._check_app_state() + self._sync_apps() + + def handle_exit(self, event) -> None: + self.set_debug_mode(False) + + def set_debug_mode(self, mode: bool) -> None: + gdb.execute(f"set variable fap_loader_debug_active = {int(mode)}") -helper = FlipperAppDebugHelper() +# Init additional 'fap-set-debug-elf-root' command and set up hooks +SetFapDebugElfRoot() +helper = FlipperAppStateHelper() print("Support for Flipper external apps debug is loaded") diff --git a/documentation/AppsOnSDCard.md b/documentation/AppsOnSDCard.md index 9ab7e9b2..75430570 100644 --- a/documentation/AppsOnSDCard.md +++ b/documentation/AppsOnSDCard.md @@ -32,6 +32,8 @@ Images and animated icons should follow the same [naming convention](../assets/R With it, you can debug FAPs as if they were a part of the main firmware — inspect variables, set breakpoints, step through the code, etc. +If debugging session is active, firmware will trigger a breakpoint after loading a FAP it into memory, but before running any code from it. This allows you to set breakpoints in the FAP's code. Note that any breakpoints set before the FAP is loaded may need re-setting after the FAP is actually loaded, since before loading it debugger cannot know the exact address of the FAP's code. + ### Setting up debugging environment The debugging support script looks up debugging information in the latest firmware build directory (`build/latest`). That directory is symlinked by `fbt` to the latest firmware configuration (Debug or Release) build directory when you run `./fbt` for the chosen configuration. See [fbt docs](./fbt.md#nb) for details. diff --git a/lib/flipper_application/elf/elf_file.c b/lib/flipper_application/elf/elf_file.c index 146afccb..0338144a 100644 --- a/lib/flipper_application/elf/elf_file.c +++ b/lib/flipper_application/elf/elf_file.c @@ -830,8 +830,9 @@ void elf_file_init_debug_info(ELFFile* elf, ELFDebugInfo* debug_info) { const void* data_ptr = itref->value.data; if(data_ptr) { - debug_info->mmap_entries[mmap_entry_idx].address = (uint32_t)data_ptr; - debug_info->mmap_entries[mmap_entry_idx].name = itref->key; + ELFMemoryMapEntry* entry = &debug_info->mmap_entries[mmap_entry_idx]; + entry->address = (uint32_t)data_ptr; + entry->name = itref->key; mmap_entry_idx++; } } diff --git a/lib/flipper_application/flipper_application.c b/lib/flipper_application/flipper_application.c index ca917cf1..1b4f5681 100644 --- a/lib/flipper_application/flipper_application.c +++ b/lib/flipper_application/flipper_application.c @@ -3,7 +3,9 @@ #include #include "application_assets.h" -#define TAG "fapp" +#include + +#define TAG "Fap" struct FlipperApplication { ELFDebugInfo state; @@ -13,8 +15,39 @@ struct FlipperApplication { void* ep_thread_args; }; -/* For debugger access to app state */ -FlipperApplication* last_loaded_app = NULL; +/********************** Debugger access to loader state **********************/ + +LIST_DEF(FlipperApplicationList, const FlipperApplication*, M_POD_OPLIST); + +FlipperApplicationList_t flipper_application_loaded_app_list = {0}; +static bool flipper_application_loaded_app_list_initialized = false; + +static void flipper_application_list_add_app(const FlipperApplication* app) { + furi_assert(app); + + if(!flipper_application_loaded_app_list_initialized) { + FlipperApplicationList_init(flipper_application_loaded_app_list); + flipper_application_loaded_app_list_initialized = true; + } + FlipperApplicationList_push_back(flipper_application_loaded_app_list, app); +} + +static void flipper_application_list_remove_app(const FlipperApplication* app) { + furi_assert(flipper_application_loaded_app_list_initialized); + furi_assert(app); + + FlipperApplicationList_it_t it; + for(FlipperApplicationList_it(it, flipper_application_loaded_app_list); + !FlipperApplicationList_end_p(it); + FlipperApplicationList_next(it)) { + if(*FlipperApplicationList_ref(it) == app) { + FlipperApplicationList_remove(flipper_application_loaded_app_list, it); + break; + } + } +} + +/*****************************************************************************/ FlipperApplication* flipper_application_alloc(Storage* storage, const ElfApiInterface* api_interface) { @@ -37,8 +70,8 @@ void flipper_application_free(FlipperApplication* app) { furi_thread_free(app->thread); } - if(!flipper_application_is_plugin(app)) { - last_loaded_app = NULL; + if(app->state.entry) { + flipper_application_list_remove_app(app); } elf_file_clear_debug_info(&app->state); @@ -153,14 +186,12 @@ const FlipperApplicationManifest* flipper_application_get_manifest(FlipperApplic } FlipperApplicationLoadStatus flipper_application_map_to_memory(FlipperApplication* app) { - if(!flipper_application_is_plugin(app)) { - last_loaded_app = app; - } ELFFileLoadStatus status = elf_file_load_sections(app->elf); switch(status) { case ELFFileLoadStatusSuccess: elf_file_init_debug_info(app->elf, &app->state); + flipper_application_list_add_app(app); return FlipperApplicationLoadStatusSuccess; case ELFFileLoadStatusNoFreeMemory: return FlipperApplicationLoadStatusNoFreeMemory;