C++ GDB: A Practical Debugger Guide

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

CategoryCommandDescription
RunrunStart the program
continue (c)Continue to next breakpoint
next (n)Next line (step over)
step (s)Next line (step into)
finishUntil current function returns
BreakpointsbreakSet a breakpoint
watchSet a watchpoint
info breakpointsList breakpoints
deleteDelete breakpoints
InspectprintPrint an expression
displayAuto-print each stop
info localsLocal variables
backtrace (bt)Call stack
MemoryxExamine memory
ptypeType information
Threadsinfo threadsList threads
threadSwitch thread

Summary

Key takeaways

  1. GDB: the standard C/C++ debugger
  2. -g: required for rich debug info
  3. Breakpoints: use break
  4. Variables: print, display
  5. Stack: backtrace for call chains
  6. Conditional breaks: break ... if
  7. Watchpoints: watch for changes
  8. Core dumps: post-mortem crash analysis

Practical tips

Building:

  • Prefer -g -O0 or -g -Og for debug builds
  • Release builds often use -O2 or -O3
  • Reserve strip for release artifacts (or use split debug info)

Debugging:

  • Use conditional breakpoints to catch rare cases
  • Use watch for unexpected mutations
  • TUI mode helps you stay oriented in source
  • backtrace quickly localizes crashes

Efficiency:

  • Use few, well-placed breakpoints to narrow the problem
  • display keeps important values visible
  • For threads, info threads shows the global picture

Next steps