Bladeren bron

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.
Darian Leung 7 jaren geleden
bovenliggende
commit
1786fc9ed2

+ 2 - 0
components/esp32/CMakeLists.txt

@@ -16,6 +16,8 @@ else()
                    "coexist.c"
                    "cpu_start.c"
                    "crosscore_int.c"
+                   "debug_helpers.c"
+                   "debug_helpers_asm.S"
                    "dbg_stubs.c"
                    "dport_access.c"
                    "dport_panic_highint_hdl.S"

+ 72 - 0
components/esp32/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_panic.h"
+#include "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;
+}

+ 57 - 0
components/esp32/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 <xtensa/coreasm.h>
+#include <xtensa/corebits.h>
+#include <xtensa/config/system.h>
+#include <xtensa/hal.h>
+
+/*
+ * 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

+ 80 - 14
components/esp32/include/esp_panic.h

@@ -1,5 +1,18 @@
-#ifndef PANIC_H
-#define PANIC_H
+// 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
 extern "C"
@@ -21,8 +34,27 @@ extern "C"
 
 #include "esp_err.h"
 #include "soc/soc.h"
+#include "soc/soc_memory_layout.h"
 
 
+/*
+ * @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.
@@ -53,7 +85,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
  *
@@ -82,21 +113,56 @@ inline static bool esp_stack_ptr_in_extram(uint32_t sp)
 }
 #endif
 
+/*
+ * 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
+ */
+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 Checks stack pointer
+ * @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
  */
-static inline bool esp_stack_ptr_is_sane(uint32_t sp)
-{
-#if CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY
-    return (esp_stack_ptr_in_dram(sp) || esp_stack_ptr_in_extram(sp));
-#else
-    return esp_stack_ptr_in_dram(sp);
-#endif
-}
+esp_err_t esp_backtrace_print(int depth);
+
 #endif
 
 #ifdef __cplusplus
 }
 #endif
-
-#endif

+ 23 - 19
components/esp32/panic.c

@@ -31,6 +31,7 @@
 #include "soc/cpu.h"
 #include "soc/rtc.h"
 #include "soc/rtc_wdt.h"
+#include "soc/soc_memory_layout.h"
 
 #include "esp_gdbstub.h"
 #include "esp_panic.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");
 }

+ 1 - 0
components/espcoredump/src/core_dump_port.c

@@ -14,6 +14,7 @@
 #include <string.h>
 #include <stdbool.h>
 #include "esp_panic.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";

+ 21 - 0
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

+ 7 - 0
components/soc/include/soc/soc_memory_layout.h

@@ -206,3 +206,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));
+}

+ 4 - 0
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()

+ 9 - 1
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"

+ 4 - 0
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

+ 14 - 0
components/unity/include/priv/setjmp.h

@@ -0,0 +1,14 @@
+#include_next <setjmp.h>
+#include "esp_panic.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)

+ 1 - 0
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