Tools: Valgrind with Memcheck

A recent thread on social media reminded me that some of the development tools I take for granted are not widely known. Veteran game developer Martin Linklater (@Fizzychicken@mastodon.gamedev.place) asked about profiling on Linux, which prompted me to mention my favourite Valgrind, which prompted a question about its use. I offered to write up a quick tutorial on it.

But thinking about it a little more, quick, introductory tutorials to the tools we use makes for a useful addition to this section of the blog.


In the first part of this miniature series, I’d like to introduce Valgrind and its Memcheck tool.

Valgrind bills itself as an “instrumentation framework”, which means it provides a collection of tools that analyse code as it runs. The Memcheck tool deals with memory and its allocations.

Example Code #1

To this end, I have a little C++ class that has, well, a few issues, and we’ll use the tool to figure them out.

struct named_object
{
  char * name = nullptr;

  named_object(char const * _name)
    : name{strdup(_name)}
  {
  }

  ~named_object()
  {
    delete name;
  }

  void print_name()
  {
    std::cerr << name << std::endl;
  }
};

There is no “real” functionality here, but you might consider this some kind of base class for a hierarchy of named objects or some such. It’s just an object that holds its name.

The name is passed as a character pointer, which I dutifully copy, because I can’t be sure of its lifetime. Since I allocate memory in the strdup() call, I need to delete it again in the destructor. Finally, there is a function for printing the name.

This structure requires two headers:

#include <string.h>
#include <iostream>

Finally, we can try some test code:

int main(int argc, char **argv)
{
  named_object o1{"foo"};
  o1.print_name();
}

There is no particular way this needs to be compiled, but it becomes clearer when smaller functions aren’t automatically inlined. So on Linux (gdb or clang), I pass -O0 to optimize nothing.

Analysis with Memcheck #1

The executable for running is always valgrind, but you need to pass which tool you wish to use. Additionally, the tools may receive parameters of their own. When using Memcheck, I tend to ask for a full leak check, and I’d also like to know about possible leaks. The tool can distinguish between definite and possible leaks, and I figure both warrant a closer look.

$ valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all \
      ./example

The output of this is highly informative, though I’ll reduce it to the relevant parts:

Mismatched free() / delete / delete []
   at 0x484BB6F: operator delete(void*, unsigned long)
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x1093AC: named_object::~named_object() (in example)
   by 0x1092A6: main (in example)
 Address 0x4df6c80 is 0 bytes inside a block of size 4 alloc'd
   at 0x4848899: malloc
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x4B7960E: strdup (strdup.c:42)
   by 0x109375: named_object::named_object(char const*) (in example)
   by 0x10928E: main (in example)


HEAP SUMMARY:
    in use at exit: 0 bytes in 0 blocks
  total heap usage: 2 allocs, 2 frees, 72,708 bytes allocated

All heap blocks were freed -- no leaks are possible

For lists of detected and suppressed errors, rerun with: -s
ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Let’s start at the bottom – the tool reports no memory leaks. It claims that all heap blocks were freed. This is good news!

But above that it warns about a Mismatched free() / delete / delete []. What is that about?

Because Valgrind instruments code, it can do a little more than just produce a stack trace at the point invalid memory access led to an error. It actually produces traces allocations. As you can see, it claims for both the generic operator delete as well as malloc that these functions are loaded from /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so.

And that is correct: Valgrind preloads this library in order to instrument allocations, and provides its own implementation of memory allocation functions that allow it to then analyse the stack both at the point of allocation as well as at the point of use and/or freeing.

In this case, it correctly identifies that the named_object constructor allocates memory in the strdup() function, and does so via malloc() because that is a pure C function.

Naively, in the destructor, however, I used the C++ operator delete. In many cases, this can be valid – delete is almost certainly implemented via free(). However, all guidelines for mixing C and C++ code recommend to manage memory either with C functions or C++ operators, but not mix the two, because there is no guarantee it is valid.

Example Code #2

Alright, let’s assume we’re panicking a little at this point and treat the delete operator as a problem. We’ll just comment it out for the time being, rebuild and re-run via Valgrind.

Analysis with Memcheck #2

