C++ GDB: A Practical Debugger Guide
이 글의 핵심
Use C++ GDB for breakpoints, stepping, variable inspection, and backtraces. Covers -g builds, core analysis, and multithreaded debugging with practical examples.
Introduction
GDB (GNU Debugger) is essential for debugging C and C++ programs. Breakpoints, variable inspection, stack traces, and more help you find and fix bugs quickly.
What production is really like
When you learn to develop, everything feels neat and theoretical. Production is different: legacy code, tight deadlines, and bugs you did not expect. The topics here started as theory, but applying them in real projects is when the design choices clicked.
What stuck with me was trial and error on an early project. I followed the book and still could not see why things failed for days. A senior’s review surfaced the issue, and I learned a lot in the process. This article covers not only theory but pitfalls and fixes you are likely to hit in practice.
1. GDB basics
Installation
Run the following in your terminal.
# Ubuntu/Debian
sudo apt install gdb
# macOS (LLDB is often preferred)
brew install gdb
# Windows (MinGW)
# gdb is included with MinGW
Compile and run
Run the following in your terminal.
# Include debug info (-g)
g++ -g program.cpp -o program
# Disable optimization (easier debugging)
g++ -g -O0 program.cpp -o program
# Or debug-friendly optimization
g++ -g -Og program.cpp -o program
# Start GDB
gdb ./program
# Run the program
(gdb) run
# Run with arguments
(gdb) run arg1 arg2
Key ideas:
- -g: embeds debug info (symbols, line numbers)
- -O0: no optimization (variables are not optimized away)
- -Og: optimization level suited to debugging
2. Essential commands
Execution control
Run the following in your terminal.
# Run the program
(gdb) run # from the start
(gdb) run arg1 arg2 # with arguments
(gdb) continue (c) # continue to next breakpoint
(gdb) next (n) # next line (step over calls)
(gdb) step (s) # next line (step into calls)
(gdb) finish # run until current function returns
(gdb) until # run until end of current loop
(gdb) quit (q) # exit GDB
Breakpoints
Run the following in your terminal.
# Set breakpoints
(gdb) break main # at a function
(gdb) break file.cpp:42 # at file:line
(gdb) break MyClass::method # at a method
(gdb) break +5 # five lines ahead of current position
# Conditional breakpoints
(gdb) break factorial if n == 3
(gdb) condition 1 x > 100 # add condition to breakpoint 1
# Manage breakpoints
(gdb) info breakpoints # list breakpoints
(gdb) delete 1 # delete breakpoint 1
(gdb) delete # delete all breakpoints
(gdb) disable 1 # disable breakpoint 1
(gdb) enable 1 # enable breakpoint 1
Inspecting variables
Run the following in your terminal.
# Print variables
# Example session
(gdb) print var # value
(gdb) print &var # address
(gdb) print *ptr # pointed-to value
(gdb) print arr[0] # array element
(gdb) print obj.member # member
# Auto-print on each stop
(gdb) display var # enable auto-print
(gdb) undisplay 1 # remove auto-print
# Inspect state
(gdb) info locals # local variables
(gdb) info args # function arguments
(gdb) info variables # static/global file scope (where available)
Stack traces
Run the following in your terminal.
# Stack traces
(gdb) backtrace (bt) # full stack
(gdb) backtrace 5 # last five frames
(gdb) frame 0 # select frame 0
(gdb) up # toward caller
(gdb) down # toward callee
(gdb) info frame # current frame info
3. Hands-on examples
Example 1: Basic debugging
A minimal factorial example.
// program.cpp
#include <iostream>
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main() {
int result = factorial(5);
std::cout << "Result: " << result << std::endl;
return 0;
}
GDB session:
Run the following in your terminal.
# Compile
$ g++ -g program.cpp -o program
# Start GDB
$ gdb ./program
# Set a breakpoint
(gdb) break factorial
Breakpoint 1 at 0x1189: file program.cpp, line 5.
# Run
(gdb) run
Starting program: ./program
Breakpoint 1, factorial (n=5) at program.cpp:5
# Inspect a variable
(gdb) print n
$1 = 5
# Continue to next hit
(gdb) continue
Breakpoint 1, factorial (n=4) at program.cpp:5
# Stack trace
(gdb) backtrace
#0 factorial (n=4) at program.cpp:5
#1 0x0000555555555195 in factorial (n=5) at program.cpp:6
#2 0x00005555555551b5 in main () at program.cpp:10
# Quit
(gdb) quit
Example 2: Conditional breakpoints
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int i = 0; i < numbers.size(); ++i) {
int value = numbers[i] * 2;
std::cout << value << std::endl;
}
return 0;
}
GDB session:
Run the following in your terminal.
# Break only when i == 5
(gdb) break 7 if i == 5
(gdb) run
# Stops only when i is 5
# Verify
(gdb) print i
$1 = 5
(gdb) print value
$2 = 12
Example 3: Watchpoints
#include <iostream>
int main() {
int counter = 0;
for (int i = 0; i < 10; ++i) {
counter += i;
if (counter > 20) {
counter = 0; // bug: reset here
}
}
std::cout << "Final: " << counter << std::endl;
return 0;
}
GDB session:
Run the following in your terminal.
# Stop when counter changes
(gdb) watch counter
(gdb) run
# GDB stops on each change
Hardware watchpoint 2: counter
Old value = 0
New value = 1
# Continue and observe changes
(gdb) continue
Example 4: Core dump analysis
#include <iostream>
void crash() {
int* ptr = nullptr;
*ptr = 42; // Segmentation fault!
}
int main() {
crash();
return 0;
}
Analyzing a core dump:
Run the following in your terminal.
# Allow core files
$ ulimit -c unlimited
# Run
$ ./program
Segmentation fault (core dumped)
# Open the core in GDB
$ gdb ./program core
# Where did it crash?
(gdb) backtrace
#0 0x0000555555555189 in crash () at program.cpp:5
#1 0x00005555555551a5 in main () at program.cpp:10
# Inspect the crashing frame
(gdb) frame 0
#0 0x0000555555555189 in crash () at program.cpp:5
5 *ptr = 42;
# Inspect variables
(gdb) print ptr
$1 = (int *) 0x0
4. Advanced features
Memory inspection
Run the following in your terminal.
# Examine memory (x = examine)
(gdb) x/10x address # 10 words in hex
(gdb) x/10d address # 10 words in decimal
(gdb) x/10c address # 10 bytes as characters
(gdb) x/s address # null-terminated string
(gdb) x/10i address # 10 instructions (disassembly)
# Example
(gdb) print &var
$1 = (int *) 0x7fffffffe3fc
(gdb) x/4x 0x7fffffffe3fc
0x7fffffffe3fc: 0x0000000a 0x00000000 0xf7dc2620 0x00007fff
Type information
# Types
(gdb) ptype var # detailed type
(gdb) whatis var # simple type
# Example
(gdb) ptype std::vector<int>
type = class std::vector<int, std::allocator<int>> {
...
}
Multithreaded debugging
Run the following in your terminal.
# List threads
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7fc0740 (LWP 12345) main () at main.cpp:10
2 Thread 0x7ffff6fbf700 (LWP 12346) worker () at worker.cpp:5
# Switch thread
(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff6fbf700)]
# Backtrace for current thread
(gdb) backtrace
# Backtrace for all threads
(gdb) thread apply all backtrace
Reverse debugging
Run the following in your terminal.
# Start recording
(gdb) record
(gdb) continue
# Execute backward
(gdb) reverse-step
(gdb) reverse-next
(gdb) reverse-continue
(gdb) reverse-finish
5. Common problems
Problem 1: No debug info
Run the following in your terminal.
# ❌ No debug info
$ g++ program.cpp -o program
$ gdb ./program
(gdb) list
No symbol table is loaded.
# ✅ Add -g
$ g++ -g program.cpp -o program
$ gdb ./program
(gdb) list
1 #include <iostream>
2
3 int factorial(int n) {
...
Fix: Always compile with -g when you need source-level debugging.
Problem 2: Variables optimized out
Example main:
#include <iostream>
int main() {
int x = 10;
int y = x * 2;
int z = y + 5;
std::cout << z << std::endl;
return 0;
}
Run the following in your terminal.
# ❌ -O3
$ g++ -g -O3 program.cpp -o program
$ gdb ./program
(gdb) break main
(gdb) run
(gdb) print x
$1 = <optimized out>
# ✅ -O0 or -Og
$ g++ -g -O0 program.cpp -o program
$ gdb ./program
(gdb) print x
$1 = 10
Fix: Use -O0 or -Og while debugging.
Problem 3: Stripped symbols
Run the following in your terminal.
# ❌ After strip
$ strip program
$ gdb ./program
(gdb) break main
Function "main" not defined.
# ✅ Do not strip debug builds
# Or keep a separate symbol file
$ objcopy --only-keep-debug program program.debug
$ strip program
$ objcopy --add-gnu-debuglink=program.debug program
Fix: Do not strip binaries you still need to debug—or keep split debug info.
Problem 4: Multithreaded debugging
#include <iostream>
#include <thread>
void worker(int id) {
for (int i = 0; i < 5; ++i) {
std::cout << "Thread " << id << ": " << i << std::endl;
}
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
return 0;
}
GDB session:
Run the following in your terminal.
(gdb) break worker
(gdb) run
[New Thread 0x7ffff6fbf700 (LWP 12346)]
Thread 2 "program" hit Breakpoint 1, worker (id=1) at program.cpp:5
(gdb) info threads
Id Target Id Frame
* 2 Thread 0x7ffff6fbf700 (LWP 12346) worker (id=1) at program.cpp:5
1 Thread 0x7ffff7fc0740 (LWP 12345) 0x00007ffff7bc0a9d in __pthread_join
(gdb) thread 1
(gdb) backtrace
6. TUI mode
TUI (Text User Interface) shows source in the terminal alongside GDB.
Run the following in your terminal.
# Start in TUI
$ gdb -tui ./program
# Or toggle while running
(gdb) tui enable
(gdb) tui disable
# Layouts
(gdb) layout src # source
(gdb) layout asm # disassembly
(gdb) layout split # source + asm
(gdb) layout regs # registers + source
# Window focus / refresh
Ctrl+X, A # toggle TUI
Ctrl+X, O # next window
Ctrl+L # refresh screen
7. Practical example: finding a bug
#include <iostream>
#include <vector>
double average(const std::vector<int>& numbers) {
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum / numbers.size(); // bug: integer division!
}
int main() {
std::vector<int> scores = {85, 92, 78, 95, 88};
double avg = average(scores);
std::cout << "Average: " << avg << std::endl;
return 0;
}
Finding the bug with GDB:
# Build and run
$ g++ -g bug.cpp -o bug
$ ./bug
Average: 87 # expected: 87.6
$ gdb ./bug
(gdb) break average
(gdb) run
Breakpoint 1, average (numbers=...) at bug.cpp:5
(gdb) next
(gdb) next
...
(gdb) print sum
$1 = 438
(gdb) print numbers.size()
$2 = 5
(gdb) ptype sum
type = int
(gdb) ptype numbers.size()
type = std::size_t
# Issue: int / size_t promotes to integer division
# Fix: static_cast<double>(sum) / numbers.size()
8. GDB command cheat sheet
| Category | Command | Description |
|---|---|---|
| Run | run | Start the program |
continue (c) | Continue to next breakpoint | |
next (n) | Next line (step over) | |
step (s) | Next line (step into) | |
finish | Until current function returns | |
| Breakpoints | break | Set a breakpoint |
watch | Set a watchpoint | |
info breakpoints | List breakpoints | |
delete | Delete breakpoints | |
| Inspect | print | Print an expression |
display | Auto-print each stop | |
info locals | Local variables | |
backtrace (bt) | Call stack | |
| Memory | x | Examine memory |
ptype | Type information | |
| Threads | info threads | List threads |
thread | Switch thread |
Summary
Key takeaways
- GDB: the standard C/C++ debugger
- -g: required for rich debug info
- Breakpoints: use
break - Variables:
print,display - Stack:
backtracefor call chains - Conditional breaks:
break ... if - Watchpoints:
watchfor changes - Core dumps: post-mortem crash analysis
Practical tips
Building:
- Prefer
-g -O0or-g -Ogfor debug builds - Release builds often use
-O2or-O3 - Reserve
stripfor release artifacts (or use split debug info)
Debugging:
- Use conditional breakpoints to catch rare cases
- Use
watchfor unexpected mutations - TUI mode helps you stay oriented in source
backtracequickly localizes crashes
Efficiency:
- Use few, well-placed breakpoints to narrow the problem
displaykeeps important values visible- For threads,
info threadsshows the global picture