Aussie AI

Chapter 6. Safe C++ Tools

  • Book Excerpt from "Safe C++: Fixing Memory Safety Issues"
  • by David Spuler

Tools Overview

There are several distinct types of C++ tools that improve overall quality. Some of them focus on memory safety issues, whereas other tools have a broader range of features. The general categories with a specific focus on safety issues include:

  • Runtime memory checkers (i.e., sanitizers, valgrind)
  • General sanitizers (non-memory issues)
  • Linters and static analysis tools
  • Automated test harnesses
  • Test coverage tools
  • Security vulnerability analysis tools

In addition, some of the general programming tools are important in making a significant impact on programmer productivity and overall quality. These include:

  • IDE environments (without and without AI copilots)
  • Compilers (with useful warnings and runtime features)
  • Interactive debuggers (IDE-based debuggers and gdb)
  • Performance profilers (IDE-based or command-line)
  • Tracing tools

Runtime Memory Checkers

There are a variety of runtime memory checkers available to find memory errors in C++. I remember using Purify back in the 1990s, and it still exists today. However, there are several free high-quality memory sanitizer tools now. Some examples include:

  • Valgrind
  • AddressSanitizer (ASan)
  • MemorySanitizer (MSan)
  • LeakSanitizer

There are also runtime checkers with a broader focus than memory safety issues:

  • ThreadSanitizer (TSan)
  • UndefinedBehaviorSanitizer (UBSan)

Several compilers and IDEs have builtin support for running sanitizers. GCC has options such as:

    -fsanitize=address
    -fsanitize=kernel-address
    -fsanitize=hwaddress
    -fsanitize=thread
    -fsanitize=leak
    -fsanitize=undefined
    -fsanitize=signed-integer-overflow
    -fsanitize=bounds

That's only some of the GCC runtime error checking options for sanitizing. There are many more options, including granular control over specific types of error checks.

Valgrind

The Linux version of Valgrind Memcheck is very capable and well supported. The method to use Valgrind for Linux on your application is simply to run the executable:

    valgrind a.out

If Valgrind is not installed in your Linux environment, you’ll need to do something like this:

    apt install valgrind

The start of the Valgrind output is like this:

    ==1143== Memcheck, a memory error detector
    ==1143== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
    ==1143== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
    ==1143== Command: ./a.out

As it executes your program, the output from your program will be interleaved with error reports from Valgrind. Hopefully, there won’t be any!

The end of the Valgrind execution gives you a nice summary of memory leaks and errors.

    ==1143== HEAP SUMMARY:
    ==1143==     in use at exit: 12,710,766 bytes in 10,810 blocks
    ==1143==   total heap usage: 15,851 allocs, 5,041 frees, 47,396,077 bytes allocated
    ==1143== 
    ==1143== LEAK SUMMARY:
    ==1143==    definitely lost: 0 bytes in 0 blocks
    ==1143==    indirectly lost: 0 bytes in 0 blocks
    ==1143==      possibly lost: 30,965 bytes in 199 blocks
    ==1143==    still reachable: 12,679,801 bytes in 10,611 blocks
    ==1143==         suppressed: 0 bytes in 0 blocks
    ==1143== Rerun with --leak-check=full to see details of leaked memory
    ==1143== 
    ==1143== For lists of detected and suppressed errors, rerun with: -s
    ==1143== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Running programs in Valgrind is obviously slower because of the instrumentation, but this is also true of similar sanitizer tools.

Gnu Debugger: gdb

I’m a big fan of gdb for debugging standard C++ on Linux. The basic commands for gdb are:

  • r or run — run the code (with optional arguments), or restart if already running.
  • c or continue — continue running (after stopping at a breakpoint).
  • s or step — stepping through statements (also just Enter).
  • where — stack trace (also aliased to “bt” for backtrace).
  • list — source code listing
  • p or print — print a variable or expression.
  • up
  • n or next

Abnormal program termination

gdb batch mode. You can detect this program crash better in gdb as it will trap the signals, so just run an interactive debugging sessions. Alternatively, if you have a simple reproducible case, you can automate this with batch mode, where the command to run is like this:

    gdb -batch -x=gdbtest.txt a.out

The batch input file is a set of gdb commands:

    run
    where
    exit

