Debugging

Overview

Teaching: 10 min
Exercises: 10 min
Questions
  • How do I debug everything?

Objectives
  • Know how to find problems in CMake

  • Know how to set up builds for debugging

Debugging is easy with CMake. We’ll cover two forms of debugging: debugging your CMake code, and debugging your C++ code.

CMake debugging

First, let’s look at ways to debug a CMakeLists or other CMake file.

Printing variables

The time honored method of print statements looks like this in CMake:

message(STATUS "MY_VARIABLE=${MY_VARIABLE}")

However, a built in module makes this even easier:

include(CMakePrintHelpers)
cmake_print_variables(MY_VARIABLE)

If you want to print out a property, this is much, much nicer! Instead of getting the properties one by one of of each target (or other item with properties, such as SOURCES, DIRECTORIES, TESTS, or CACHE_ENTRIES - global properties seem to be missing for some reason), you can simply list them and get them printed directly:

cmake_print_properties(
    TARGETS my_target
    PROPERTIES POSITION_INDEPENDENT_CODE
)

Warning

You can’t actually access SOURCES, since it conflictes with the SOURCES keyword in the function.

Tracing a run

Have you wanted to watch exactly what happens in your CMake file, and when? The --trace-source="filename" feature is fantastic. Every line run in the file that you give will be echoed to the screen when it is run, letting you follow exactly what is happening. There are related options as well, but they tend to bury you in output.

Watching a build

Let’s try this out. Let’s go to the code/04-debug folder and configure with trace mode on:

cmake -S . -B build --trace-source=CMakeLists.txt

Try adding --trace-expand too. What is the difference? How about replacing --trace-source=CMakeLists.txt with --trace?

Find call information

CMake scripts can search for dependent libraries, executables and more. More details on this will be shown in the next section.

For now, let’s watch where CMake searches for find_... locations in our current example! You can print extra find call information during the cmake run to standard error by adding --debug-find (CMake 3.17+).

Alternatively, CMAKE_FIND_DEBUG_MODE can be set around sections of your CMakeLists.txt to limit debug printing to a specific region.

C++ debugging

To run a C++ debugger, you need to set several flags in your build. CMake does this for you with “build types”. You can run CMake with CMAKE_BUILD_TYPE=Debug for full debugging, or RelWithDebInfo for a release build with some extra debug info. You can also use Release for an optimized release build, or MinSizeRel for a minimum size release (which I’ve never used).

Debug example

C++ code (click to expand)
#include "simple_lib.h"

#include <math.h>

/// Factorial function. Note that int has a maximum of 12!, and long 20!
int factorial(int a) {
    return 0 == a ? 1 : a * factorial(a - 1);
}

/// Approximate the sin function.
///
/// Uses the formula:
///   x - x³/3! + x⁵/5! - ···
///
double my_sin(double x) {
    int i;
    double sign;
    double value = 0;

    // Code has a bug
    for(i=1; i<12; i+=2) {
        sign = (i % 2 ? -1 : 1);
        value += sign * pow(x,i) / factorial(i);
    }

    return value;
}

Let’s try it. Go to code/04-debug, and build in debug mode. Our program has a bug. Let’s try it out in a debugger.

cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build-debug
gdb build-debug/simple_example

Now, since we think there’s a problem in my_sin, let’s set a breakpoint in my_sin. Note that I’m providing the gdb commands on the left, and lldb commands on the right.

# GDB                # LLDB
break my_sin         breakpoint set --name my_sin
r                    r

Now, let’s watch what happens to the sign variable. Set a watchpoint:

# GDB                # LLDB
watch sign           watchpoint set variable sign
c                    c

Keep running continue (c). Do you find the problem in the code?

Aside: Linking to math

You may find that the example provided does not work unless it’s linked with the math library “m”, which looks like -lm when linking with gcc (llvm does not seem to need to link to it). Let’s look for the “m” library:

# Does -lm work? (notice this is find_library, not find_package)
find_library(MATH_LIBRARY m)

If it is found, this saves the location of the m library in a variable that we gave it the name of, in our case, MATH_LIBRARY. We can add the path (not a target) using the same target_link_libraries command. It is very unfortunate that this command happens to accept both targets and raw paths and linker flags, but it’s a historical leftover.

# If there is a -lm, let's use it
if(MATH_LIBRARY)
    target_link_libraries(simple_lib PUBLIC ${MATH_LIBRARY})
endif()

Note that CMake defaults to an “empty” build type, which is neither optimized nor debug. You can fix this manually, or always specify a build type.

Adopting a convention from Linux, all build types append compiler flags from the environment variables CFLAGS, CXXFLAGS, CUDAFLAGS, and LDFLAGS (full list). This feature is often used by package management software, in conjunction with the already mentioned CC, CXX, CUDACXX, and CUDAHOSTCXX environment variables. Otherwise, you can set the release and debug flags separately.

Common needs

There are several common utilities that CMake can integrate with to help you with your builds. Here are just a few:

You can set these when building if you want.

Key Points

  • There are several methods for debugging your CMake code.

  • CMake can help you debug and profile your source code.