Skip to main content
  • crash dumps
  • esp32
  • debugging
  • backtrace
  • embedded software

Crash Dump Backtrace: Read ESP32 Panics Without Guessing

The Boss Factory9 min read

A panic dump looks precise enough to invite a guess: one exception name, a register value, and a column of hexadecimal addresses. The useful answer is narrower: identify what the exception proves, resolve the backtrace against the exact firmware image, then test the cheapest likely cause before changing hardware.

Capture the complete crash report first

Keep the serial output from the first line of the panic through the reboot message. A partial backtrace is often worse than no backtrace because it encourages a confident diagnosis from missing evidence.

For an ESP32 Guru Meditation report, preserve these fields:

  • The exception type, such as LoadProhibited, StoreProhibited, LoadStoreError, or IllegalInstruction.
  • PC, the program counter where the CPU trapped.
  • EXCVADDR, the address involved in the exception on Xtensa-based ESP32 targets.
  • The register dump.
  • Every line of Backtrace.
  • The firmware build identifier, board target, compiler settings, and source revision.

The exact field names vary by chip family and framework. Classic ESP32, ESP32-S2, ESP32-S3, and ESP32-C3 do not all use the same CPU core, so do not copy an Xtensa interpretation onto a RISC-V report. The principle remains the same: capture the CPU's exception record and match it to the firmware that was running.

Do not start by blaming the last peripheral you connected. A bad sensor can trigger a code path that exposes a pointer bug, but the panic report is evidence about the instruction that failed, not a parts list.

Resolve the backtrace against the exact binary

A crash backtrace is a list of addresses that must be resolved against the exact binary that was running to be meaningful. A different compile, even from unchanged source, can move functions enough to produce the wrong names or line numbers. Build flags, link layout, compiler version, partition layout, and optimization all matter.

Keep the ELF file produced by the same build as the flashed firmware. The .bin file is what the device boots, but the ELF normally carries the symbols and debug line information needed by a backtrace decoder. If the build system has a built-in exception decoder, give it the matching ELF rather than a later rebuild.

For a manual check, the general form is:

xtensa-esp32-elf-addr2line -pfiaC -e firmware.elf 0x4008xxxx 0x400dxxxx

Use the toolchain belonging to the target. An ESP32-C3 needs the appropriate RISC-V addr2line, not an Xtensa executable. The addresses in a panic may include ROM, bootloader, framework, and application regions. Application addresses should resolve to your source when the symbols match; ROM frames may resolve only to a library label or remain opaque.

The first frame is not always the bug. The program counter identifies the instruction that trapped. The next frames show how execution reached it. A call chain that ends in a logging function may mean the logger failed, or it may be the path that reported an earlier fault. Read the instruction and the call chain together.

If every address resolves to ??:0, check these before interpreting the result:

  • You used the ELF from a different build.
  • The address decoder targets the wrong ESP32 variant.
  • The firmware was flashed with an offset or image that does not match the decoded file.
  • Debug symbols or line tables were stripped.
  • The panic text was copied incorrectly.

Read the exception as a narrowing clue

The exception type does not name the complete bug. It identifies the class of CPU operation that could not complete. Use it to choose the first test, not to skip the tests.

Exception type or clueTechnical address clueFirst area to testConfirmation test
LoadProhibitedRead from an invalid address; a very low address strongly suggests a null pointer dereferenceUninitialized, cleared, or failed object pointerLog the pointer immediately before the failing call and add an explicit null check
StoreProhibitedWrite to an invalid address; a very low address strongly suggests a null pointer dereferenceNull, stale, or corrupted destination pointerLog the destination and verify the object lifetime at the write site
LoadStoreErrorAccess alignment, width, or memory-region rules were violated on targets that report this exceptionPacked data, invalid casts, or an unsupported access widthReplace the cast with a correctly aligned field access and compare behavior
IllegalInstructionThe program counter does not contain a valid instruction for the CPUCorrupted return address, bad function pointer, wrong image, or incompatible binaryConfirm the flashed image and inspect the call chain around the return or indirect call
Watchdog resetUsually no useful fault address; a task or interrupt failed to yieldInfinite loop, blocked critical section, or work inside an interrupt handlerAdd progress markers and measure task or handler duration

A load or store exception at a very low address is the signature of dereferencing a null pointer. On a typical 32-bit system, a member access such as object->field can fault at a small nonzero address because the field offset is added to a null base. That still points first at object == nullptr, not at a defective RAM chip.