Here’s an example output (abridged):

    Thread 1 "a.out" received signal SIGSEGV, Segmentation fault.
    0x00007ffff7cdfa4e in ?? () from /lib/x86_64-linux-gnu/libc.so.6
    #0  0x00007ffff7cdfa4e in ?? () from /lib/x86_64-linux-gnu/libc.so.6
    #1  0x000055555555fdb5 in aussie_malloc(void**, int) ()
    #2  0x0000555555562ea9 in aussie_run_clear_vector(int) ()
    #3  0x00005555555633aa in main ()
    A debugging session is active.
    Inferior 1 [process 5143] will be killed.
    Quit anyway? (y or n) [answered Y; input not from terminal]

There are various other useful things that can be automated using batch gdb and various script commands. For example, you can use it as a trace mechanism that prints out the stack trace at every call to a certain function.

Pre-Breakpointing Trick

One advanced tip for using gdb is to define a function called “breakpoint” in your C++ application. Here’s an example:

    void breakpoint()
    {
        volatile int x = 0;
        x = 0;  // Set breakpoint here
    }

It looks like a silly function, but it serves one useful purpose. The idea is that when you start an interactive debugging session in gdb, or automatically in your “.gdbinit” resource file, you can set a breakpoint there:

    b breakpoint

Why do that? The reason is that you also add calls to your “breakpoint” function at relevant points in various places where failures can occur:

  • Error check macros
  • Assertion macros
  • Debug wrapper function failure detection
  • Unit test failures

Hence, if any of those bad things happen while you’re running interactively in the debugger, you’re immediately stopped at exactly that point. If you’re not running in the debugger, this is a very fast function (though admittedly, it can’t be inline!), so it doesn’t slow things down much. You can even consider leaving this in production code, since the breakpoint function is only called in rare situations where a serious failure has already occurred, in which case execution speed is not a priority.

This technique is particularly useful because don’t have to go back and figure out how to reproduce the failure, which can be difficult to do for some types of intermittent failures from race conditions or other synchronization problems. Instead, it’s already been pre-breakpointed for you, with the cursor blinking at you, politely asking you to debug it right now, or maybe after lunch.

Postmortem Debugging

Postmortem debugging involves trying to debug a program crash, such as a “core dump” on Linux. In this situation, you should have a “core” file that you can load into gdb. The command to use is:

    gdb a.out core

Unfortunately, not all errors in an application will trigger a core dump, so you might have nothing to debug if it doesn’t.

Programmatic C++ core dumps. One way to ensure that you get a core file is to trigger one yourself with the abort function. For example, you might do this in your assertion failure routines or other internally self-detected error states.

You can even do this without exiting your application! If you’re wanting to have your application to take control of its own core dumps (e.g., exceptions, assertion failures, etc.), there are various points:

  • You can always fork-and-abort on Linux.
  • Surely you can write some code to crash!

On the other hand, maybe you’re only thinking about core dumps because you want to save debug context information. Doing this might obviate the need for a core dump:

  • Use std::backtrace or another backtrace library.
  • Print error context information (e.g., user’s query)
  • Print platform details

Customer core dumps. One of the supportability issues with postmortem debugging is that you want your customers to be able to submit a core file that they have triggered in your application. These are usually large files, so there are logistical issues to overcome with uploads.

Another issue is that in order to run gdb on a core file, the developer needs to have exactly the right executable that created the core dump. Hence, your build and release management needs to maintain available copies of all executable files in versions shipped to customers or in beta testing (or to internal customers for in-house applications). This means not only tracking the production releases of stripped executables, but also the correlated debug version of the executable with symbolic information.

Also, there needs to be a command-line option or other method whereby the phone support staff can instruct customers to report the exact version and build number of the executable they are using. It’s easy to lose track!

References

  1. GNU, Sep 2024 (accessed), 3.8 Options to Request or Suppress Warnings (GCC warning options), https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
  2. GNU, Oct 2024 (accessed), 3.12 Program Instrumentation Options https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
  3. GNU, Oct 2024 (accessed), 3.9 Options That Control Static Analysis, https://gcc.gnu.org/onlinedocs/gcc/Static-Analyzer-Options.html

 

Online: Table of Contents

PDF: Free PDF book download

Buy: Safe C++: Fixing Memory Safety Issues

Safe C++ Safe C++: Fixing Memory Safety Issues:
  • The memory safety debate
  • Memory and non-memory safety
  • Pragmatic approach to safe C++
  • Rust versus C++
  • DIY memory safety methods
  • Safe standard C++ library

Get it from Amazon: Safe C++: Fixing Memory Safety Issues