On the subsequent run, Memcheck detects another issue:

HEAP SUMMARY:
    in use at exit: 4 bytes in 1 blocks
  total heap usage: 2 allocs, 1 frees, 72,708 bytes allocated

4 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4848899: malloc
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x4B7960E: strdup (strdup.c:42)
   by 0x109355: named_object::named_object(char const*) (in example)
   by 0x10926E: main (in example)

LEAK SUMMARY:
   definitely lost: 4 bytes in 1 blocks
   indirectly lost: 0 bytes in 0 blocks
     possibly lost: 0 bytes in 0 blocks
   still reachable: 0 bytes in 0 blocks
        suppressed: 0 bytes in 0 blocks

For lists of detected and suppressed errors, rerun with: -s
ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

It should come as little surprise that not freeing memory at all is no help. But it demonstrates the leak checking facility quite well.

Of course it’s not possible for the tool to guess where memory should be freed, but it does correctly specify that the memory allocated by strdup() never gets released.

Obviously, we’ll have to use C’s free() function instead.

Example Code #3

// ...

  ~named_object()
  {
    free(name);
  }

// ...

named_object
make_object(char const * name = nullptr)
{
  return named_object{name};
}

In addition to fixing the destructor, we’ll also introduce a function that creates named objects. Because even if named_object behaves reasonably enough in its own right, calling code might not – and this leads to a better example with a little more complexity, approaching a real-world situation.

Analysis with Memcheck #3

Running this through Memcheck produces a far more interesting result.

Invalid read of size 1
   at 0x484ED16: strlen
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x4B79602: strdup (strdup.c:41)
   by 0x10936F: named_object::named_object(char const*) (in example)
   by 0x10924F: make_object(char const*) (in example)
   by 0x109289: main (in example)
 Address 0x0 is not stack'd, malloc'd or (recently) free'd


Process terminating with default action of signal 11 (SIGSEGV)
 Access not within mapped region at address 0x0
   at 0x484ED16: strlen
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x4B79602: strdup (strdup.c:41)
   by 0x10936F: named_object::named_object(char const*) (in example)
   by 0x10924F: make_object(char const*) (in example)
   by 0x109289: main (in example)
 If you believe this happened as a result of a stack
 overflow in your program's main thread (unlikely but
 possible), you can try to increase the size of the
 main thread stack using the --main-stacksize= flag.
 The main thread stack size used in this run was 8388608.

HEAP SUMMARY:
    in use at exit: 72,704 bytes in 1 blocks
  total heap usage: 1 allocs, 0 frees, 72,704 bytes allocated

72,704 bytes in 1 blocks are still reachable in loss record 1 of 1
   at 0x4848899: malloc
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x4931979: ???
      (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30)
   by 0x400647D: call_init.part.0 (dl-init.c:70)
   by 0x4006567: call_init (dl-init.c:33)
   by 0x4006567: _dl_init (dl-init.c:117)
   by 0x40202E9: ???
      (in /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2)

LEAK SUMMARY:
   definitely lost: 0 bytes in 0 blocks
   indirectly lost: 0 bytes in 0 blocks
     possibly lost: 0 bytes in 0 blocks
   still reachable: 72,704 bytes in 1 blocks
        suppressed: 0 bytes in 0 blocks

For lists of detected and suppressed errors, rerun with: -s
ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

The first block is the interesting one: it tells us that strlen(), as invoked by strdup() and ultimately our constructor performed an Invalid read of size 1. In the last line it asserts that Address 0x0 is not stack'd, malloc'd or (recently) free'd.

Now of course we are seasoned enough to understand that a nullptr would not be allocated, and we can search for uses of nullptr that may not be entirely safe. The stack trace leads us to the make_object() function as we invoke it in main().

We can quickly figure out that the contract of that function – that the name is optional – is not compatible with the contract of the constructor – that the name is mandatory – and that the function does nothing to make the two fit, such as providing a default name, etc.

The fix is simple.

But the output also tells us a few other things. First, it explains that the use of nullptr also led to a segmentation fault, and where that occurred. I’ll get back to the use of that later.

