Quick answer: Build your GDExtension with target=template_debug debug_symbols=yes, ship the .dSYM/.pdb sidecar alongside the library, and attach LLDB or the Visual Studio debugger to the running Godot process. Without symbols your stack trace is a list of hex addresses.
A GDExtension that crashes and prints ?? frames on Godot’s error panel is effectively unfixable until you get symbols wired up. The problem is split across three systems — your build flags, the debug template of Godot itself, and the debugger you attach after the fact. Set each correctly and you can drop a breakpoint inside your C++ code from the comfort of your IDE.
Build the extension with debug info
Most templates use SCons to build the shared library from godot-cpp. The relevant flags are target and debug_symbols:
# macOS / Linux
scons platform=macos target=template_debug debug_symbols=yes arch=universal
# Windows (MSVC)
scons platform=windows target=template_debug debug_symbols=yes
target=template_debug builds a library compatible with a debug template of Godot and enables assertions inside godot-cpp itself. debug_symbols=yes emits the DWARF or PDB data you need. If you set one without the other you get mismatched calling conventions or symbols that the debugger cannot map.
Keep the sidecar files near the library
On macOS and Linux, debug info can live inside the library or next to it as a .dSYM bundle. On Windows, MSVC emits a .pdb file separately. Both need to travel with the .so/.dylib/.dll or the debugger will load the library with no symbols even though they exist on disk.
A typical addon layout after a debug build looks like:
addons/myext/bin/
myext.macos.template_debug.framework/
myext.macos.template_debug.dSYM/
myext.windows.template_debug.x86_64.dll
myext.windows.template_debug.x86_64.pdb
Do not rename or move the sidecar during packaging. The debugger looks for it using the hash embedded in the binary.
Split DWARF for Linux
Linux builds with GCC produce very large binaries when debug info is inlined. Use -gsplit-dwarf in your build flags to emit a companion .dwo file per translation unit and a compact index in the library. LLDB and GDB both understand it transparently; the library itself stays small enough to load quickly.
# In your SCons customization
env.Append(CCFLAGS=["-g", "-gsplit-dwarf"])
Match a debug template
If you want the debugger to step into Godot’s own code as well as your extension, run a debug build of Godot. Download the editor build suffixed debug from the Godot site, or build from source with scons target=editor dev_build=yes. For most extension authors, running the stock editor plus a debug extension is enough — you can still break on your code, you just cannot step into engine internals.
Attach the debugger
Launch Godot normally. Find its PID (macOS activity monitor, pgrep Godot on Linux, Task Manager on Windows). Attach:
# macOS and Linux
lldb -p $(pgrep -n Godot)
(lldb) breakpoint set -n myext::MyNode::do_thing
(lldb) continue
In Visual Studio, use Debug -> Attach to Process, pick the Godot editor, and set breakpoints in your C++ project. When you trigger the code path in the running editor the breakpoint hits and you get a full backtrace with locals.
Capture crashes even without a debugger
For crashes you cannot reproduce on demand, enable core dumps (ulimit -c unlimited on Linux, procdump on Windows) and open the resulting file with the same symbols. Ship a stripped library to end users but keep the debug sidecar and core file on your build machine, and you can resolve stack frames after the fact.
“A GDExtension crash without symbols is noise. Ten minutes of build flag work turns it into a one-line fix.”
Related Issues
For runtime errors that look similar to crashes, see Fix Godot cannot call method null value. If you are also hunting missing stack traces across build configurations, Fix Unity ScriptableObject singleton null after build covers the editor-versus-build split in another engine.
Tip: keep a tiny reproducer scene that crashes the extension in your repo — makes attaching the debugger a two-minute exercise instead of twenty.