The value of EXCVADDR helps separate cases. A zero or low value supports a null-base theory. A changing, implausible address points more toward memory corruption, an out-of-bounds index, use-after-free behavior, or a function pointer overwritten by stray data. The register dump and source expression still have to agree before the theory is accepted.

For IllegalInstruction, check the firmware image and reset cause before rewriting application logic. A corrupted flash image, a call through a damaged function pointer, stack corruption, or execution returning through a smashed address can all land in this category. On a chip family with a different instruction set, an image built for the wrong target is a particularly direct explanation.

Test causes in the order that saves parts

Start with the least expensive change that can disprove the most likely cause. Our normal sequence is:

  1. Reproduce with the same input. Record whether the crash needs a cold boot, a particular command, Wi-Fi activity, a sensor response, or a long runtime. A fault that occurs at the same operation is easier to isolate than one that merely appears after an hour.
  2. Add narrow logging. Print pointer values, buffer lengths, indexes, state transitions, and return codes immediately before the resolved source line. Avoid logging inside a suspected interrupt or inside code that may already be out of stack space.
  3. Check every allocation and lookup. Test the result of malloc, new, queue creation, handle lookup, file open, and driver initialization before use. A failed allocation or missing handle often becomes a null dereference several calls later.
  4. Check bounds and lifetimes. Compare every index against the actual buffer length, not a constant copied from an earlier version. Confirm that a task, callback, or interrupt cannot use an object after the task that owns it has freed or replaced it.
  5. Reduce concurrency. Temporarily disable one callback, timer, task, or network path at a time. If the panic vanishes, add synchronization or move shared state behind a defined ownership rule. Do not treat disappearance as proof that the disabled peripheral is bad.
  6. Check stack headroom. A stack overwrite can corrupt return addresses and make the eventual exception look unrelated. Use the framework's high-water-mark facility for each task, and test with stack checking enabled where the target and framework support it.
  7. Only then inspect hardware. Confirm supply voltage under load, reset cause, cable connections, and signal levels. Replace a module only when the software test isolates that module or an independent measurement shows it failing.

A useful controlled test is to replace a live input with a fixed value while leaving the same code path active. If the crash remains at the same resolved instruction, the input device is less likely than the pointer or buffer handling. If the crash moves or disappears, inspect parsing, units, range checks, and timing around that input.

Do not “fix” a null pointer by adding a guard that silently skips the operation and carries on. That can be correct for an optional object, but it can also hide a failed initialization and leave the system in a state that later corrupts data. Make the guard report the missing dependency or stop the affected task deliberately.

Make the next dump more useful

Once the immediate fault is understood, improve the evidence for the next one. Keep build artifacts with the release image, include a source revision in the boot log, and make the reset reason part of startup diagnostics. A small build identifier printed at boot prevents hours of decoding the wrong ELF.

For failures that do not reproduce under normal logging, use a ring buffer in RAM or flash for short event records rather than printing every operation. Record state changes, queue failures, allocation failures, and watchdog progress markers. Logging changes timing, stack use, and buffer pressure, so treat a disappearing crash as a timing clue rather than a repair.

The best fix is usually at the first invalid assumption: check the return value, define ownership, enforce the buffer limit, or correct the target build. Replacing the board is the expensive response to a report that already points at a source-level fault.

Frequently asked questions

What does EXCVADDR: 0x00000000 mean on an ESP32?

On Xtensa-based ESP32 targets, it means the trapped memory operation referenced address zero. With LoadProhibited or StoreProhibited, that is strong evidence of a null pointer dereference. Resolve the program counter and inspect the pointer used by that instruction before replacing hardware.

Can I decode an ESP32 backtrace without the original source?

You can often recover function names and line numbers from the matching ELF, even without the source files on the current machine. Without matching debug symbols, you may get addresses only. A rebuilt ELF is not a safe substitute unless it came from the identical build inputs and target configuration.

Why does the ESP32 crash only after running for a while?

Delayed crashes commonly involve a cumulative condition: a leak, an out-of-bounds write, stack exhaustion, queue buildup, or a race between tasks and callbacks. Capture free heap, task stack high-water marks, queue depth, and reset reason over time. The delay describes when corrupted state becomes visible, not necessarily when it was created.

We build custom electronics and software to order through The Boss Factory.

Related services

All services →

Have a project in mind?

Tell us what you want built — we reply within 24–48 hours.

Request a quote

Keep reading