[esp32] Capture fault address and raw cause in crash handler (#17769)

This commit is contained in:
J. Nick Koston
2026-07-25 18:12:53 -10:00
committed by GitHub
parent 98e9dd7954
commit e0644ab20a
3 changed files with 77 additions and 5 deletions
+4 -2
View File
@@ -3108,9 +3108,10 @@ def _parse_register(config, regex, line):
STACKTRACE_ESP32_PC_RE = re.compile(r".*PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7}).*")
STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r"EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})")
STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r".*EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})")
STACKTRACE_ESP32_C3_PC_RE = re.compile(r"MEPC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})")
STACKTRACE_ESP32_C3_RA_RE = re.compile(r"RA\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})")
STACKTRACE_ESP32_C3_MTVAL_RE = re.compile(r".*MTVAL\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})")
STACKTRACE_BAD_ALLOC_RE = re.compile(
r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$"
)
@@ -3128,9 +3129,10 @@ def process_stacktrace(config, line, backtrace_state):
# ESP32 PC/EXCVADDR
_parse_register(config, STACKTRACE_ESP32_PC_RE, line)
_parse_register(config, STACKTRACE_ESP32_EXCVADDR_RE, line)
# ESP32-C3 PC/RA
# ESP32-C3 PC/RA/MTVAL
_parse_register(config, STACKTRACE_ESP32_C3_PC_RE, line)
_parse_register(config, STACKTRACE_ESP32_C3_RA_RE, line)
_parse_register(config, STACKTRACE_ESP32_C3_MTVAL_RE, line)
# bad alloc
match = re.match(STACKTRACE_BAD_ALLOC_RE, line)
+31 -3
View File
@@ -122,7 +122,7 @@ static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *ou
// Magic is second to validate the data. Remaining fields can change between versions.
// Version is uint32_t because it would be padded to 4 bytes anyway before the next
// uint32_t field, so we use the full width rather than wasting 3 bytes of padding.
static constexpr uint32_t CRASH_DATA_VERSION = 2;
static constexpr uint32_t CRASH_DATA_VERSION = 3;
struct RawCrashData {
uint32_t version;
uint32_t magic;
@@ -132,7 +132,8 @@ struct RawCrashData {
uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG)
uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic)
uint32_t backtrace[MAX_BACKTRACE];
uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V)
uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V)
uint32_t fault_addr; // Faulting memory address: excvaddr (Xtensa) or mtval (RISC-V)
uint8_t crashed_core;
#if SOC_CPU_CORES_NUM > 1
static_assert(SOC_CPU_CORES_NUM == 2, "Dual-core logic assumes exactly 2 cores");
@@ -240,6 +241,16 @@ static const char *get_exception_reason() {
nullptr,
"LoadProhibited",
"StoreProhibited",
nullptr,
nullptr,
"Cp0Dis",
"Cp1Dis",
"Cp2Dis",
"Cp3Dis",
"Cp4Dis",
"Cp5Dis",
"Cp6Dis",
"Cp7Dis",
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
@@ -332,12 +343,24 @@ void crash_handler_log() {
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
const char *reason = get_exception_reason();
if (reason != nullptr) {
ESP_LOGE(TAG, " Reason: %s - %s", get_exception_type(), reason);
ESP_LOGE(TAG, " Reason: %s - %s (cause %" PRIu32 ")", get_exception_type(), reason, s_raw_crash_data.cause);
} else {
ESP_LOGE(TAG, " Reason: %s", get_exception_type());
}
ESP_LOGE(TAG, " Crashed core: %d", s_raw_crash_data.crashed_core);
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc);
// Faulting memory address — only meaningful for real CPU faults, not
// aborts/watchdogs or SoC-level pseudo exceptions. Uses the same register
// name as ESP-IDF's live register dump for the architecture (EXCVADDR on
// Xtensa, MTVAL on RISC-V) so the CLI decodes it when it happens to be a
// code address.
if (s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause) {
#if CONFIG_IDF_TARGET_ARCH_XTENSA
ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32 " (faulting address)", s_raw_crash_data.fault_addr);
#elif CONFIG_IDF_TARGET_ARCH_RISCV
ESP_LOGE(TAG, " MTVAL: 0x%08" PRIX32 " (faulting address)", s_raw_crash_data.fault_addr);
#endif
}
log_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, s_raw_crash_data.reg_frame_count);
#if SOC_CPU_CORES_NUM > 1
@@ -382,6 +405,9 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
s_raw_crash_data.exception = (uint8_t) info->exception;
s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
s_raw_crash_data.crashed_core = (uint8_t) info->core;
// Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot
s_raw_crash_data.cause = 0;
s_raw_crash_data.fault_addr = 0;
#if SOC_CPU_CORES_NUM > 1
s_raw_crash_data.other_backtrace_count = 0;
s_raw_crash_data.other_reg_frame_count = 0;
@@ -392,6 +418,7 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
if (info->frame != nullptr) {
auto *xt_frame = (XtExcFrame *) info->frame;
s_raw_crash_data.cause = xt_frame->exccause;
s_raw_crash_data.fault_addr = xt_frame->excvaddr;
s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE);
}
@@ -414,6 +441,7 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
if (info->frame != nullptr) {
auto *rv_frame = (RvExcFrame *) info->frame;
s_raw_crash_data.cause = rv_frame->mcause;
s_raw_crash_data.fault_addr = rv_frame->mtval;
s_raw_crash_data.backtrace_count =
capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count);
}
@@ -137,3 +137,45 @@ def test_process_stacktrace_esp32_crash_handler(
state = process_stacktrace(config, line_bt1, False)
mock_esp32_decode_pc.assert_called_once_with(config, "42005ABC")
assert state is False
mock_esp32_decode_pc.reset_mock()
# Reason line carries no address, must not trigger a decode
line_reason = "[E][esp32.crash:079]: Reason: Fault - LoadProhibited (cause 28)"
state = process_stacktrace(config, line_reason, False)
mock_esp32_decode_pc.assert_not_called()
assert state is False
mock_esp32_decode_pc.reset_mock()
# EXCVADDR pointing at code (e.g. jumping through a corrupted pointer) decodes
line_excvaddr = "[E][esp32.crash:081]: EXCVADDR: 0x400D9ABC (faulting address)"
state = process_stacktrace(config, line_excvaddr, False)
mock_esp32_decode_pc.assert_called_once_with(config, "400D9ABC")
assert state is False
mock_esp32_decode_pc.reset_mock()
# EXCVADDR pointing at data (heap/null) is not a code address, must be ignored
line_excvaddr_data = (
"[E][esp32.crash:081]: EXCVADDR: 0x0000001C (faulting address)"
)
state = process_stacktrace(config, line_excvaddr_data, False)
mock_esp32_decode_pc.assert_not_called()
assert state is False
mock_esp32_decode_pc.reset_mock()
# RISC-V MTVAL pointing at code decodes
line_mtval = "[E][esp32.crash:081]: MTVAL: 0x42001234 (faulting address)"
state = process_stacktrace(config, line_mtval, False)
mock_esp32_decode_pc.assert_called_once_with(config, "42001234")
assert state is False
mock_esp32_decode_pc.reset_mock()
# RISC-V MTVAL pointing at data must be ignored
line_mtval_data = "[E][esp32.crash:081]: MTVAL: 0x3FC80123 (faulting address)"
state = process_stacktrace(config, line_mtval_data, False)
mock_esp32_decode_pc.assert_not_called()
assert state is False