GDB / LLDB Memory & Logic Debugging
Systematic debugger workflows for diagnosing segfaults, memory leaks, use-after-free,
and logic errors in the nostrc C/GTK4 stack. Designed for LLM agents who can run
gdb/lldb commands via Bash and interpret structured output.
Prerequisites
- Linux:
gdbinstalled, app built with-g(debug symbols) - macOS:
lldbinstalled (ships with Xcode CLT) - Debug build:
cmake -B build -DCMAKE_BUILD_TYPE=Debug - Optional: ASan build:
-DGNOSTR_ENABLE_ASAN=ON -DGNOSTR_ENABLE_UBSAN=ON
Build Configurations
Debug (symbols, no optimization)
cmake -B build-debug \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-g3 -O0 -fno-omit-frame-pointer"
cmake --build build-debug
ASan + Debug (catches UAF, leaks, buffer overflows)
cmake -B build-asan \
-DCMAKE_BUILD_TYPE=Debug \
-DGNOSTR_ENABLE_ASAN=ON \
-DGNOSTR_ENABLE_UBSAN=ON
cmake --build build-asan
RelWithDebInfo (for profiling with symbols)
cmake -B build-rel \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build-rel
GDB Workflows (Linux)
Basic crash diagnosis
# Run app under GDB
gdb -ex run -ex bt -ex quit --args \
build-debug/apps/gnostr/gnostr
# If it crashes, GDB prints backtrace automatically
# For interactive debugging:
gdb --args build-debug/apps/gnostr/gnostr
(gdb) run
# ... crash occurs ...
(gdb) bt full # Full backtrace with locals
(gdb) info threads # List all threads (important for GTK apps)
(gdb) thread apply all bt # Backtrace ALL threads
Running a specific test under GDB
gdb --args build-debug/apps/gnostr/tests/gnostr-test-ndb-main-thread-violations
(gdb) run
# On failure:
(gdb) bt
(gdb) info locals
(gdb) print *item # Inspect GObject fields
GDB batch mode (for LLM agents)
Non-interactive — run commands and capture output:
gdb -batch \
-ex "set pagination off" \
-ex "set print pretty on" \
-ex "run" \
-ex "bt full" \
-ex "info threads" \
-ex "thread apply all bt" \
--args build-debug/apps/gnostr/gnostr 2>&1 | head -200
Breakpoints for common crash patterns
gdb --args build-debug/apps/gnostr/gnostr
(gdb) # Break on GLib critical warnings (often precede crashes)
(gdb) break g_log_default_handler
(gdb) # Break on specific function
(gdb) break factory_unbind_cb
(gdb) # Break on NDB transaction opens (latency investigation)
(gdb) break storage_ndb_begin_query
(gdb) # Conditional: only break on main thread
(gdb) break storage_ndb_begin_query if g_thread_self() == g_main_context_get_thread_default()
(gdb) run
GObject reference counting diagnosis
(gdb) # Print ref count of a GObject
(gdb) print ((GObject*)widget)->ref_count
# Watch for ref count going to 0 (finalization)
(gdb) watch ((GObject*)0x5555DEADBEEF)->ref_count
(gdb) condition 1 ((GObject*)0x5555DEADBEEF)->ref_count == 0
# Break on GObject dispose/finalize
(gdb) break g_object_unref if object == 0x5555DEADBEEF
Signal handler investigation
(gdb) # List all signal handlers on a GObject
(gdb) call g_signal_list_ids(G_OBJECT_TYPE(item), &n_ids)
(gdb) # Check if a specific handler is connected
(gdb) print g_signal_handler_is_connected(item, handler_id)
NDB transaction tracing
(gdb) # Set breakpoint on NDB transaction open
(gdb) break storage_ndb_begin_query
(gdb) commands
bt 5
continue
end
(gdb) run
# Every NDB transaction open prints a 5-frame backtrace
# Look for calls originating from the main thread GTK stack
Watchpoints for memory corruption
(gdb) # Watch a specific memory location
(gdb) watch *(int*)0x5555DEADBEEF
(gdb) # Watch a struct field
(gdb) watch self->disposed
(gdb) # Hardware watchpoint on the GObject ref count
(gdb) watch -l ((GObject*)self)->ref_count
LLDB Workflows (macOS)
Basic crash diagnosis
# Run app under LLDB
lldb -- build-debug/apps/gnostr/gnostr
(lldb) run
# ... crash occurs ...
(lldb) bt all # All threads backtrace
(lldb) frame variable # Local variables
(lldb) thread list # List threads
LLDB batch mode (for LLM agents)
lldb -b \
-o "run" \
-o "bt all" \
-o "thread list" \
-k "bt all" \
-k "quit" \
-- build-debug/apps/gnostr/gnostr 2>&1 | head -200
Breakpoints
(lldb) breakpoint set -n factory_unbind_cb
(lldb) breakpoint set -n storage_ndb_begin_query
(lldb) breakpoint set -n g_log_default_handler
# Conditional breakpoint
(lldb) breakpoint set -n storage_ndb_begin_query -c 'gn_test_on_main_thread'
# Break on GLib warnings
(lldb) breakpoint set -n g_logv
GObject inspection
(lldb) # Print ref count
(lldb) expr ((GObject*)widget)->ref_count
# Print GType name
(lldb) expr (char*)g_type_name(G_OBJECT_TYPE(item))
# Check signal handler
(lldb) expr (int)g_signal_handler_is_connected(item, handler_id)
Watchpoints
(lldb) watchpoint set expression -- ((GObject*)self)->ref_count
(lldb) watchpoint set variable self->disposed
ASan Integration
Interpreting ASan output
ASan reports look like:
=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
READ of size 8 at 0x... thread T0
#0 0x... in factory_bind_cb note-card-factory.c:245
#1 0x... in gtk_list_item_factory_bind ...
#2 0x... in gtk_list_view_activate_item ...
previously freed by thread T3:
#0 0x... in g_object_unref ...
#1 0x... in gn_nostr_event_item_finalize ...
#2 0x... in on_sync_complete sync-service.c:89
Reading the report:
- First block: Where the crash happened (accessing freed memory)
- Second block: Where the memory was freed (the root cause)
- Thread info: T0 = main thread, T3 = worker — this is a threading bug
ASan + GDB combo (Linux)
# ASan can break into GDB on error
ASAN_OPTIONS=abort_on_error=1 \
gdb -ex run -ex bt --args build-asan/apps/gnostr/gnostr
ASan + LLDB combo (macOS)
ASAN_OPTIONS=abort_on_error=1 \
lldb -o run -k "bt all" -k quit \
-- build-asan/apps/gnostr/gnostr
Leak detection
# Linux only (LSAN is part of ASan)
ASAN_OPTIONS=detect_leaks=1 \
build-asan/apps/gnostr/tests/gnostr-test-lifecycle-leaks
Suppression files
For known leaks in third-party libraries:
# Create suppressions file
cat > /tmp/lsan.supp << 'EOF'
leak:libpango
leak:libfontconfig
leak:g_type_register_static
EOF
LSAN_OPTIONS=suppressions=/tmp/lsan.supp \
build-asan/apps/gnostr/gnostr
Structured Debug Workflows for LLM Agents
Workflow 1: Diagnose a segfault
1. Reproduce:
gdb -batch -ex run -ex "bt full" -ex "thread apply all bt" \
--args build-debug/apps/gnostr/gnostr 2>&1
2. Parse the backtrace:
- Identify the crashing function and line number
- Read the source file at that line
- Check if it's a NULL deref, UAF, or logic error
3. If UAF suspected, rebuild with ASan:
cmake --build build-asan
build-asan/apps/gnostr/gnostr 2>&1
4. ASan report shows:
- WHERE it crashed (read/write of freed memory)
- WHERE it was freed (the actual bug location)
- WHICH thread freed it vs which thread accessed it
5. Fix the root cause (usually a missing g_object_ref, or
a callback accessing freed user_data)
Workflow 2: Track down a GObject leak
1. Run under Valgrind or ASan with leak detection:
ASAN_OPTIONS=detect_leaks=1 \
build-asan/apps/gnostr/tests/gnostr-test-lifecycle-leaks
2. If leak reported, check the allocation backtrace
3. In GDB, set a breakpoint on the constructor:
(gdb) break gn_nostr_event_item_new
(gdb) commands
print $rax # Return value = the new object pointer
continue
end
4. After the test, check if all objects were finalized:
- Use gn_test_watch_object() / gn_test_assert_finalized()
- Or check ref counts at test end
Workflow 3: Find main-thread blocking
1. Run with NDB violation detection:
build-debug/apps/gnostr/tests/gnostr-test-ndb-main-thread-violations
2. If violations detected, use GDB to get the full call chain:
gdb --args build-debug/apps/gnostr/tests/gnostr-test-ndb-main-thread-violations
(gdb) break gn_test_record_violation
(gdb) commands
bt 15
continue
end
(gdb) run
3. Each backtrace shows exactly how the main thread reached
the NDB transaction — trace back to find the entry point
that should be async
4. Fix: Move the call to g_task_run_in_thread() or
go_blocking_submit()
Workflow 4: Debug signal handler lifetime
1. In GDB, break on signal connect and disconnect:
(gdb) break g_signal_connect_data
(gdb) break g_signal_handler_disconnect
(gdb) # Track handler IDs being connected vs disconnected
2. Run the recycling stress test:
gdb --args build-debug/nostr-gtk/tests/nostr-gtk-test-listview-recycle
3. On crash, check:
(gdb) print handler_id # The handler that wasn't disconnected
(gdb) print item # The source object
(gdb) print row # The user_data (possibly freed)
GLib Debug Environment Variables
| Variable | Value | Purpose |
|---|---|---|
G_DEBUG |
fatal-warnings |
Abort on GLib warnings |
G_DEBUG |
fatal-warnings,gc-friendly |
+ GC-friendly for leak detection |
G_SLICE |
always-malloc |
Disable slab allocator (needed for ASan) |
G_MESSAGES_DEBUG |
all |
Print all debug messages |
G_MESSAGES_DEBUG |
GLib-GObject |
Print only GObject messages |
GOBJECT_DEBUG |
objects |
Track all GObject instances |
GOBJECT_DEBUG |
signals |
Trace signal emissions |
GTK_DEBUG |
actions |
Trace GTK actions |
GTK_DEBUG |
layout |
Trace layout/sizing |
Combining for maximum diagnostics
G_DEBUG=fatal-warnings,gc-friendly \
G_SLICE=always-malloc \
G_MESSAGES_DEBUG=all \
GOBJECT_DEBUG=objects \
gdb --args build-debug/apps/gnostr/gnostr
Platform-Specific Notes
Linux
- GDB is the primary debugger
- ASan/LSAN fully supported
/proc/self/mapsfor memory mapping inspectionstrace -e trace=mmap,brkfor allocation tracing- Valgrind available as alternative:
valgrind --leak-check=full ./app
macOS
- LLDB is the primary debugger (GDB not well-supported on modern macOS)
- ASan supported via
-fsanitize=address(Clang) - LSAN has limited support on macOS — use
leaksinstead:MallocStackLogging=1 build-debug/apps/gnostr/gnostr & leaks --atExit -- $(pgrep gnostr) - Instruments.app for profiling (Allocations, Leaks, Time Profiler)
malloc_historyfor allocation tracking
Related
skills/broadway-debug/SKILL.md— UI debugging via Broadwayskills/gtk-inspector/SKILL.md— GTK Inspector for widget/CSS/signal/performance debuggingskills/closed-loop-debug/SKILL.md— Full debug workflowdocs/TESTING.md— Test suite documentation
Converted and distributed by TomeVault — claim your Tome and manage your conversions.