Skip to content

Debugging with GDB, LLDB, and Visual Studio

A native debugger controls a process, maps machine locations back to source using debug information, and lets you inspect stacks, variables, registers, memory, threads, and exceptions or signals. Interfaces differ; the evidence model is shared.

Build debuggable artifacts

For GCC-compatible drivers:

sh
c++ -std=c++20 -g -O0 main.cpp -o app

-g emits debug information. -O0 makes source stepping straightforward, but optimized debugging is both possible and necessary for release-only failures. Optimized variables may be unavailable, statements reordered, and functions inlined.

MSVC commonly uses /Zi plus linker debug settings, configured by the Debug project configuration. Debug information format and file layout are platform-specific.

Start with the failing process contract

Record executable, arguments, working directory, environment, input, architecture, and exact build. IDE launch settings frequently differ from a terminal. Attach to an existing process only when launch itself is not relevant.

In GDB or LLDB, typical operations are conceptually:

text
set breakpoint at function/file:line
run with arguments
continue
step into / step over / step out
show backtrace
select stack frame
inspect variable/expression/memory

Commands spell these differently; learn the concepts first. Visual Studio maps them to windows and shortcuts.

The stack trace is a causal map

On a crash, stop at the fault and inspect the complete stack. The top frame shows where the invalid operation surfaced, not necessarily where the bad pointer or corrupted value originated. Walk toward callers, inspect ownership transitions, and use watchpoints or sanitizers to catch earlier mutation.

Debuggers display what the current build emitted. If source and binary differ, breakpoints move or variables make no sense. Clean rebuild and verify loaded module/symbol paths before inventing a language explanation.

Watchpoints find writes

A data breakpoint/watchpoint stops when memory changes. It can identify who corrupts a field, though hardware supports only limited watched ranges. Establish a valid address and lifetime before setting one; watching freed or moved storage answers the wrong question.

Threads require a global view

List threads and inspect each stack during a deadlock. Look for mutex ownership, waits, and inconsistent lock order. Pausing under a debugger changes timing, so combine interactive inspection with ThreadSanitizer, logging, and reproducible stress tests.

Core dumps and crash reports preserve failures

Unix-like systems can capture core dumps subject to policy; debuggers load the executable, core, and matching symbols. Windows dump files and Apple crash reports serve related purposes. Symbol files must match the exact binary. Archive release symbols even when shipping stripped artifacts.

Debuggers and sanitizers complement each other

A debugger shows state at a chosen stop. Sanitizers insert checks that often stop closer to the first invalid access and retain allocation stacks. Start with sanitizer evidence when available, then use the debugger to explore the reported state.

Optional prompts

Debug: Breakpoints are hollow and variables nonsense. What should be verified before code logic?

Answer: Confirm the debugger loaded the intended binary and matching debug symbols from the current source/build configuration.

Explain: Why can the crashing line be downstream from the defect?

Answer: Memory may have been corrupted or freed earlier; the crash occurs only when invalid state is later dereferenced or interpreted.

Further reference