From 037c079e9a86a54d95b0daa934cd2e8200a7d79b Mon Sep 17 00:00:00 2001 From: Darian Leung Date: Thu, 29 Nov 2018 17:06:21 +0800 Subject: [PATCH] esp32: Refactor backtrace and add esp_backtrace_print() This commit refactors backtracing within the panic handler so that a common function esp_backtrace_get_next_frame() is used iteratively to traverse a callstack. A esp_backtrace_print() function has also be added that allows the printing of a backtrace at runtime. The esp_backtrace_print() function allows unity to print the backtrace of failed test cases and jump back to the main test menu without the need reset the chip. esp_backtrace_print() can also be used as a debugging function by users. - esp_stack_ptr_is_sane() moved to soc_memory_layout.h - removed uncessary includes of "esp_debug_helpers.h" --- components/esp32/cpu_start.c | 1 - components/esp32/gdbstub.c | 2 +- components/esp32/panic.c | 42 +++++----- components/espcoredump/src/core_dump_port.c | 2 +- components/freertos/tasks.c | 1 - components/heap/test/test_malloc.c | 1 - components/soc/esp32/include/soc/cpu.h | 21 +++++ .../soc/include/soc/soc_memory_layout.h | 7 ++ components/unity/CMakeLists.txt | 4 + components/unity/Kconfig | 10 ++- components/unity/component.mk | 4 + components/unity/include/priv/setjmp.h | 14 ++++ components/xtensa/CMakeLists.txt | 2 +- components/xtensa/debug_helpers.c | 72 ++++++++++++++++ components/xtensa/debug_helpers_asm.S | 57 +++++++++++++ components/xtensa/include/esp_debug_helpers.h | 84 +++++++++++++++++-- tools/unit-test-app/sdkconfig.defaults | 1 + 17 files changed, 293 insertions(+), 32 deletions(-) create mode 100644 components/unity/include/priv/setjmp.h create mode 100644 components/xtensa/debug_helpers.c create mode 100644 components/xtensa/debug_helpers_asm.S diff --git a/components/esp32/cpu_start.c b/components/esp32/cpu_start.c index 3a782f298..7e5a1c93e 100644 --- a/components/esp32/cpu_start.c +++ b/components/esp32/cpu_start.c @@ -59,7 +59,6 @@ #include "esp_phy_init.h" #include "esp32/cache_err_int.h" #include "esp_coexist_internal.h" -#include "esp_debug_helpers.h" #include "esp_core_dump.h" #include "esp_app_trace.h" #include "esp_private/dbg_stubs.h" diff --git a/components/esp32/gdbstub.c b/components/esp32/gdbstub.c index 08623dce4..c690e42b5 100644 --- a/components/esp32/gdbstub.c +++ b/components/esp32/gdbstub.c @@ -22,8 +22,8 @@ #include "esp32/rom/ets_sys.h" #include "soc/uart_periph.h" #include "soc/gpio_periph.h" +#include "soc/soc_memory_layout.h" #include "esp_private/gdbstub.h" -#include "esp_debug_helpers.h" #include "driver/gpio.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" diff --git a/components/esp32/panic.c b/components/esp32/panic.c index b66ba9437..e7ed6540e 100644 --- a/components/esp32/panic.c +++ b/components/esp32/panic.c @@ -30,6 +30,7 @@ #include "soc/cpu.h" #include "soc/rtc.h" #include "soc/rtc_wdt.h" +#include "soc/soc_memory_layout.h" #include "esp_private/gdbstub.h" #include "esp_debug_helpers.h" @@ -446,33 +447,36 @@ static void esp_panic_dig_reset() static void putEntry(uint32_t pc, uint32_t sp) { - if (pc & 0x80000000) { - pc = (pc & 0x3fffffff) | 0x40000000; - } panicPutStr(" 0x"); panicPutHex(pc); panicPutStr(":0x"); panicPutHex(sp); } -static void doBacktrace(XtExcFrame *frame) +static void doBacktrace(XtExcFrame *exc_frame, int depth) { - uint32_t i = 0, pc = frame->pc, sp = frame->a1; + //Initialize stk_frame with first frame of stack + esp_backtrace_frame_t stk_frame = {.pc = exc_frame->pc, .sp = exc_frame->a1, .next_pc = exc_frame->a0}; panicPutStr("\r\nBacktrace:"); - /* Do not check sanity on first entry, PC could be smashed. */ - putEntry(pc, sp); - pc = frame->a0; - while (i++ < 100) { - uint32_t psp = sp; - if (!esp_stack_ptr_is_sane(sp) || i++ > 100) { - break; - } - sp = *((uint32_t *) (sp - 0x10 + 4)); - putEntry(pc - 3, sp); // stack frame addresses are return addresses, so subtract 3 to get the CALL address - pc = *((uint32_t *) (psp - 0x10)); - if (pc < 0x40000000) { - break; + putEntry(esp_cpu_process_stack_pc(stk_frame.pc), stk_frame.sp); + + //Check if first frame is valid + bool corrupted = (esp_stack_ptr_is_sane(stk_frame.sp) && + esp_ptr_executable((void*)esp_cpu_process_stack_pc(stk_frame.pc))) ? + false : true; + uint32_t i = ((depth <= 0) ? INT32_MAX : depth) - 1; //Account for stack frame that's already printed + while (i-- > 0 && stk_frame.next_pc != 0 && !corrupted) { + if (!esp_backtrace_get_next_frame(&stk_frame)) { //Get next stack frame + corrupted = true; } + putEntry(esp_cpu_process_stack_pc(stk_frame.pc), stk_frame.sp); + } + + //Print backtrace termination marker + if (corrupted) { + panicPutStr(" |<-CORRUPTED"); + } else if (stk_frame.next_pc != 0) { //Backtrace continues + panicPutStr(" |<-CONTINUES"); } panicPutStr("\r\n"); } @@ -549,7 +553,7 @@ static void commonErrorHandler_dump(XtExcFrame *frame, int core_id) panicPutStr("\r\n"); /* With windowed ABI backtracing is easy, let's do it. */ - doBacktrace(frame); + doBacktrace(frame, 100); panicPutStr("\r\n"); } diff --git a/components/espcoredump/src/core_dump_port.c b/components/espcoredump/src/core_dump_port.c index 3e3414a4c..2ac1b6482 100644 --- a/components/espcoredump/src/core_dump_port.c +++ b/components/espcoredump/src/core_dump_port.c @@ -13,7 +13,7 @@ // limitations under the License. #include #include -#include "esp_debug_helpers.h" +#include "soc/soc_memory_layout.h" #include "esp_core_dump_priv.h" const static DRAM_ATTR char TAG[] __attribute__((unused)) = "esp_core_dump_port"; diff --git a/components/freertos/tasks.c b/components/freertos/tasks.c index 91c761664..b19ba84a6 100644 --- a/components/freertos/tasks.c +++ b/components/freertos/tasks.c @@ -78,7 +78,6 @@ task.h is included from an application file. */ #include "esp32/rom/ets_sys.h" #include "esp_newlib.h" -#include "esp_debug_helpers.h" /* FreeRTOS includes. */ #include "FreeRTOS.h" diff --git a/components/heap/test/test_malloc.c b/components/heap/test/test_malloc.c index 1ed67ce50..e2916e2bc 100644 --- a/components/heap/test/test_malloc.c +++ b/components/heap/test/test_malloc.c @@ -14,7 +14,6 @@ #include "unity.h" #include "esp_heap_caps.h" -#include "esp_debug_helpers.h" #include "sdkconfig.h" diff --git a/components/soc/esp32/include/soc/cpu.h b/components/soc/esp32/include/soc/cpu.h index f28feb59f..190786a43 100644 --- a/components/soc/esp32/include/soc/cpu.h +++ b/components/soc/esp32/include/soc/cpu.h @@ -110,4 +110,25 @@ void esp_cpu_reset(int cpu_id); */ bool esp_cpu_in_ocd_debug_mode(); +/** + * @brief Convert the PC register value to its true address + * + * The address of the current instruction is not stored as an exact uint32_t + * representation in PC register. This function will convert the value stored in + * the PC register to a uint32_t address. + * + * @param pc_raw The PC as stored in register format. + * + * @return Address in uint32_t format + */ +static inline uint32_t esp_cpu_process_stack_pc(uint32_t pc) +{ + if (pc & 0x80000000) { + //Top two bits of a0 (return address) specify window increment. Overwrite to map to address space. + pc = (pc & 0x3fffffff) | 0x40000000; + } + //Minus 3 to get PC of previous instruction (i.e. instruction executed before return address) + return pc - 3; +} + #endif diff --git a/components/soc/include/soc/soc_memory_layout.h b/components/soc/include/soc/soc_memory_layout.h index 6fb8c6519..de7c449d5 100644 --- a/components/soc/include/soc/soc_memory_layout.h +++ b/components/soc/include/soc/soc_memory_layout.h @@ -203,3 +203,10 @@ inline static bool IRAM_ATTR esp_ptr_in_diram_dram(const void *p) { inline static bool IRAM_ATTR esp_ptr_in_diram_iram(const void *p) { return ((intptr_t)p >= SOC_DIRAM_IRAM_LOW && (intptr_t)p < SOC_DIRAM_IRAM_HIGH); } + + +inline static bool IRAM_ATTR esp_stack_ptr_is_sane(uint32_t sp) +{ + //Check if stack ptr is in between SOC_DRAM_LOW and SOC_DRAM_HIGH, and 16 byte aligned. + return !(sp < SOC_DRAM_LOW + 0x10 || sp > SOC_DRAM_HIGH - 0x10 || ((sp & 0xF) != 0)); +} diff --git a/components/unity/CMakeLists.txt b/components/unity/CMakeLists.txt index 3f8ecac23..53494ae0f 100644 --- a/components/unity/CMakeLists.txt +++ b/components/unity/CMakeLists.txt @@ -4,6 +4,10 @@ set(COMPONENT_SRCS "unity/src/unity.c" set(COMPONENT_ADD_INCLUDEDIRS "include" "unity/src") +if(CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL) + list(APPEND COMPONENT_PRIV_INCLUDEDIRS "include/priv") +endif() + if(CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER) list(APPEND COMPONENT_SRCS "unity_runner.c") endif() diff --git a/components/unity/Kconfig b/components/unity/Kconfig index 4cf2a3073..e9efc6381 100644 --- a/components/unity/Kconfig +++ b/components/unity/Kconfig @@ -42,4 +42,12 @@ menu "Unity unit testing library" the build. These provide an optional set of macros and functions to implement test groups. -endmenu # "Unity unit testing library" + config UNITY_ENABLE_BACKTRACE_ON_FAIL + bool "Print a backtrace when a unit test fails" + default n + help + If set, the unity framework will print the backtrace information before + jumping back to the test menu. The jumping is usually occurs in assert + functions such as TEST_ASSERT, TEST_FAIL etc. + +endmenu # "Unity unit testing library" \ No newline at end of file diff --git a/components/unity/component.mk b/components/unity/component.mk index 3e1501c79..2d2a106d7 100644 --- a/components/unity/component.mk +++ b/components/unity/component.mk @@ -9,6 +9,10 @@ endif COMPONENT_ADD_INCLUDEDIRS = include unity/src COMPONENT_SRCDIRS = unity/src . +ifdef CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL +COMPONENT_PRIV_INCLUDEDIRS += include/priv +endif + ifndef CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER COMPONENT_OBJEXCLUDE += unity_runner.o endif diff --git a/components/unity/include/priv/setjmp.h b/components/unity/include/priv/setjmp.h new file mode 100644 index 000000000..c467cbd94 --- /dev/null +++ b/components/unity/include/priv/setjmp.h @@ -0,0 +1,14 @@ +#include_next +#include "esp_debug_helpers.h" + +/* + * This is the middle layer of setjmp to be used with the unity. + */ + +/** Insert backtrace before longjmp (TEST_ABORT). + * + * Currently we only do long jump before test is ignored or failed. + * If this is also called when test pass, we may need to add some check before + * backtrace is called. + */ +#define longjmp(buf, val) do {esp_backtrace_print(100); longjmp(buf, val);} while(0) diff --git a/components/xtensa/CMakeLists.txt b/components/xtensa/CMakeLists.txt index cb9c9ce49..1bb997d67 100644 --- a/components/xtensa/CMakeLists.txt +++ b/components/xtensa/CMakeLists.txt @@ -1,4 +1,4 @@ -set(COMPONENT_SRCS "eri.c" "trax.c") +set(COMPONENT_SRCS "eri.c" "trax.c" "debug_helpers.c" "debug_helpers_asm.S") set(COMPONENT_ADD_INCLUDEDIRS "include" "${IDF_TARGET}/include") diff --git a/components/xtensa/debug_helpers.c b/components/xtensa/debug_helpers.c new file mode 100644 index 000000000..564e6eb15 --- /dev/null +++ b/components/xtensa/debug_helpers.c @@ -0,0 +1,72 @@ +// Copyright 2015-2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "esp_types.h" +#include "esp_attr.h" +#include "esp_err.h" +#include "esp_debug_helpers.h" +#include "esp32/rom/ets_sys.h" +#include "soc/soc_memory_layout.h" +#include "soc/cpu.h" + +bool IRAM_ATTR esp_backtrace_get_next_frame(esp_backtrace_frame_t *frame) +{ + //Use frame(i-1)'s BS area located below frame(i)'s sp to get frame(i-1)'s sp and frame(i-2)'s pc + void *base_save = (void *)frame->sp; //Base save area consists of 4 words under SP + frame->pc = frame->next_pc; + frame->next_pc = *((uint32_t *)(base_save - 16)); //If next_pc = 0, indicates frame(i-1) is the last frame on the stack + frame->sp = *((uint32_t *)(base_save - 12)); + + //Return true if both sp and pc of frame(i-1) are sane, false otherwise + return (esp_stack_ptr_is_sane(frame->sp) && esp_ptr_executable((void*)esp_cpu_process_stack_pc(frame->pc))); +} + +esp_err_t IRAM_ATTR esp_backtrace_print(int depth) +{ + //Check arguments + if (depth <= 0) { + return ESP_ERR_INVALID_ARG; + } + + //Initialize stk_frame with first frame of stack + esp_backtrace_frame_t stk_frame; + esp_backtrace_get_start(&(stk_frame.pc), &(stk_frame.sp), &(stk_frame.next_pc)); + //esp_cpu_get_backtrace_start(&stk_frame); + ets_printf("\r\n\r\nBacktrace:"); + ets_printf("0x%08X:0x%08X ", esp_cpu_process_stack_pc(stk_frame.pc), stk_frame.sp); + + //Check if first frame is valid + bool corrupted = (esp_stack_ptr_is_sane(stk_frame.sp) && + esp_ptr_executable((void*)esp_cpu_process_stack_pc(stk_frame.pc))) ? + false : true; + + uint32_t i = (depth <= 0) ? INT32_MAX : depth; + while (i-- > 0 && stk_frame.next_pc != 0 && !corrupted) { + if (!esp_backtrace_get_next_frame(&stk_frame)) { //Get previous stack frame + corrupted = true; + } + ets_printf("0x%08X:0x%08X ", esp_cpu_process_stack_pc(stk_frame.pc), stk_frame.sp); + } + + //Print backtrace termination marker + esp_err_t ret = ESP_OK; + if (corrupted) { + ets_printf(" |<-CORRUPTED"); + ret = ESP_FAIL; + } else if (stk_frame.next_pc != 0) { //Backtrace continues + ets_printf(" |<-CONTINUES"); + } + ets_printf("\r\n\r\n"); + return ret; +} diff --git a/components/xtensa/debug_helpers_asm.S b/components/xtensa/debug_helpers_asm.S new file mode 100644 index 000000000..0df70df86 --- /dev/null +++ b/components/xtensa/debug_helpers_asm.S @@ -0,0 +1,57 @@ +// Copyright 2015-2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +/* + * esp_backtrace_get_start(uint32_t *pc, uint32_t *sp, uint32_t *next_pc) + * + * High Addr + * .................. + * | i-3 BS | + * | i-1 locals | Function B + * .................. i-1 SP + * | i-2 BS | + * | i locals | Function A (Start of backtrace) + * ------------------ i SP + * | i-1 BS | + * | i+1 locals | Backtracing function (e.g. esp_backtrace_print()) + * ------------------ i+1 SP + * | i BS | + * | i+2 locals | esp_backtrace_get_start() <- This function + * ------------------ i+2 SP + * | i+1 BS | + * | i+3 locals | xthal_window_spill() + * ------------------ i+3 SP + * .................. Low Addr + */ + .section .iram1, "ax" + .align 4 + .global esp_backtrace_get_start + .type esp_backtrace_get_start, @function +esp_backtrace_get_start: + entry a1, 32 + call8 xthal_window_spill //Spill registers onto stack (excluding this function) + //a2, a3, a4 should be out arguments for i SP, i PC, i-1 PC respectively. Use a5 and a6 as scratch + l32e a5, sp, -16 //Get i PC, which is ret addres of i+1 + s32i a5, a2, 0 //Store i PC to arg *pc + l32e a6, sp, -12 //Get i+1 SP. Used to access i BS + l32e a5, a6, -12 //Get i SP + s32i a5, a3, 0 //Store i SP to arg *sp + l32e a5, a6, -16 //Get i-1 PC, which is ret address of i + s32i a5, a4, 0 //Store i-1 PC to arg *next_pc + retw diff --git a/components/xtensa/include/esp_debug_helpers.h b/components/xtensa/include/esp_debug_helpers.h index 981fa34a5..40167780c 100644 --- a/components/xtensa/include/esp_debug_helpers.h +++ b/components/xtensa/include/esp_debug_helpers.h @@ -1,3 +1,17 @@ +// Copyright 2015-2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #pragma once #ifdef __cplusplus @@ -12,6 +26,24 @@ extern "C" { #define ESP_WATCHPOINT_STORE 0x80000000 #define ESP_WATCHPOINT_ACCESS 0xC0000000 +/* + * @brief Structure used for backtracing + * + * This structure stores the backtrace information of a particular stack frame + * (i.e. the PC and SP). This structure is used iteratively with the + * esp_cpu_get_next_backtrace_frame() function to traverse each frame within a + * single stack. The next_pc represents the PC of the current frame's caller, thus + * a next_pc of 0 indicates that the current frame is the last frame on the stack. + * + * @note Call esp_backtrace_get_start() to obtain initialization values for + * this structure + */ +typedef struct { + uint32_t pc; /* PC of the current frame */ + uint32_t sp; /* SP of the current frame */ + uint32_t next_pc; /* PC of the current frame's caller */ +} esp_backtrace_frame_t; + /** * @brief If an OCD is connected over JTAG. set breakpoint 0 to the given function * address. Do nothing otherwise. @@ -37,7 +69,6 @@ void esp_set_breakpoint_if_jtag(void *fn); */ esp_err_t esp_set_watchpoint(int no, void *adr, int size, int flags); - /** * @brief Clear a watchpoint * @@ -47,12 +78,53 @@ esp_err_t esp_set_watchpoint(int no, void *adr, int size, int flags); void esp_clear_watchpoint(int no); /** - * @brief Checks stack pointer + * Get the first frame of the current stack's backtrace + * + * Given the following function call flow (B -> A -> X -> esp_backtrace_get_start), + * this function will do the following. + * - Flush CPU registers and window frames onto the current stack + * - Return PC and SP of function A (i.e. start of the stack's backtrace) + * - Return PC of function B (i.e. next_pc) + * + * @note This function is implemented in assembly + * + * @param[out] pc PC of the first frame in the backtrace + * @param[out] sp SP of the first frame in the backtrace + * @param[out] next_pc PC of the first frame's caller */ -static inline bool esp_stack_ptr_is_sane(uint32_t sp) -{ - return !(sp < 0x3ffae010UL || sp > 0x3ffffff0UL || ((sp & 0xf) != 0)); -} +extern void esp_backtrace_get_start(uint32_t *pc, uint32_t *sp, uint32_t *next_pc); + +/** + * Get the next frame on a stack for backtracing + * + * Given a stack frame(i), this function will obtain the next stack frame(i-1) + * on the same call stack (i.e. the caller of frame(i)). This function is meant to be + * called iteratively when doing a backtrace. + * + * Entry Conditions: Frame structure containing valid SP and next_pc + * Exit Conditions: + * - Frame structure updated with SP and PC of frame(i-1). next_pc now points to frame(i-2). + * - If a next_pc of 0 is returned, it indicates that frame(i-1) is last frame on the stack + * + * @param[inout] frame Pointer to frame structure + * + * @return + * - True if the SP and PC of the next frame(i-1) are sane + * - False otherwise + */ +bool esp_backtrace_get_next_frame(esp_backtrace_frame_t *frame); + +/** + * @brief Print the backtrace of the current stack + * + * @param depth The maximum number of stack frames to print (should be > 0) + * + * @return + * - ESP_OK Backtrace successfully printed to completion or to depth limit + * - ESP_FAIL Backtrace is corrupted + */ +esp_err_t esp_backtrace_print(int depth); + #endif #ifdef __cplusplus diff --git a/tools/unit-test-app/sdkconfig.defaults b/tools/unit-test-app/sdkconfig.defaults index ca30ad526..947c23287 100644 --- a/tools/unit-test-app/sdkconfig.defaults +++ b/tools/unit-test-app/sdkconfig.defaults @@ -31,3 +31,4 @@ CONFIG_SPI_MASTER_IN_IRAM=y CONFIG_EFUSE_VIRTUAL=y CONFIG_SPIRAM_BANKSWITCH_ENABLE=n CONFIG_FATFS_ALLOC_EXTRAM_FIRST=y +CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y