GDB — GNU Debugger
"GDB reference for C++ developers: breakpoints, watchpoints, stack inspection, STL pretty-printing, TUI mode, and scripting."
TL;DR
GDB is the standard debugger on Linux. Compile with -g (and -O0 or -Og for readable variables), then use gdb ./myapp. The TUI mode (Ctrl+X A) gives a split source/command view.
g++ -g -O0 -o myapp main.cpp
gdb ./myapp
(gdb) run arg1 arg2Essential commands
# Start
gdb ./myapp
(gdb) run # run program
(gdb) run arg1 arg2 # run with arguments
(gdb) set args arg1 arg2 # set args for future runs
# Breakpoints
(gdb) break main # break at function
(gdb) break file.cpp:42 # break at line
(gdb) break Widget::update # break at member function
(gdb) info breakpoints # list all
(gdb) delete 2 # delete breakpoint #2
(gdb) disable 3 # disable without deleting
(gdb) condition 1 x > 10 # break only when x > 10
# Execution control
(gdb) continue # (c) run until next breakpoint
(gdb) next # (n) step over
(gdb) step # (s) step into
(gdb) finish # run until return from current function
(gdb) until 55 # run until line 55
# Inspection
(gdb) print x # (p) print variable
(gdb) print *ptr # dereference and print
(gdb) print arr[0]@5 # print 5 elements of arr
(gdb) display x # print x after every step
(gdb) info locals # all local variables
(gdb) info args # function arguments
# Stack
(gdb) backtrace # (bt) print call stack
(gdb) frame 2 # switch to frame 2
(gdb) up / down # move up/down in the stack
(gdb) info frame # current frame detailsWatchpoints — break on data changes
# Break when variable changes value
(gdb) watch x
# Break when memory at address is written
(gdb) watch *(int*)0xdeadbeef
# Break when read
(gdb) rwatch x
# Break on read or write
(gdb) awatch xSTL pretty-printing
GDB includes Python pretty-printers for the STL. They should be active by default on modern GDB + libstdc++.
(gdb) print v # std::vector<int> — prints as [1, 2, 3, 4]
(gdb) print m # std::map — prints as {{"key", 1}, {"key2", 2}}
(gdb) print opt # std::optional — prints value or "no value"
# If not active, load manually:
(gdb) python import sys; sys.path.insert(0, '/usr/share/gcc-*/python')
(gdb) python from libstdcxx.v6.printers import register_libstdcxx_printers
(gdb) python register_libstdcxx_printers(None)TUI mode
The Text User Interface splits the terminal into source and command panels.
Ctrl+X A — toggle TUI on/off
Ctrl+X 1 — single window (source only)
Ctrl+X 2 — two windows (source + asm, or source + registers)
Ctrl+L — refresh display (after garbled output)
Ctrl+X O — switch focus between windows
Ctrl+P / Ctrl+N — scroll command history (in focus mode)Core dumps
# Enable core dumps (Linux)
ulimit -c unlimited
# Run crashing program
./myapp
# Produces: core (or core.PID)
# Debug the core
gdb ./myapp core
(gdb) bt # backtrace — see crash location
(gdb) info locals # state at crashAttaching to a running process
gdb -p 12345 # attach to PID
# or:
gdb ./myapp
(gdb) attach 12345
(gdb) detach # detach without killingScripting GDB
.gdbinit — startup commands
# ~/.gdbinit or ./.gdbinit
set print pretty on
set print object on
set print static-members off
set history save on
set disassembly-flavor intel
define print_vec
print $arg0._M_impl._M_start@($arg0._M_impl._M_finish-$arg0._M_impl._M_start)
endPython scripting
# custom-commands.py
import gdb
class PrintVector(gdb.Command):
def __init__(self):
super().__init__("pvec", gdb.COMMAND_DATA)
def invoke(self, arg, from_tty):
val = gdb.parse_and_eval(arg)
# access internals...
PrintVector()(gdb) source custom-commands.py
(gdb) pvec my_vectorReverse debugging (rr)
For time-travel debugging (replay execution backward), use rr instead of gdb:
rr record ./myapp
rr replay
(rr) reverse-continue # run backward to previous breakpoint
(rr) reverse-next # step backward
(rr) reverse-finish # step out backwardrr records a deterministic execution trace and replays it, making intermittent bugs trivially reproducible.
GDB with CMake
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
gdb ./build/myappUse Debug build type — it sets -g -O0 automatically. RelWithDebInfo (-g -O2) is good for profiling where you want symbols but not fully unoptimized code.
Multi-Thread Debugging
GDB is thread-aware by default, but all threads run freely unless you lock the scheduler. The key insight is that when you hit a breakpoint on thread 1, threads 2–N keep executing — which is often not what you want during careful single-step debugging.
(gdb) info threads # list all threads with their IDs and locations
(gdb) thread 3 # switch to thread 3
(gdb) thread apply all bt # print backtrace for every thread (crash triage)
(gdb) thread apply 1 2 print x # run command on specific threadsLocking the scheduler prevents other threads from advancing while you step through code on the current thread:
(gdb) set scheduler-locking on # only current thread runs during step/next
(gdb) set scheduler-locking off # default: all threads run freely
(gdb) set scheduler-locking step # lock only during step; free during continueThread-specific breakpoints let you break only when a particular thread hits a location — useful when a data race manifests only in the writer thread:
(gdb) break Widget::update thread 3 # break only when thread 3 hits this function
(gdb) break file.cpp:88 thread 2 # break at line only for thread 2For crash dump triage, thread apply all bt is the first command to run. It prints every thread's full call stack in one shot, letting you immediately see which thread faulted and which threads were blocked in system calls.
Conditional Breakpoints and Commands
Conditions and command lists turn GDB into a lightweight tracing tool without recompiling. You can count executions, log values, or perform complex filtering — all from the GDB prompt.
(gdb) break processItem # set breakpoint #1
(gdb) condition 1 x > 100 # only stop when x > 100
(gdb) condition 1 # clear the condition (break always)The commands directive attaches a GDB command list to a breakpoint. When the breakpoint fires, GDB executes the list automatically:
(gdb) break processItem
(gdb) commands 1
silent # suppress the default "Breakpoint N" message
print x
print item.name
continue # resume immediately — turns it into a trace point
endsilent suppresses the breakpoint banner, and ending with continue makes the breakpoint non-stopping: GDB logs the values and keeps running. This is the idiomatic way to count how many times a code path executes or to record variable values across thousands of iterations without modifying the source:
(gdb) set $count = 0
(gdb) break expensivePath
(gdb) commands 2
silent
set $count = $count + 1
continue
end
(gdb) run
(gdb) print $count # how many times expensivePath was hitRemote Debugging with gdbserver
gdbserver decouples the debugger UI from the process being debugged, making it essential for embedded Linux, containers, and any scenario where running a full GDB on the target is impractical.
On the target machine (or embedded board), start the program under gdbserver:
# Target: listen on TCP port 1234
gdbserver :1234 ./myapp arg1 arg2
# Target: attach to an already-running process
gdbserver :1234 --attach 5678On the host machine, connect GDB to the remote stub:
(gdb) target remote 192.168.1.10:1234 # connect to target
(gdb) target remote :1234 # localhost (e.g., a container)
(gdb) continue # the program is already paused at entryFor cross-compilation workflows, use the matching cross-GDB binary so that it understands the target architecture. The executable on the host must be the same one deployed to the target (same build artifacts, with debug info):
# Cross-compiled for ARM: use the ARM-aware GDB on your x86 host
arm-linux-gnueabihf-gdb ./arm-binary
(gdb) target remote 192.168.1.10:1234
(gdb) set sysroot /path/to/target/rootfs # resolve shared library symbolsQEMU user-mode emulation works the same way — useful for testing ARM binaries on an x86 CI machine:
# Terminal 1: run ARM binary under QEMU, halt at entry waiting for debugger
qemu-arm -g 1234 ./arm-binary
# Terminal 2: connect cross-GDB
arm-linux-gnueabihf-gdb ./arm-binary
(gdb) target remote localhost:1234
(gdb) break main
(gdb) continuePython Scripting Deep Dive
GDB's Python API exposes the full type system and inferior state, letting you write debugger commands that understand your data structures at a semantic level — far beyond what define macros can express.
gdb.parse_and_eval evaluates an expression in the current frame and returns a gdb.Value. From there you can walk fields, dereference pointers, and cast types using the same names they have in C++:
# Walk a std::vector manually (bypassing the pretty-printer)
import gdb
class InspectVector(gdb.Command):
"""Usage: ivec <vector-expression>"""
def __init__(self):
super().__init__("ivec", gdb.COMMAND_DATA)
def invoke(self, arg, from_tty):
vec = gdb.parse_and_eval(arg)
start = vec["_M_impl"]["_M_start"]
finish = vec["_M_impl"]["_M_finish"]
size = int(finish - start)
print(f"size = {size}")
for i in range(size):
print(f" [{i}] = {start[i]}")
InspectVector()You can subclass gdb.Breakpoint to attach Python logic that runs every time the breakpoint fires. The stop() method returns True to halt execution or False to continue silently:
import gdb
class CountingBreakpoint(gdb.Breakpoint):
def __init__(self, spec):
super().__init__(spec)
self.hit_count = 0
def stop(self):
self.hit_count += 1
frame = gdb.selected_frame()
# Log the caller each time, but only stop every 100th hit
if self.hit_count % 100 == 0:
print(f"Hit #{self.hit_count}: called from {frame.older().name()}")
return True # stop here
return False # keep running
bp = CountingBreakpoint("Widget::render")gdb.events provides hooks that fire on GDB events. stop fires every time the inferior halts — at a breakpoint, watchpoint, signal, or single-step:
import gdb
def on_stop(event):
frame = gdb.selected_frame()
sal = frame.find_sal() # source and line info
if sal.symtab:
print(f"Stopped in {frame.name()} at {sal.symtab.filename}:{sal.line}")
gdb.events.stop.connect(on_stop)Load scripts with source script.py or place them in .gdbinit via source. The full API reference ships with GDB: run help python inside GDB for a starting point.
GDB Dashboard and .gdbinit Patterns
A well-crafted .gdbinit eliminates repetitive setup and turns GDB into a persistent tool with memory — saved breakpoints, sane defaults, and automatic display rules that survive across sessions.
Essential settings to add to ~/.gdbinit:
# ~/.gdbinit
# Usability
set pagination off # never pause output with "---Type <return>---"
set confirm off # skip "Delete all breakpoints? (y or n)" prompts
set print thread-events off # silence "[New Thread ...]" noise
set print pretty on # indent nested structures
set print object on # print derived type, not declared type
set history save on # persist command history across sessions
set history size 10000
# Signal handling
handle SIGPIPE noprint nostop # ignore broken-pipe signals (common in network code)
handle SIGUSR1 noprint nostop # ignore app-defined signals unless you need them
# Automatically show context at every stop
define hook-stop
frame 0
endThe define hook-stop trick runs automatically every time the inferior stops. Combine it with info locals or a custom Python printer to get an instant variable dump on every breakpoint hit.
Save and restore breakpoints so you don't retype them after restarting GDB:
(gdb) save breakpoints session.bp # write all current breakpoints to a file
(gdb) source session.bp # reload them (also works from .gdbinit)gdb-dashboard is a community .gdbinit plugin that renders a rich terminal dashboard — registers, source, assembly, stack, and variables — using ANSI color codes. Install it with:
pip install gdb-dashboard # or: wget -P ~ git.io/gdb-dashboard
# Then add to ~/.gdbinit:
# source ~/dashboard.pyAfter sourcing, the dashboard renders automatically on every stop. Use dashboard -layout source stack variables to pick which modules appear. Individual modules can be toggled: dashboard source -enabled off.
Inspecting Memory and Assembly
Drop to raw memory and assembly when source-level debugging isn't enough — corrupted heap, JIT-generated code, or a crash deep inside a stripped library all require working at this level.
The x (examine) command reads raw memory in any format. The syntax is x/NFU addr where N is count, F is format, and U is unit size:
(gdb) x/20xw 0x7fffffffd000 # 20 words (4-byte) as hex — stack inspection
(gdb) x/10xg $rsp # 10 giant-words (8-byte) from stack pointer
(gdb) x/s 0x4006a0 # examine as null-terminated string
(gdb) x/10i $pc # disassemble 10 instructions from current PC
(gdb) x/b &myByte # single byte in hex
# Common format letters: x=hex, d=decimal, u=unsigned, o=octal, t=binary, s=string, i=instruction
# Common unit letters: b=byte, h=halfword(2), w=word(4), g=giant(8)disassemble gives a cleaner view for a whole function. The /m flag interleaves source lines with assembly — invaluable for understanding what the compiler did to your code:
(gdb) disassemble main # disassemble entire function
(gdb) disassemble /m Widget::update # interleaved source + assembly
(gdb) disassemble 0x400550,0x4005a0 # disassemble address rangeSwitch to Intel syntax if you prefer it (AT&T is the GDB default on x86):
(gdb) set disassembly-flavor intel # Intel: mov eax, 1
(gdb) set disassembly-flavor att # AT&T: movl $1, %eax (default)TUI assembly layout overlays the disassembly window alongside source, and updates the highlighted line as you step:
Ctrl+X 2 — cycle to asm+source layout in TUI
(gdb) layout asm — switch TUI to assembly view
(gdb) layout split — source on top, assembly on bottom
(gdb) layout regs — add register panelinfo proc mappings (Linux) or info memory-map shows the full virtual address layout of the process — useful for distinguishing stack addresses from heap addresses from mapped libraries when a pointer value looks suspicious:
(gdb) info proc mappings # virtual memory map: address ranges + permissions
(gdb) info sharedlibrary # loaded shared libraries and their address ranges
(gdb) maintenance info sections # all ELF sections and their load addressesSee Also
- LLDB — LLVM's debugger; preferred on macOS and with Clang-built binaries
- rr — time-travel debugger built on GDB; record once, replay deterministically
- Valgrind — memory error detector and heap profiler; no recompilation required
- Address Sanitizer — compile-time instrumentation for memory bugs; faster than Valgrind