- crash dumps
- esp32
- debugging
- backtrace
- embedded software
Crash Dump Backtrace: Read ESP32 Panics Without Guessing

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, orIllegalInstruction. 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 clue | Technical address clue | First area to test | Confirmation test |
|---|---|---|---|
LoadProhibited | Read from an invalid address; a very low address strongly suggests a null pointer dereference | Uninitialized, cleared, or failed object pointer | Log the pointer immediately before the failing call and add an explicit null check |
StoreProhibited | Write to an invalid address; a very low address strongly suggests a null pointer dereference | Null, stale, or corrupted destination pointer | Log the destination and verify the object lifetime at the write site |
LoadStoreError | Access alignment, width, or memory-region rules were violated on targets that report this exception | Packed data, invalid casts, or an unsupported access width | Replace the cast with a correctly aligned field access and compare behavior |
IllegalInstruction | The program counter does not contain a valid instruction for the CPU | Corrupted return address, bad function pointer, wrong image, or incompatible binary | Confirm the flashed image and inspect the call chain around the return or indirect call |
| Watchdog reset | Usually no useful fault address; a task or interrupt failed to yield | Infinite loop, blocked critical section, or work inside an interrupt handler | Add 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:
- 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.
- 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.
- 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. - 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.
- 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.
- 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.
- 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.
