How to Use ESP-IDF Monitor and GDB to Debug a Crashing ESP32 Application

Every ESP32 developer remembers their first Guru Meditation Error. You flash your firmware, open the serial monitor, and instead of your printf output, you get 40 lines of register dumps, hex addresses, and a chip that reboots into the same crash every three seconds. You Google “Guru Meditation Error,” find a forum thread from 2019 that says “check your pointers,” and close your laptop.
Here’s the thing: that wall of hex is actually a detailed crash report. The ESP-IDF toolchain already decodes most of it for you. You just need to know where to look. And with one menuconfig change, you can freeze the entire crash state into a core dump and explore it interactively in GDB, inspecting every variable, every stack frame, every FreeRTOS task, without buying any hardware.
By the end of this article, you’ll decode a real crash, load it into GDB, and identify the exact line of code that caused it. The only prerequisite is a working ESP-IDF v5.x install and the ability to build and flash.
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ REPRODUCE │────▶│ CAPTURE │────▶│ INSPECT │
│ the crash │ │ monitor output │ │ backtrace/GDB │
│ │ │ + core dump │ │ → find root │
└─────────────┘ └──────────────────┘ │ cause │
▲ └────────┬────────┘
│ │
│ ┌──────────────────┐ │
└────────────│ FIX & VERIFY │◀──────────────┘
└──────────────────┘This three-step loop, reproduce, capture, inspect, covers the vast majority of application crashes. Let’s walk through it with a real bug.
The Example Bug: A Null Pointer Crash You Can Reproduce in 30 Seconds
Create a new project (or overwrite main.c in an existing one) with this code:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void app_main(void)
{
printf("App started. Crashing in 2 seconds...\n");
int *p = NULL;
vTaskDelay(pdMS_TO_TICKS(2000));
printf("Value: %d\n", *p); // dereferencing NULL — this will crash
}The vTaskDelay is deliberate: it proves the crash isn’t a boot failure, and it gives idf.py monitor time to connect before the panic happens. Build and flash it:
idf.py build flash monitorTwo seconds after boot, your terminal will explode. Good. That’s exactly what we need.
Step 1: Reading the Backtrace in idf.py monitor
When the ESP32’s panic handler fires, it dumps registers, a backtrace, and memory state over serial. The key insight: idf.py monitor automatically decodes hex addresses into file names and line numbers using addr2line and your project’s ELF binary. You don’t need to run any extra tools.
Here’s what the relevant portion of the crash output looks like (trimmed for clarity):
Guru Meditation Error: Core 0 panic'ed (LoadProhibited). Exception was unhandled.
Core 0 register dump:
PC : 0x400d1234 PS : 0x00060030 A0 : 0x800d1200 A1 : 0x3ffb1230
A2 : 0x00000000 A3 : 0x3ffb1260 A4 : 0x00000001 A5 : 0x00000000
...
Backtrace: 0x400d1234:0x3ffb1230 0x400d1200:0x3ffb1250
0x400d1234: app_main at /home/dev/myproject/main/main.c:12
0x400d1200: main_task at /home/dev/esp-idf/components/freertos/app_startup.c:208Three things to read, in order:
1. The panic reason: LoadProhibited. In plain English: the CPU tried to read from a memory address it’s not allowed to access. For null pointer dereferences, this is almost always what you’ll see. A write to a null pointer would show StoreProhibited instead.
2. The backtrace, bottom-up. Read the decoded backtrace from the bottom. The lowest frame (main_task) is the FreeRTOS scheduler calling your code, which is normal. The top frame is where the crash happened: app_main at main.c:12. That’s your line. That’s the bug.
Backtrace: 0x400d1234:0x3ffb1230 0x400d1200:0x3ffb1250
─────┬──── ─────┬────
│ │
instruction stack pointer
address at that frame
│
▼
idf.py monitor decodes this to:
app_main (main.c:12)3. The register dump, the smoking gun. Look at register A2: it holds 0x00000000. That’s the null pointer the CPU tried to dereference. You don’t need to memorize the Xtensa calling convention, but knowing that the exception address is often in the first few A registers helps confirm what you’re looking at.
For many crashes, this is enough. You now know the file, line, and the value that caused the fault. But sometimes the backtrace points to a generic function deep in a library, and you need to see why a variable was null, what the state of your application actually was at the moment it died. That’s where core dumps come in.
If your backtrace shows only hex addresses with no file names, your ELF binary is out of sync with what’s flashed on the chip. Run
idf.py build flash monitoragain to ensure they match. Also confirm you’re building in Debug mode (the default), not Release with stripped symbols.
Step 2: Enabling Core Dumps So You Can Inspect the Full Crash State
A core dump is a frozen snapshot of your application’s entire memory at the moment of the crash: every task’s stack, every local variable, every register. Once saved to flash, you can load it into GDB and poke around as if you’d paused the program with a breakpoint.
Enable it through menuconfig:
idf.py menuconfig
└─ (Top)
└─ Core dump
├─ Data destination → Flash
├─ Core dump format → ELF format
└─ Maximum number of tasks → (leave default)Selecting “Flash” means the core dump is written to a dedicated partition in flash automatically on crash. ELF format is required for GDB compatibility. The default partition table in most ESP-IDF projects already includes a coredump partition. If yours doesn’t, you’ll get a build error telling you to add one.
Rebuild, reflash, and let the crash happen again:
idf.py build flash monitorAfter the panic, the monitor will print Core dump written to flash (or similar) before rebooting. The core dump is now sitting in flash, waiting.
Retrieve it with:
idf.py coredump-infoThis pulls the dump off flash over serial and prints a summary: the faulting task, a backtrace for every thread, and the panic reason. It’s like the monitor output, but richer. You see the state of every FreeRTOS task, not just the one that crashed.
But the real power is the interactive GDB session:
idf.py coredump-debugThis downloads the core dump, loads it alongside your ELF into GDB, and drops you at a prompt. You’re now debugging the crash, offline, with full source-level access.
Step 3: Finding the Root Cause in GDB
You’re in GDB, loaded with the core dump. The program is “frozen” at the exact moment of the crash. Nothing is running; you’re examining a corpse. Here’s exactly what to type:
bt — print the full backtrace:
(gdb) bt
#0 app_main () at /home/dev/myproject/main/main.c:12
#1 0x400d1200 in main_task (args=0x0) at /home/dev/esp-idf/components/freertos/app_startup.c:208Frame #0 is your code. In a more complex application, you might see 10+ frames of FreeRTOS and library calls. Look for the frame with your source file.
frame 0 — select the crashing frame:
(gdb) frame 0
#0 app_main () at /home/dev/myproject/main/main.c:12
12 printf("Value: %d\n", *p);GDB shows you the exact source line. No guessing.
list — see surrounding context:
(gdb) list
7 {
8 printf("App started. Crashing in 2 seconds...\n");
9
10 int *p = NULL;
11 vTaskDelay(pdMS_TO_TICKS(2000));
12
13 printf("Value: %d\n", *p);
14 }info locals — inspect every local variable:
(gdb) info locals
p = 0x0There it is. p is null. The crash is on line 12, which dereferences p. Root cause identified.
p *p — confirm the dereference fails:
(gdb) p *p
Cannot access memory at address 0x0GDB confirms exactly what the CPU told us.
Type quit to exit. The core dump remains on flash, so you can run idf.py coredump-debug again anytime without reproducing the crash.
Here are a few more commands worth knowing:
| Command | What it does |
|---|---|
info threads | List all FreeRTOS tasks at crash time |
thread N | Switch to another task’s context |
x/16xw $sp | Examine 16 words of memory at the stack pointer |
info registers | Full register dump for the current frame |
The info threads / thread N combination is particularly valuable when your crash occurs in a callback or timer task and you need to see what other tasks were doing at the same moment.
Fix the Bug, Verify the Fix
The fix is obvious here: don’t dereference a null pointer. In a real application, you’d add a null check, fix the initialization logic, or trace back to understand why the pointer was never assigned. For our example:
void app_main(void)
{
int value = 42;
int *p = &value;
vTaskDelay(pdMS_TO_TICKS(2000));
printf("Value: %d\n", *p); // prints 42, no crash
}Rebuild, reflash, confirm clean output in idf.py monitor. The loop closes: reproduce → capture → inspect → fix → verify.
A Brief Note on JTAG Debugging
Core dumps are post-mortem. You can’t set breakpoints, step through code, or watch a variable change in real time. For that, you need JTAG.
The ESP32-S3, ESP32-C3, and ESP32-C6 have a built-in USB-JTAG interface, so no extra hardware is required. For the original ESP32 and ESP32-S2, you’ll need an adapter like the ESP-Prog (~$15). Once connected, idf.py openocd starts the debug server, and you attach GDB to it for live debugging: breakpoints, watchpoints, step-by-step execution.
JTAG debugging deserves its own article. The official ESP-IDF JTAG documentation covers setup for each chip variant. Think of it as the next level up: powerful, but not required for most crash debugging. Monitor output and core dumps will solve the majority of your crashes without any additional hardware.
Your Debugging Checklist for the Next Crash
The next time your terminal fills with hex, don’t close your laptop. Follow this:
- Reproduce the crash with
idf.py monitorrunning. Read the decoded backtrace. Note the file, line number, and panic reason. - Capture a core dump (enable it once in
menuconfig, then it’s automatic). Runidf.py coredump-debugto get a full GDB session. - Inspect with
bt,frame,info locals, andlist. Identify the variable or state that caused the fault. - Fix the root cause, reflash, and verify the crash is gone.
Most ESP32 crashes (null pointers, stack overflows, uninitialized memory, bad peripheral access) are solvable with just these tools. The wall of hex was never the enemy. You just needed to know how to read it.
Hubble Network connects ESP32-class devices directly to satellites—no gateways, no infrastructure. See how it works →