Finally, it tells us about a memory leak. This is an interesting one, because at no point did we ourselves allocate memory other than in the failed strdup() attempt. So where are those 72,704 bytes in 1 blocks come from? Well, it turns out that the preloaded instrumentation library allocates them itself when it is first loaded into memory.

Example Code #4

Now… debugging this was relatively easy, because it was simple code. But perhaps you noticed that the output showed very clearly where in the strdup() function we had an issue (strdup.c:41), while it didn’t tell us anything about our own software.

That’s because we didn’t really compile our executable with debugging symbols. Pass -g or -ggdb to the compiler, and you’ll see something different.

Analysis with Memcheck #4

Invalid read of size 1
  at 0x484ED16: strlen
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
  by 0x4B79602: strdup (strdup.c:41)
  by 0x10936F: named_object::named_object(char const*) (example.cpp:9)
  by 0x10924F: make_object(char const*) (example.cpp:28)
  by 0x109289: main (example.cpp:34)
Address 0x0 is not stack'd, malloc'd or (recently) free'd

There you go: file name and line number of the issue. That’s better.

Example Code #5

Let’s assume we used the make_object() function more sensibly and avoided this issue. There’s another glaring problem with the code above, which we’ll demonstrate with this usage:

auto o1 = make_object("foo");
o1.print_name();

auto o2 = o1;

In this simple example, it may be obvious what the problem was – but in larger code bases, I find this kind of error can slip through unnoticeable, in part because the compiler does not complain about it.

Analysis with Memcheck #5

The new log shows a different kind of problem.

Invalid free() / delete / delete[] / realloc()
   at 0x484B27F: free
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x1093B6: named_object::~named_object() (example.cpp:15)
   by 0x1092BA: main (example.cpp:38)
 Address 0x4df6c80 is 0 bytes inside a block of size 4 free'd
   at 0x484B27F: free
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x1093B6: named_object::~named_object() (example.cpp:15)
   by 0x1092AE: main (example.cpp:38)
 Block was alloc'd at
   at 0x4848899: malloc
      (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x4B7960E: strdup (strdup.c:42)
   by 0x109389: named_object::named_object(char const*) (example.cpp:9)
   by 0x10924F: make_object(char const*) (example.cpp:28)
   by 0x10928E: main (example.cpp:34)


HEAP SUMMARY:
    in use at exit: 0 bytes in 0 blocks
  total heap usage: 2 allocs, 3 frees, 72,708 bytes allocated

This is odd, isn’t it? We fixed the malloc()/delete mismatch by using free(), and then we fixed the usage of the make_object() function. Suddenly the call to free() is invalid? How did that happen?

Well, the summary at the end gives us a pretty good hint: it talks about two allocations and three frees. That implies a double free problem somewhere, does it not?

Indeed, the problem here is how C++ tries to be helpful. We copied the named object in the last line of our sample code, and C++ helpfully provides a default implementation for a copy constructor. That in turn provides a bitwise copy.

Now both o1 and o2 have name point at the same memory address, and both try to free() it in their destructor.

Summary

There are a bunch of potential solutions here:

  1. Don’t use character points, this is what std::string is for.
  2. Use a std::unique_ptr<> – because it has no copy constructor, C++ cannot provide a default constructor for you, exposing the problem at compile time.
  3. Use a std::shared_ptr<> – because it’s reference counted, it will free the memory only when the last object referencing it goes out of scope.
  4. etc.

This post is not the place to talk about C++ best practices, so the actual solution is left as an exercise to the reader.

The aim was to run through a number of memory issues, and show how Valgrind and Memcheck not only detect them, but also help you find their cause. Debugging symbols help make the information most useful, but even without, having stack traces at both the point of allocation and at the point of use is very helpful.

It should also be noted that the nullptr example is maybe a little easy to detect. But memory errors aren’t just about dereferencing nullptr – they can also be passing an address past the end of an array or some such; in itself a valid memory address. As Valgrind traces allocations, it can tell you as well when such an address has never been part of its allocations. In such a case, debugging tends to be a lot harder without Valgrind.

In the next post in this mini series, I’ll write about Callgrind, another one of Valgrind’s tools to help with profiling.


Published on July 20, 2023