Debugging C++ projects with GDB

I am currently working on a Python/C++ project where Python code calls methods from a C++ library, objects being carried from one to the other by Boost.Python. I was introduced (again...) to some elementary use of GDB, which I want to post here for sharing as well as future reference. This is more of a hands-on demonstration than an introduction to GDB since I do not explain concepts such as the program stack.

Getting started

First, you need to compile the objects of your C++ library with the -g flag, for instance:

$ g++ -g -std=c++0x -Wall -fPIC main.cpp bis.cpp

Then, you can run GDB on your executable directly. In the case of a Python script calling C++ functions, you can use the following syntax:

$ gdb --args python script.py

The most frequently used GDB commands are the following (adapted from the GDB manpage):

  • r: start the program
  • bt: backtrace: display the program stack
  • b file.cpp:42: set a breakpoint at line 42 of file file.cpp
  • c: resume execution (after stopping, e.g. at a breakpoint)
  • s (step): execute next program line (after stopping); step into any function calls in the line
  • n (next): execute next program line (after stopping); step over any function calls in the line
  • d: delete breakpoints
  • p expr: print the value of an expression expr (may be a single variable)
  • f n: go to frame n (see bt)

When using the STL, you may want to include the following to your gdbinit script (authors: Dan Marinescu, Anders Elton). This script provides you with printers for your favorite STL containers, such as:

  • pvector v: print an std::vector
  • plist l: print an std::list
  • plist_member l: print one element from the list l

Debugging objects at execution

Consider the following situation: some buggy function segfaults at execution, and we want to survey the content of an object called constraints in the context where the function crashed. First, we use bt and f to get to this context:

(gdb) r
[run until segfault]

(gdb) bt
...
#42 0x00002aaac3bd67b7 in MyProject::BuggyFunction (constraints=...,
    xlist=@0x7fffffffc6f8) at BuggyFile.cpp:528
...

(gdb) f 42
#42 0x00002aaac3bd67b7 in MyProject::BuggyFunction (constraints=...,
    xlist=@0x7fffffffc6f8) at BuggyFile.cpp:528
528     double z = LetsCrash(constraints, xlist);

The output of f shows the current frame (#42), values of the parameters in the function call (I redacted constraints here, but e.g. the pointer value of the pointer xlist is show as a memory address) as well as the line of code where the execution is at (here, line 528 of file BuggyFile.cpp, where the function LetsCrash is called). Now constraints is in the current context and we can print it:

(gdb) p constraints
$1 = (MyProject::Constraints &) @0x1b7de60: {
  <MyProject::Constraints> = {
    _vptr.Constraints = 0x2aaac3dffb50,
    trajectory = {
      dimension = 2,
      duration = 1,
      degree = 3,
      chunks_list = { ... }
    },
    n = 42
  }, <No data fields>}

Let us take a peak at the chunks_list field of the trajectory:

(gdb) p constraints.trajectory.chunks_list
$2 = {
  <std::_List_base<MyProject::Chunk, std::allocator<MyProject::Chunk> >> = {
    _M_impl = {
      <std::allocator<std::_List_node<MyProject::Chunk> >> = {
        <__gnu_cxx::new_allocator<std::_List_node<MyProject::Chunk> >> = {<No data fields>}, <No data fields>},
      members of std::_List_base<MyProject::Chunk, std::allocator<MyProject::Chunk> >::_List_impl:
      _M_node = {
        _M_next = 0x1b930b0,
        _M_prev = 0x1b930b0
      }
    }
  }, <No data fields>}

We can call the plist pretty printer to get a more helpful output:

(gdb) plist constraints.trajectory.chunks_list
List size = 1
List type = std::list<MyProject::Chunk, std::allocator<MyProject::Chunk> >
Use plist <variable_name> <element_type> to see the elements in the list.

It says we need to specify the element_type argument (indicated above in the "List type" line) to print out elements one by one. That is:

(gdb) plist constraints.trajectory.chunks_list Chunk
elem[0]: $3 = {
  dimension = 2,
  degree = 3,
  duration = 1,
  sbegin = 0,
  send = 1,
  polynomials_vector = { ... }
}
List size = 1

We can access the first list element with front():

(gdb) p constraints.trajectory.chunks_list.front()
$4 = (MyProject::Chunk &) @0x1b930c0: {
  dimension = 2,
  degree = 3,
  duration = 1,
  sbegin = 0,
  send = 1,
  polynomials_vector = { ... }
}

However, we already had this element at hand. Had you noticed the $i printed out by GDB? They are registers that can be used to refer to printed objects. For instance:

(gdb) p $3
$5 = (MyProject::Chunk &) @0x1b930c0: {
  dimension = 2,
  degree = 3,
  duration = 1,
  sbegin = 0,
  send = 1,
  polynomials_vector = { ... }
}

In particular, whenever you print a list or a vector, each element is bound to such a register so you can access it directly afterwards. We saw this above when we printed the chunks list and $3 was mapped to the first element. If the list had had more elements, the second would have been bound to $4, the third to $5, ans so on. These registers can be used like variables in C++ expressions, so we can e.g. access their fields:

(gdb) pvect $5.polynomialsvector
elem[0]: $6 = {
  degree = 3,
  coefficients_vector = { ... }
}
elem[1]: $7 = {
  degree = 3,
  coefficients_vector = { ... }
}
Vector size = 2
Vector capacity = 2
Element type = MyProject::Polynomial *

As arguments to print commands are full-blown C++ expressions, one can use function calls (see front() above), membership operators (we used the dot, but -> can also be used for pointers), etc.

Pages of this website are under the CC-BY 4.0 license.