diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e21ee607cca..190d4c5ffb02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,13 @@ if(CONFIG_ESP_SYSTEM_USE_EH_FRAME) list(APPEND link_options "-Wl,--eh-frame-hdr") endif() +if(CONFIG_ESP_SYSTEM_USE_FRAME_POINTER) + list(APPEND compile_options "-fno-omit-frame-pointer") + if(CMAKE_C_COMPILER_ID MATCHES "GNU") + list(APPEND compile_options "-mno-omit-leaf-frame-pointer") + endif() +endif() + list(APPEND link_options "-fno-lto") if(CONFIG_IDF_TARGET_LINUX AND CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") diff --git a/components/esp_system/CMakeLists.txt b/components/esp_system/CMakeLists.txt index b99131ad99ab..af54c5d2a60c 100644 --- a/components/esp_system/CMakeLists.txt +++ b/components/esp_system/CMakeLists.txt @@ -56,6 +56,10 @@ else() list(APPEND srcs "eh_frame_parser.c") endif() + if(CONFIG_ESP_SYSTEM_USE_FRAME_POINTER) + list(APPEND srcs "fp_unwind.c") + endif() + if(CONFIG_SOC_SYSTIMER_SUPPORT_ETM) list(APPEND srcs "systick_etm.c") endif() diff --git a/components/esp_system/Kconfig b/components/esp_system/Kconfig index 282eade65fcb..efacb07d35df 100644 --- a/components/esp_system/Kconfig +++ b/components/esp_system/Kconfig @@ -105,17 +105,38 @@ menu "ESP System Settings" similar to that of DRAM region but without DMA. Speed wise RTC fast memory operates on APB clock and hence does not have much performance impact. - config ESP_SYSTEM_USE_EH_FRAME - bool "Generate and use eh_frame for backtracing" - default n + choice ESP_BACKTRACING_METHOD + prompt "Backtracing method" + default ESP_SYSTEM_NO_BACKTRACE depends on IDF_TARGET_ARCH_RISCV help - Generate DWARF information for each function of the project. These information will parsed and used to - perform backtracing when panics occur. Activating this option will activate asynchronous frame unwinding - and generation of both .eh_frame and .eh_frame_hdr sections, resulting in a bigger binary size (20% to - 100% larger). The main purpose of this option is to be able to have a backtrace parsed and printed by - the program itself, regardless of the serial monitor used. - This option shall NOT be used for production. + Configure how backtracing will be performed at runtime when a panic occurs. + + config ESP_SYSTEM_NO_BACKTRACE + bool "No backtracing" + help + When selected, no backtracing will be performed at runtime. By using idf.py monitor, it + is still possible to get a backtrace when a panic occurs. + + config ESP_SYSTEM_USE_EH_FRAME + bool "Generate and use eh_frame for backtracing" + help + Generate DWARF information for each function of the project. These information will parsed and used to + perform backtracing when panics occur. Activating this option will activate asynchronous frame + unwinding and generation of both .eh_frame and .eh_frame_hdr sections, resulting in a bigger binary + size (20% to 100% larger). The main purpose of this option is to be able to have a backtrace parsed + and printed by the program itself, regardless of the serial monitor used. + This option shall NOT be used for production. + + config ESP_SYSTEM_USE_FRAME_POINTER + bool "Use CPU Frame Pointer register" + help + This configuration allows the compiler to allocate CPU register s0 as the frame pointer. The main usage + of the frame pointer is to be able to generate a backtrace from the panic handler on exception. + Enabling this option results in bigger code and slower code since all functions will have to populate + this register and won't be able to use it as a general-purpose register anymore. + + endchoice menu "Memory protection" diff --git a/components/esp_system/fp_unwind.c b/components/esp_system/fp_unwind.c new file mode 100644 index 000000000000..78bcb0c2b290 --- /dev/null +++ b/components/esp_system/fp_unwind.c @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "sdkconfig.h" +#include +#include "esp_private/panic_internal.h" +#include "esp_memory_utils.h" +#include "riscv/libunwind-riscv.h" + +#define FP_MAX_CALLERS 64 +#define RA_INDEX_IN_FP -1 +#define SP_INDEX_IN_FP -2 + +/** + * @brief When one step of the backtrace is generated, output it to the serial. + * This function can be overridden as it is defined as weak. + * + * @param pc Program counter of the backtrace step. + * @param sp Stack pointer of the backtrace step. + */ +void __attribute__((weak)) esp_eh_frame_generated_step(uint32_t pc, uint32_t sp) +{ + panic_print_str(" 0x"); + panic_print_hex(pc); + panic_print_str(":0x"); + panic_print_hex(sp); +} + +/** + * @brief Check if the given pointer is a data pointer that can be dereferenced + * + * @param ptr Data pointer to check + */ +static inline bool esp_fp_ptr_is_data(void* ptr) +{ + return esp_ptr_in_dram(ptr) || esp_ptr_external_ram(ptr); +} + +/** + * @brief Generate a call stack starting at the given frame pointer. + * + * @note Since this function can be called from RAM (from heap trace), it shall also be put in RAM. + * + * @param frame[in] Frame pointer to start unrolling from. + * @param callers[out] Array of callers where 0 will store the most recent caller. Can be NULL. + * @param stacks[out] Array of callers' stacks where 0 will store the most recent caller. Can be NULL. + * @param depth[in] Number of maximum entries to fill in the callers array. + * + * @returns Number of entries filled in the array. + */ +uint32_t IRAM_ATTR esp_fp_get_callers(uint32_t frame, void** callers, void** stacks, uint32_t depth) +{ + uint32_t written = 0; + uint32_t pc = 0; + uint32_t sp = 0; + uint32_t* fp = (uint32_t*) frame; + + if (depth == 0 || (callers == NULL && stacks == NULL)) { + return 0; + } + + /** + * We continue filling the callers array until we meet a function in ROM (not compiled + * with frame pointer) or the pointer we retrieved is not in RAM (binary libraries + * not compiled with frame pointer configuration) + */ + while (depth > 0 && esp_fp_ptr_is_data(fp) && !esp_ptr_in_rom((void *) pc)) { + /* Dereference the RA register from the frame pointer and store it in PC */ + pc = fp[RA_INDEX_IN_FP]; + sp = fp[SP_INDEX_IN_FP]; + fp = (uint32_t*) sp; + + if (callers) { + callers[written] = (void*) pc; + } + if (stacks) { + stacks[written] = (void*) sp; + } + + depth--; + written++; + } + + return written; +} + +/** + * @brief Print backtrace for the given execution frame. + * + * @param frame_or Snapshot of the CPU registers when the CPU stopped its normal execution. + */ +void esp_fp_print_backtrace(const void *frame_or) +{ + assert(frame_or != NULL); + ExecutionFrame frame = *((ExecutionFrame*) frame_or); + + const uint32_t sp = EXECUTION_FRAME_SP(frame); + const uint32_t pc = EXECUTION_FRAME_PC(frame); + const uint32_t fp = frame.s0; + void* callers[FP_MAX_CALLERS]; + void* stacks[FP_MAX_CALLERS]; + + panic_print_str("Backtrace:"); + esp_eh_frame_generated_step(pc, sp); + + uint32_t len = esp_fp_get_callers(fp, callers, stacks, FP_MAX_CALLERS); + + for (int i = 0; i < len; i++) { + esp_eh_frame_generated_step((uint32_t) callers[i], (uint32_t) stacks[i]); + } + + panic_print_str("\r\n"); +} diff --git a/components/esp_system/port/arch/riscv/panic_arch.c b/components/esp_system/port/arch/riscv/panic_arch.c index 1f4dd57b5405..f49b3fd2349f 100644 --- a/components/esp_system/port/arch/riscv/panic_arch.c +++ b/components/esp_system/port/arch/riscv/panic_arch.c @@ -305,6 +305,7 @@ void panic_arch_fill_info(void *frame, panic_info_t *info) info->addr = (void *) regs->mepc; } +#if !CONFIG_ESP_SYSTEM_USE_FRAME_POINTER static void panic_print_basic_backtrace(const void *frame, int core) { // Basic backtrace @@ -322,6 +323,7 @@ static void panic_print_basic_backtrace(const void *frame, int core) } } } +#endif void panic_print_backtrace(const void *frame, int core) { @@ -333,6 +335,9 @@ void panic_print_backtrace(const void *frame, int core) } else { esp_eh_frame_print_backtrace(frame); } +#elif CONFIG_ESP_SYSTEM_USE_FRAME_POINTER + extern void esp_fp_print_backtrace(const void*); + esp_fp_print_backtrace(frame); #else panic_print_basic_backtrace(frame, core); #endif diff --git a/docs/en/api-guides/fatal-errors.rst b/docs/en/api-guides/fatal-errors.rst index 49228163a866..b9920108fa2c 100644 --- a/docs/en/api-guides/fatal-errors.rst +++ b/docs/en/api-guides/fatal-errors.rst @@ -234,7 +234,7 @@ If :doc:`IDF Monitor ` is used, Program Counter values will b #5 0x00000000 in ?? () Backtrace stopped: frame did not save the PC - While the backtrace above is very handy, it requires the user to use :doc:`IDF Monitor `. Thus, in order to generate and print a backtrace while using another monitor program, it is possible to activate :ref:`CONFIG_ESP_SYSTEM_USE_EH_FRAME` option from the menuconfig. + While the backtrace above is very handy, it requires the user to use :doc:`IDF Monitor `. Thus, in order to generate and print a backtrace while using another monitor program, it is possible to activate ``CONFIG_ESP_SYSTEM_USE_EH_FRAME`` option from the menuconfig, under the "Backtracing method" menu. This option will let the compiler generate DWARF information for each function of the project. Then, when a CPU exception occurs, the panic handler will parse these data and determine the backtrace of the task that failed. The output looks like this: @@ -245,7 +245,13 @@ If :doc:`IDF Monitor ` is used, Program Counter values will b These ``PC:SP`` pairs represent the PC (Program Counter) and SP (Stack Pointer) for each stack frame of the current task. - The main benefit of the :ref:`CONFIG_ESP_SYSTEM_USE_EH_FRAME` option is that the backtrace is generated by the board itself (without the need for :doc:`IDF Monitor `). However, the option's drawback is that it results in an increase of the compiled binary's size (ranging from 20% to 100% increase in size). Furthermore, this option causes debug information to be included within the compiled binary. Therefore, users are strongly advised not to enable this option in mass/final production builds. + The main benefit of the ``CONFIG_ESP_SYSTEM_USE_EH_FRAME`` option is that the backtrace is generated by the board itself (without the need for :doc:`IDF Monitor `). However, the option's drawback is that it results in an increase of the compiled binary's size (ranging from 20% to 100% increase in size). Furthermore, this option causes debug information to be included within the compiled binary. Therefore, users are strongly advised not to enable this option in mass/final production builds. + + Another option to generate such backtrace on the device itself is to enable ``CONFIG_ESP_SYSTEM_USE_FRAME_POINTER`` option from the menuconfig, under the "Backtracing method" menu. + + This option will let the compiler reserve a CPU register that keeps track of the frame of each routine of the program. This registers makes it possible for the panic handler to unwind the call stack at any given time, and more importantly, when a CPU exception occurs. + + Enabling ``CONFIG_ESP_SYSTEM_USE_FRAME_POINTER`` option will result in an increase of the compiled binary's size of around +5-6% and a performance decrease of around 1%. Contrarily to the ``CONFIG_ESP_SYSTEM_USE_EH_FRAME`` option, the compiler won't generate debug information in the generated binary, so it is possible to use this feature in mass/final production builds. To find the location where a fatal error has happened, look at the lines which follow the "Backtrace" line. Fatal error location is the top line, and subsequent lines show the call stack. diff --git a/docs/zh_CN/api-guides/fatal-errors.rst b/docs/zh_CN/api-guides/fatal-errors.rst index 7cab6e65dfb9..2ba0b7fbc9b4 100644 --- a/docs/zh_CN/api-guides/fatal-errors.rst +++ b/docs/zh_CN/api-guides/fatal-errors.rst @@ -234,7 +234,7 @@ #5 0x00000000 in ?? () Backtrace stopped: frame did not save the PC - 虽然以上的回溯信息非常方便,但要求用户使用 :doc:`IDF 监视器 `。因此,如果用户希望使用其它的串口监控软件也能显示堆栈回溯信息,则需要在 menuconfig 中启用 :ref:`CONFIG_ESP_SYSTEM_USE_EH_FRAME` 选项。 + 虽然以上的回溯信息非常方便,但要求用户使用 :doc:`IDF 监视器 `。因此,如果用户希望使用其它的串口监控软件也能显示堆栈回溯信息,则需要在 menuconfig 中启用 ``CONFIG_ESP_SYSTEM_USE_EH_FRAME`` 选项。 该选项会让编译器为项目的每个函数生成 DWARF 信息。然后,当 CPU 异常发生时,紧急处理程序将解析这些数据并生成出错任务的堆栈回溯信息。输出结果如下: @@ -245,7 +245,7 @@ 这些 ``PC:SP`` 对代表当前任务每一个栈帧的程序计数器值 (Program Counter) 和栈顶地址 (Stack Pointer)。 - :ref:`CONFIG_ESP_SYSTEM_USE_EH_FRAME` 选项的主要优点是,回溯信息可以由程序自己解析生成并打印(而不依靠 :doc:`tools/idf-monitor`)。但是该选项会导致编译后的二进制文件更大(增幅可达 20% 甚至 100%)。此外,该选项会将调试信息也保存在二进制文件里。因此,强烈不建议用户在量产/生产版本中启用该选项。 + ``CONFIG_ESP_SYSTEM_USE_EH_FRAME`` 选项的主要优点是,回溯信息可以由程序自己解析生成并打印(而不依靠 :doc:`tools/idf-monitor`)。但是该选项会导致编译后的二进制文件更大(增幅可达 20% 甚至 100%)。此外,该选项会将调试信息也保存在二进制文件里。因此,强烈不建议用户在量产/生产版本中启用该选项。 若要查找发生严重错误的代码位置,请查看 "Backtrace" 的后面几行,发生严重错误的代码显示在顶行,后续几行显示的是调用堆栈。 diff --git a/tools/test_apps/system/panic/main/include/test_panic.h b/tools/test_apps/system/panic/main/include/test_panic.h index e4cb6de8a0b5..9350857e3621 100644 --- a/tools/test_apps/system/panic/main/include/test_panic.h +++ b/tools/test_apps/system/panic/main/include/test_panic.h @@ -75,6 +75,8 @@ void test_setup_coredump_summary(void); void test_coredump_summary(void); #endif +void test_panic_print_backtrace(void); + #ifdef __cplusplus } #endif diff --git a/tools/test_apps/system/panic/main/test_app_main.c b/tools/test_apps/system/panic/main/test_app_main.c index a51d22f36005..7b63a3440f6b 100644 --- a/tools/test_apps/system/panic/main/test_app_main.c +++ b/tools/test_apps/system/panic/main/test_app_main.c @@ -119,6 +119,9 @@ void app_main(void) HANDLE_TEST(test_name, test_assert_cache_disabled); HANDLE_TEST(test_name, test_assert_cache_write_back_error_can_print_backtrace); HANDLE_TEST(test_name, test_tcb_corrupted); +#if CONFIG_ESP_SYSTEM_USE_FRAME_POINTER + HANDLE_TEST(test_name, test_panic_print_backtrace); +#endif #if CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH && CONFIG_ESP_COREDUMP_DATA_FORMAT_ELF HANDLE_TEST(test_name, test_setup_coredump_summary); HANDLE_TEST(test_name, test_coredump_summary); diff --git a/tools/test_apps/system/panic/main/test_panic.c b/tools/test_apps/system/panic/main/test_panic.c index 0c9c0d0f603b..260aeca3241b 100644 --- a/tools/test_apps/system/panic/main/test_panic.c +++ b/tools/test_apps/system/panic/main/test_panic.c @@ -344,3 +344,34 @@ void test_capture_dram(void) assert(0); } #endif + + +#if CONFIG_ESP_SYSTEM_USE_FRAME_POINTER + +static NOINLINE_ATTR void step3(void) { + printf("Step 3\n"); + /* For some reason, the compiler doesn't generate the proper sequence for `panic_abort` function: + * the `ra` register is not saved on the stack upon entry */ + abort(); +} + +static NOINLINE_ATTR void step2(void) { + step3(); + printf("Step 2\n"); +} + +static NOINLINE_ATTR void step1(void) { + step2(); + printf("Step 1\n"); +} + +/** + * @brief Create a stack trace of several functions that will be shown at runtime, + * The functions must not be inlined! + */ +void test_panic_print_backtrace(void) +{ + step1(); +} + +#endif diff --git a/tools/test_apps/system/panic/pytest_panic.py b/tools/test_apps/system/panic/pytest_panic.py index 9937d968def3..418bce471db1 100644 --- a/tools/test_apps/system/panic/pytest_panic.py +++ b/tools/test_apps/system/panic/pytest_panic.py @@ -59,6 +59,11 @@ pytest.param('panic', marks=TARGETS_ALL), ] +CONFIGS_BACKTRACE = [ + # One single-core target and one dual-core target is enough + pytest.param('framepointer', marks=[pytest.mark.esp32c3, pytest.mark.esp32p4]), +] + CONFIGS_DUAL_CORE = [ pytest.param('coredump_flash_bin_crc', marks=TARGETS_DUAL_CORE), pytest.param('coredump_flash_elf_sha', marks=TARGETS_DUAL_CORE), @@ -1090,3 +1095,22 @@ def test_tcb_corrupted(dut: PanicTestDut, target: str, config: str, test_func_na expected_backtrace=None, expected_coredump=coredump_pattern ) + + +@pytest.mark.parametrize('config', CONFIGS_BACKTRACE, indirect=True) +@pytest.mark.generic +def test_panic_print_backtrace(dut: PanicTestDut, config: str, test_func_name: str) -> None: + dut.run_test_func(test_func_name) + regex_pattern = rb'abort\(\) was called at PC [0-9xa-f]+ on core 0' + dut.expect(regex_pattern) + dut.expect_backtrace() + dut.expect_elf_sha256() + dut.expect_none(['Guru Meditation', 'Re-entered core dump']) + + coredump_pattern = re.compile(PANIC_ABORT_PREFIX + regex_pattern.decode('utf-8')) + common_test( + dut, + config, + expected_backtrace=None, + expected_coredump=[coredump_pattern] + ) diff --git a/tools/test_apps/system/panic/test_panic_util/panic_dut.py b/tools/test_apps/system/panic/test_panic_util/panic_dut.py index ff8004cb8501..6e488b83669c 100644 --- a/tools/test_apps/system/panic/test_panic_util/panic_dut.py +++ b/tools/test_apps/system/panic/test_panic_util/panic_dut.py @@ -81,7 +81,6 @@ def expect_none(self, pattern, **kwargs) -> None: # type: ignore pass def expect_backtrace(self, corrupted: bool = False) -> None: - assert self.is_xtensa, 'Backtrace can be printed only on Xtensa' match = self.expect(r'Backtrace:( 0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+(?P \|<-CORRUPTED)?') if corrupted: assert match.group('corrupted')