CS 70

Using GDB (The GNU Debugger)

GDB (the GNU Debugger) is a powerful tool that lets you run your program step-by-step, pause it at specific points, and inspect the values of variables while it's running. It's like being able to freeze time and examine everything that's happening inside your program.

For most debugging in CS 70, print statements and Valgrind will serve you well. But sometimes—especially when you need to inspect complex state or step through tricky logic—GDB can be invaluable.

  • Dog speaking

    So when should I use GDB versus just adding print statements?

  • LHS Cow speaking

    Good question! Use print statements when you want to trace the flow of execution or see values at specific points. Use GDB when you need to inspect lots of variables at once, step through code line by line, or examine the state at the exact moment something goes wrong.

  • Hedgehog speaking

    This sounds complicated…

  • LHS Cow speaking

    It has a learning curve, but you don't need to master everything! This guide will show you the essential commands that solve most problems.

For more general debugging advice, see the main debugging guide.

Compiling for GDB

To use GDB effectively, you must compile your code with the -g flag to include debugging information:

clang++ -g -Wall -Wextra -o find42 find42.cpp -lranduint32

The -g flag tells the compiler to include information that maps the executable back to your source code, so GDB can show you function names, line numbers, and variable names.

Starting GDB

To run your program under GDB, type gdb ./your_program program_flags, where your_program is the name of your executable file and program_flags is any set of (optional) flags or arguments necessary for running the parts of the program where you see the error.

