[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 <gui/gui.h>
#include <assets_icons.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <storage/storage.h>
#include <gui/modules/loading.h>
#include <dialogs/dialogs.h>
#include <toolbox/path.h>
#include <flipper_application/flipper_application.h>
#include <loader/firmware_api/firmware_api.h>
#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));

View File

@ -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("<I", section_data[-4:])[0]
return (elf_name, crc32)
@staticmethod
def from_gdb(gdb_app: "AppState") -> "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")

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

View File

@ -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++;
}
}

View File

@ -3,7 +3,9 @@
#include <notification/notification_messages.h>
#include "application_assets.h"
#define TAG "fapp"
#include <m-list.h>
#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;