[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
This commit is contained in:
hedger 2023-03-20 19:03:55 +04:00 committed by GitHub
parent f7024cff78
commit 60ac2e9881
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 59 deletions

View File

@ -1,16 +1,17 @@
#include "fap_loader_app.h"
#include <furi.h> #include <furi.h>
#include <gui/gui.h>
#include <assets_icons.h> #include <assets_icons.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h> #include <gui/view_dispatcher.h>
#include <storage/storage.h>
#include <gui/modules/loading.h> #include <gui/modules/loading.h>
#include <dialogs/dialogs.h> #include <dialogs/dialogs.h>
#include <toolbox/path.h> #include <toolbox/path.h>
#include <flipper_application/flipper_application.h> #include <flipper_application/flipper_application.h>
#include <loader/firmware_api/firmware_api.h> #include <loader/firmware_api/firmware_api.h>
#include "fap_loader_app.h"
#define TAG "fap_loader_app" #define TAG "FapLoader"
struct FapLoader { struct FapLoader {
FlipperApplication* app; FlipperApplication* app;
@ -22,6 +23,8 @@ struct FapLoader {
Loading* loading; Loading* loading;
}; };
volatile bool fap_loader_debug_active = false;
bool fap_loader_load_name_and_icon( bool fap_loader_load_name_and_icon(
FuriString* path, FuriString* path,
Storage* storage, Storage* storage,
@ -107,6 +110,14 @@ static bool fap_loader_run_selected_app(FapLoader* loader) {
FuriThread* thread = flipper_application_spawn(loader->app, NULL); 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(); FuriString* app_name = furi_string_alloc();
path_extract_filename_no_ext(furi_string_get_cstr(loader->fap_path), app_name); 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)); furi_thread_set_appid(thread, furi_string_get_cstr(app_name));

View File

@ -2,7 +2,6 @@ from dataclasses import dataclass
from typing import Optional, Tuple, Dict, ClassVar from typing import Optional, Tuple, Dict, ClassVar
import struct import struct
import posixpath import posixpath
import os
import zlib import zlib
import gdb import gdb
@ -66,9 +65,9 @@ class AppState:
def get_gdb_unload_command(self) -> str: def get_gdb_unload_command(self) -> str:
return f"remove-symbol-file -a 0x{self.text_address:08x}" return f"remove-symbol-file -a 0x{self.text_address:08x}"
def is_loaded_in_gdb(self, gdb_app) -> bool: @staticmethod
# Avoid constructing full app wrapper for comparison def get_gdb_app_ep(app) -> int:
return self.entry_address == int(gdb_app["state"]["entry"]) return int(app["state"]["entry"])
@staticmethod @staticmethod
def parse_debug_link_data(section_data: bytes) -> Tuple[str, int]: def parse_debug_link_data(section_data: bytes) -> Tuple[str, int]:
@ -79,10 +78,10 @@ class AppState:
crc32 = struct.unpack("<I", section_data[-4:])[0] crc32 = struct.unpack("<I", section_data[-4:])[0]
return (elf_name, crc32) return (elf_name, crc32)
@staticmethod @classmethod
def from_gdb(gdb_app: "AppState") -> "AppState": def from_gdb(cls, gdb_app: "AppState") -> "AppState":
state = AppState(str(gdb_app["manifest"]["name"].string())) 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"] app_state = gdb_app["state"]
if debug_link_size := int(app_state["debug_link_info"]["debug_link_size"]): if debug_link_size := int(app_state["debug_link_info"]["debug_link_size"]):
@ -123,59 +122,83 @@ class SetFapDebugElfRoot(gdb.Command):
try: try:
global helper global helper
print(f"Set '{arg}' as debug info lookup path for Flipper external apps") 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.stop.connect(helper.handle_stop)
gdb.events.exited.connect(helper.handle_exit)
except gdb.error as e: except gdb.error as e:
print(f"Support for Flipper external apps debug is not available: {e}") print(f"Support for Flipper external apps debug is not available: {e}")
SetFapDebugElfRoot() class FlipperAppStateHelper:
class FlipperAppDebugHelper:
def __init__(self): def __init__(self):
self.app_ptr = None
self.app_type_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: def _walk_app_list(self, list_head):
self.app_ptr = gdb.lookup_global_symbol("last_loaded_app") while list_head:
self.app_type_ptr = gdb.lookup_type("FlipperApplication").pointer() if app := list_head["data"]:
self._check_app_state() yield app.dereference()
list_head = list_head["next"]
def _check_app_state(self) -> None: def _exec_gdb_command(self, command: str) -> bool:
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:
try: try:
gdb.execute(self.current_app.get_gdb_unload_command()) gdb.execute(command)
return True
except gdb.error as e: except gdb.error as e:
print(f"Failed to unload debug ELF: {e} (might not be an error)") print(f"Failed to execute GDB command '{command}': {e}")
self.current_app = None return False
def _load_debug_elf(self, app_object) -> None: def _sync_apps(self) -> None:
self.current_app = AppState.from_gdb(app_object) 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(): loaded_apps: dict[int, gdb.Value] = dict(
gdb.execute(self.current_app.get_gdb_load_command()) (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: 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") print("Support for Flipper external apps debug is loaded")

View File

@ -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. 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 ### 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. 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.

View File

@ -830,8 +830,9 @@ void elf_file_init_debug_info(ELFFile* elf, ELFDebugInfo* debug_info) {
const void* data_ptr = itref->value.data; const void* data_ptr = itref->value.data;
if(data_ptr) { if(data_ptr) {
debug_info->mmap_entries[mmap_entry_idx].address = (uint32_t)data_ptr; ELFMemoryMapEntry* entry = &debug_info->mmap_entries[mmap_entry_idx];
debug_info->mmap_entries[mmap_entry_idx].name = itref->key; entry->address = (uint32_t)data_ptr;
entry->name = itref->key;
mmap_entry_idx++; mmap_entry_idx++;
} }
} }

View File

@ -3,7 +3,9 @@
#include <notification/notification_messages.h> #include <notification/notification_messages.h>
#include "application_assets.h" #include "application_assets.h"
#define TAG "fapp" #include <m-list.h>
#define TAG "Fap"
struct FlipperApplication { struct FlipperApplication {
ELFDebugInfo state; ELFDebugInfo state;
@ -13,8 +15,39 @@ struct FlipperApplication {
void* ep_thread_args; void* ep_thread_args;
}; };
/* For debugger access to app state */ /********************** Debugger access to loader state **********************/
FlipperApplication* last_loaded_app = NULL;
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* FlipperApplication*
flipper_application_alloc(Storage* storage, const ElfApiInterface* api_interface) { flipper_application_alloc(Storage* storage, const ElfApiInterface* api_interface) {
@ -37,8 +70,8 @@ void flipper_application_free(FlipperApplication* app) {
furi_thread_free(app->thread); furi_thread_free(app->thread);
} }
if(!flipper_application_is_plugin(app)) { if(app->state.entry) {
last_loaded_app = NULL; flipper_application_list_remove_app(app);
} }
elf_file_clear_debug_info(&app->state); 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) { 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); ELFFileLoadStatus status = elf_file_load_sections(app->elf);
switch(status) { switch(status) {
case ELFFileLoadStatusSuccess: case ELFFileLoadStatusSuccess:
elf_file_init_debug_info(app->elf, &app->state); elf_file_init_debug_info(app->elf, &app->state);
flipper_application_list_add_app(app);
return FlipperApplicationLoadStatusSuccess; return FlipperApplicationLoadStatusSuccess;
case ELFFileLoadStatusNoFreeMemory: case ELFFileLoadStatusNoFreeMemory:
return FlipperApplicationLoadStatusNoFreeMemory; return FlipperApplicationLoadStatusNoFreeMemory;