Debugging code in C++
Recognizing and debugging errors is a fundamental skill when programming in C++, as is writing safe, expressive, and modern C++. We will cover the basics in this note.
Prerequisites
Make sure you have a full GCC, GDB and Meson installation:
sudo apt install g++ gdb meson ninja-build
gdb --tui # <- this should not give an error message
We’ll work on the following files, which you can now download:
Put these files in a folder seperate from the class project.
Compile
Run the standard Meson workflow to get started:
meson setup builddir --buildtype=debug
cd builddir
meson compile
# To compile after changes to sources:
meson compile # reruns Meson if needed
# To change build type
meson setup --reconfigure --buildtype=release
meson compile
More information on buildtype
: Details for buildtype
.
Compiling other files
You’ll notice that localaddress.cpp
and outofbounds.cpp
are not compiled. To create executables, add these lines at the end of meson.build
and compile again.
executable(
'localaddress',
'localaddress.cpp',
dependencies : [eigen]
)
executable(
'outofbounds',
'outofbounds.cpp',
dependencies : [eigen]
)
Adding compilation flags
To add compilation flags for all targets, add them to the default_options
parameter of project
:
project(
'my-project',
['c', 'cpp'],
default_options : [
'warning_level=3', # Passes -Wall -Wextra -Wpedantic to compile
],
version : '0.1'
)
The AddressSanitizer (Asan) needs compilation and link flags:
add_project_arguments('-fsanitize=address', language : ['c', 'cpp'])
add_project_link_arguments('-fsanitize=address', language : ['c', 'cpp'])
Debugging
Many bugs can be caught by enabling warnings and AddressSanitizer at compile time as shown above and running the program. However, this will most times only show the point of crash of your program, not where the bug actually is. To find where the bug is, it is very useful to use a debugger like the GNU Debugger (GDB). We’ll now explain the basic workflow with GDB.
Compiling
Make sure you ran Meson in Debug
mode:
meson setup builddir --buildtype=debug
This turns off code optimizations (-O0
compiler flag) and adds debugging symbols to the executable (-g
compiler flag), which are both necessary to properly debug. Additionally, the macro NDEBUG
is not defined, which enables the assert()
function from the standard header <cassert>
and enables bounds check in Eigen arrays. All these features will make your life easier when writing code.
Running GDB
We will run GDB with a Terminal User Interface (TUI):
gdb --tui ./nullpointer
The top window should show you the source code (if not, make sure you compile with --buildtype=debug
). If the top windows is highlighted with a blue border, you can use the arrow keys to navigate the code. Use the shortcut C-x o
(Ctrl-X then O), to alternate focus between the top and bottom window. If the focus is not on the top window, the arrow keys will cycle the command history (similar to arrows in bash). If you want to enable command history you can add set history save on
to ~/.gdbinit
.
Manipulating program state
You can use the run
command to run the program until the crash. The source window will then show where the crash occurs, and why, but that’s often not where the bug is. You can still use up
to see which call of set_to_one
has the crash. Using start
you can execute the program from the very beginning, line by line.
Use the print
command to show the value of the pointer
variable. Since it is tedious to print
at every instruction, use watch pointer
, then next
to advance the program execution. The watch
command will show when pointer
changes value. When hitting enter without a command, it just repeats the last command. Advance the program until pointer
has the value 0x0
and fix the bug.
Notable other GDB commands:
info locals
: show all variables in current stackprint <var>
: show value of variabledisplay <var>
: show value of variable at every stepwatch <var>
: show when variable changes valuebacktrace
: show the call stackup
: move up the call stackdown
: move down the call stackbreak <file>:<line>
: put a breakpoint at given line in a filebreak <function>
: breakpoint when a function is calledcontinue
: resume execution until breakpoint, crash or end of programfinish
: resume execution until current function returnsuntil
: resume execution until a source line “greater” than the current location—essentially until the end of a scope (loop, if statement, etc.)C-l
(Ctrl+L) refresh display: sometimes display of the source gets weird,C-l
refreshes the source display so it’s clear againprint *pointer@N
printsN
values starting at addresspointer
Memory leaks
After having fixed the null pointer bug, activate the AddressSanitizer (see above) and run the program. It should show a memory leak of 4 bytes. AddressSanitizer can also be used for the two other examples (localaddress.cpp and outofbounds.cpp), usually showing the call stack (backtrace) at the moment of the crash.
Fixed files
Below are versions of the files above where we used idiomatic C++, the standard library and Eigen instead of C++-flavoured C:
Priting Eigen objects in GDB
Let’s work with the following file:
#include <Eigen/Core>
int main() {
Eigen::Array4d static_{0, 1, 2, 3};
Eigen::ArrayXd dynamic_(4);
dynamic_ << 0, 1, 2, 3;
return 0;
}
When debugging, we’d like to know the contents of both arrays. If we use the print
command on one of these objects we have a verbose output:
(gdb) print static_
$1 = {<Eigen::PlainObjectBase<Eigen::Array<double, 4, 1, 0, 4, 1> >> = {<Eigen::ArrayBase<Eigen::Array<double, 4, 1, 0, 4, 1> >> = {<Eigen::DenseBase<Eigen::Array<double, 4, 1, 0, 4, 1> >> = {<Eigen::DenseCoeffsBase<Eigen::Array<double, 4, 1, 0, 4, 1>, 3>> = {<Eigen::DenseCoeffsBase<Eigen::Array<double, 4, 1, 0, 4, 1>, 1>> = {<Eigen::DenseCoeffsBase<Eigen::Array<double, 4, 1, 0, 4, 1>, 0>> = {<Eigen::EigenBase<Eigen::Array<double, 4, 1, 0, 4, 1> >> = {<No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}, m_storage = {m_data = {array = {0, 1, 2, 3}}}}, <No data fields>}
(gdb) print dynamic_
$2 = {<Eigen::PlainObjectBase<Eigen::Array<double, -1, 1, 0, -1, 1> >> = {<Eigen::ArrayBase<Eigen::Array<double, -1, 1, 0, -1, 1> >> = {<Eigen::DenseBase<Eigen::Array<double, -1, 1, 0, -1, 1> >> = {<Eigen::DenseCoeffsBase<Eigen::Array<double, -1, 1, 0, -1, 1>, 3>> = {<Eigen::DenseCoeffsBase<Eigen::Array<double, -1, 1, 0, -1, 1>, 1>> = {<Eigen::DenseCoeffsBase<Eigen::Array<double, -1, 1, 0, -1, 1>, 0>> = {<Eigen::EigenBase<Eigen::Array<double, -1, 1, 0, -1, 1> >> = {<No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}, m_storage = {m_data = 0x55555556beb0, m_rows = 4}}, <No data fields>}
We can find values for static_
at array = {...}
, but this is inconvenient. A better way is to print the member variable m_storage
:
(gdb) print static_.m_storage
$3 = {m_data = {array = {0, 1, 2, 3}}}
(gdb) print dynamic_.m_storage
$4 = {m_data = 0x55555556beb0, m_rows = 4}
We can see stored values for the static array because they are stored on the stack. The dynamic array values are on the heap so we only see the address of the first element and the number of elements. We can use that information to print the stored values with the print *pointer@N
syntax, which shows N
continguous values starting at address pointer
:
(gdb) print *dynamic_.m_storage.m_data@4
$5 = {0, 1, 2, 3}
Note that this can only show values as a 1D sequence, so the order of the values for 2D matrices depends on if matrices are row- or column-major.
A less crude way to print values would be to use Eigen’s pretty printer for GDB. Follow the instructions in the file to install. You might have to add set auto-load safe-path /
to ~/.gdbinit
to enable loading of foreign python code.