GDB will start up and loads your program (but doesn't actually run it yet). You'll see GDB's startup messages and then a (gdb) prompt where you can type commands.

A First GDB Session: Examining a Crash

Let's revisit the find42 example from the main debugging guide. Imagine an alternate timeline where we take a different path after adding an assertion that fires when the index goes out of bounds:

while (v[index] != 42) {
    ++index;
    assert(index < v.size());
}

When we run the program under GDB, it crashes, and GDB catches the crash:

(gdb) run
Starting program: /home/oneill/cs70/debugging/find42
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
find42: find42.cpp:15: size_t find42(const std::vector<uint32_t> &): Assertion `index < v.size()' failed.

Program received signal SIGABRT, Aborted.

The run command starts your program. When it crashes, GDB stops and shows you where the problem was detected in your code. Remember that the point a crash occurs is not necessarily the place where the bug is; it could just be the point where whatever the bug did affects things downstream and triggers the crash.

Reading the Backtrace

The most important GDB command after a crash is bt (short for “backtrace”):

(gdb) bt
#0  __pthread_kill_implementation (...) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (...) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (...) at ./nptl/pthread_kill.c:89
#3  0x00007ffff784527e in __GI_raise (...) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff78288ff in __GI_abort () at ./stdlib/abort.c:79
#5  0x00007ffff782881b in __assert_fail_base (...)
#6  0x00007ffff783b517 in __assert_fail (assertion=0x555555556038 "index < v.size()",
    file=0x555555556049 "find42.cpp", line=15, ...) at ./assert/assert.c:105
#7  0x00005555555552a0 in find42 (v=std::vector of length 100, capacity 128 = {...}) at find42.cpp:15
#8  0x000055555555536e in main () at find42.cpp:38

It shows the sequence of function calls that led to the crash, with the most recent call listed first. The frames labeled #0 through #6 are all library code handling the assertion failure. Frame #7 is where our code triggered the assertion, and frame #8 is where main() called find42.

  • Cat speaking

    That's a lot of library code I didn't write!

  • LHS Cow speaking

    Yes, but you can ignore most of it. Look for frames that mention your source files (find42.cpp in this case).

Selecting a Frame and Inspecting Variables

To examine what was happening in our code when the assertion fired, we select frame 7:

(gdb) f 7
#7  0x00005555555552a0 in find42 (v=std::vector of length 100, capacity 128 = {...}) at find42.cpp:15
15              assert(index < v.size());

The f command (short for “frame”) switches to that stack frame so we can inspect the local variables at that point in the code.

Now we can print the value of index:

(gdb) p index
$1 = 100

The p command (short for “print”) shows us that the value of index is 100. Since the vector has length 100 (indices 0-99), index 100 is indeed out-of-bounds!

We can also see the values of all local variables with info locals:

(gdb) info locals
index = 100

And we can examine the entire vector:

(gdb) p v
$2 = std::vector of length 100, capacity 128 = {73, 66, 59, 57, 7,
  59, 91, 24, 98, 64, 87, 6, 35, 77, 89, 72, 5, 45, 70, 48, 20, 20,
  36, 47, 34, 65, 47, 50, 18, 9, 25, 3, 52, 27, 61, 12, 65, 81, 75,
  59, 88, 49, 64, 49, 37, 0, 3, 11, 35, 32, 88, 85, 28, 52, 67, 46,
  60, 80, 63, 24, 29, 64, 73, 35, 44, 65, 28, 29, 32, 1, 96, 59, 29,
  35, 75, 77, 40, 68, 84, 91, 22, 82, 61, 6, 6, 52, 34, 96, 91, 61,
  28, 69, 66, 48, 4, 20, 54, 9, 16, 82}

Looking at the vector contents, we can verify that 42 is not in this particular array, which explains why the loop ran off the end!

When you're done, type quit (or just q) to exit GDB.

  • Sheep speaking

    Wow, that's pretty mind blowing that you can just, like, stop the program and look at everything! Even see everything in the vector!

  • Rabbit speaking

    GDB actually has some special code to pretty-print C++ standard-library containers such as std::vector and std::map so you can see their contents easily. For your own handwritten classes, it's likely to be a bit more fiddly to see what's going on, but using * and -> to follow pointers works just like in C++ code, so you can write commands like p front_->next_->value to see the value in a linked-list node.

A Second Session: Stepping Through Code

Sometimes you need to watch your program execute step-by-step to catch the issue. Let's use GDB to examine how initArray fills the vector.

Setting Breakpoints

We start by setting a breakpoint at the initArray function:

(gdb) b initArray
Breakpoint 1 at 0x12c0: file find42.cpp, line 22.

The b command (short for “break”) tells GDB to pause execution when it reaches that function. Now we can run the program,

(gdb) run
Starting program: /home/oneill/cs70/debugging/find42

Breakpoint 1, initArray (rng=..., v=std::vector of length 0, capacity 0) at find42.cpp:22
22          v.clear();

and see that the program stops executing at line 22, right at the start of initArray. We can see that at this point the vector has length 0.

Stepping Through Code

GDB provides two commands for executing code one line at a time:

  • next (or n): Execute the next line, treating function calls as single steps.
  • step (or s): Execute the next line, stepping into any function calls.

Let's step through a few lines:

(gdb) next
23          for (size_t i = 0; i < 100; ++i) {
(gdb) step
24              uint32_t num = rng.get(100);
(gdb) next
25              v.push_back(num);

Now we can see what random number was generated:

(gdb) p num
$1 = 12

Conditional Execution with Breakpoints

We don't want to manually step through all 100 iterations of the loop. Instead, let's set a breakpoint at line 25 and use continue to run until we hit it:

(gdb) b 25
Breakpoint 2 at 0x5555555552e9: file find42.cpp, line 25.
(gdb) c
Continuing.

Breakpoint 2, initArray (rng=..., v=std::vector of length 1, capacity 1 = {...}) at find42.cpp:25
25              v.push_back(num);

The c command (short for “continue”) runs the program until it hits another breakpoint. We can print the value of num again, and then keep going:

(gdb) p num
$2 = 47
(gdb) c
Continuing.

Breakpoint 2, initArray (rng=..., v=std::vector of length 2, capacity 2 = {...}) at find42.cpp:25
25              v.push_back(num);
(gdb) p num
$3 = 66

Now we're at the third iteration. Let's look at what's in the vector so far:

(gdb) p v
$4 = std::vector of length 2, capacity 2 = {12, 47}

Skipping Breakpoints

If we want to skip ahead without manually continuing through dozens of breakpoint hits, we can use ignore, which allows us to specify the number of times to skip a particular breakpoint. Here, ignore 2 40 tells GDB to ignore the next 40 times breakpoint 2 is hit:

(gdb) ignore 2 40
Will ignore next 40 crossings of breakpoint 2.
(gdb) c
Continuing.

Breakpoint 2, initArray (rng=..., v=std::vector of length 43, capacity 64 = {...}) at find42.cpp:25
25              v.push_back(num);

Now we can examine the state after 42 elements have been added:

(gdb) p num
$5 = 49
(gdb) p v
$6 = std::vector of length 43, capacity 64 = {12, 47, 66, 38, 12, 91,
  11, 21, 24, 39, 78, 21, 53, 80, 89, 75, 40, 56, 17, 47, 41, 78, 42,
  76, 64, 28, 47, 42, 81, 71, 16, 37, 58, 98, 30, 58, 74, 90, 34, 50,
  98, 14, 36}

Notice that 42 appears twice in this vector (at positions 21 and 26), but there are also many duplicates of other numbers. This finding confirms our suspicion from the main debugging guide—we're generating random numbers with replacement, not a shuffled sequence!

Essential GDB Commands

Here's a quick reference for the commands we used:

run (or r) Start your program
quit (or q) Exit GDB
bt Show backtrace (call stack)
f N Select stack frame N
p expr Print the value of an expression or variable
info locals Show all local variables in the current frame
b function or b line Set a breakpoint
next (or n) Execute next line, stepping over function calls
step (or s) Execute next line, stepping into function calls
continue (or c) Continue execution until next breakpoint
ignore N count Ignore breakpoint N for the next count hits
  • Pig speaking

    Are there MORE commands? I bet it has even MORE POWERFUL commands!!

  • LHS Cow speaking

    Yes, GDB has many more features, including conditional breakpoints and watchpoints that can detect when variable values change. But these basics will handle most debugging scenarios. Once you're comfortable with these commands, you can explore the official GDB documentation for advanced features.

Here are just a few more commands you might find useful:

info breakpoints (or i b) List all breakpoints
delete N (or d N) Delete breakpoint N
catch throw Break when an exception is thrown

When to Use GDB

GDB is most useful when:

  • You need to inspect complex data structures.
  • You want to step through code line by line to understand control flow.
  • You need to see the exact state of many variables at once.
  • You're tracking down a subtle bug where print statements would be too slow or intrusive.
  • You want to examine the call stack when something crashes.

For simpler bugs, print debugging is often faster and more straightforward. There's no shame in using std::cerr statements—they're often the right tool for the job!

Summary

GDB is a powerful debugger that lets you

  • Run your program step-by-step;
  • Pause execution at specific points (breakpoints);
  • Examine variables and data structures; and
  • Inspect the call stack when crashes occur.

While it has a learning curve, mastering even these basic commands can save you significant debugging time on complex problems. And remember—GDB is just one tool in your debugging toolkit. Use it when it helps, but don't feel obligated to use it for every bug!

For more debugging strategies and techniques, see the main debugging guide.

(When logged in, completion status appears here.)