Membrane SoftwareTechnology development studio
Automated bug catching with valgrind
Thursday, July 20 2017 22:09 GMT
Posted by: MembraneTags:programmingvalgrind

A typical software application is built from a code base containing thousands of lines, and a complex application easily reaches into the millions as its developers add ever more functionality. However, each line of code in a project is a place where somebody could have made a mistake, and even a single coding mistake among millions of lines can bring everything crashing down if it falls in a critical area. Many programmers become reliant on simple visual inspection and peer review to find lines of code that might contain mistakes, an approach that proves utterly unworkable when scaling to a code base of significant size, not to mention the fact that imperfect human eyes miss things all the time when examining code bases of any size. It's inevitable, then, that we'll need to use tools of some sort to analyze our code base. Ideally, these tools would be rolled into an automated build that processes all code after each change is committed, thereby giving us a system of continuous integration. On a team with multiple developers, such a system is invaluable; if any individual commits a change found to cause errors, we can easily revert the code its previous and known good state.

Today, we'll take a look at valgrind, a tool that comes in very handy as part of an automated build for C/C++ code. In applications built from these languages, the memory leak is one of the most common types of bugs as well as one of the most pernicious, often causing the user's system to slow to a crawl before an inexorable process crash hits. With valgrind's help we can flush out such leaks, along with several other types of issues related to memory management. Furthermore, by incorporating valgrind into a project as a build step, we can detect critical bugs as soon as they are coded, hopefully preventing their propagation to production sources and keeping them out of the hands of users.

This article refers to sample source files, which are available for cloning in the valgrind-testapp project on GitHub.

Introducing valgrind

valgrind (note: pronounced "val-grinned", not "val-grined") is a tool specific to the Linux platform, made available under the GNU GPL. It's free-as-in-speech as well as free-as-in-beer; we give our hearty thanks and praise to the valgrind developers!

What with being so free and all, valgrind is easily installed onto any Linux host. The valgrind site provides source packages for those who'd like to build the tool themselves, but most or all Linux distributions should have a pre-built package available. For example, on Debian systems a simple apt-get command targeting the valgrind package does the trick.

01  $ valgrind
02  bash: valgrind: command not found
03  $ apt-get install valgrind
04  The following NEW packages will be installed:
05    libc6-dbg valgrind
06    0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
07    Need to get 14.9 MB of archives.
08  ... (more apt-get output here)
09  $ valgrind
10  valgrind: no program specified
11  valgrind: Use --help for more information.
12  $

"But wait", I hear you say, "what about the non-Linux platforms?" Since valgrind only runs on Linux, it's tempting to think that it's only good for checking Linux applications and is useless for Windows and macOS applications. However, C/C++, along with its potential for memory management errors, exists on every platform. If we're careful to program in a fully portable fashion by following the C standards employed by our target compilers, it becomes possible to build and run the same code in multiple environments: on Linux for valgrind checks, and then on Windows and macOS for distribution to typical users.

These days, Windows and macOS are both capable of running another tool known as Docker, described in our previous article "Better sysadmin living through Docker" as a powerful containerization layer that makes it easy to run Linux-specific applications in a bottled environment. It takes only a short Dockerfile and minimal effort to generate an image capable of checking code with valgrind on any of a number of platforms.

ListingDockerfile
1  FROM ubuntu:latest
2  ENV DEBIAN_FRONTEND noninteractive
3  RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends make gcc libc-dev valgrind
The test application

To illustrate the use of valgrind, we'll run it against a simple C program. This application doesn't even try to print a Hello World message; it just runs and exits.

ListingC
1  #include <stdlib.h>
2  
3  int main (int argc, char **argv) {
4    exit (0);
5  }

Building and running our program with gcc does all the nothing one would expect. But running the application binary with valgrind generates an interesting report.

01  $ gcc -g -o testapp testapp.c
02  $ ./testapp && echo "success"
03  success
04  $ valgrind --leak-check=full --show-leak-kinds=all ./testapp
05  ==8167== Memcheck, a memory error detector
06  ==8167== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
07  ==8167== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
08  ==8167== Command: ./testapp
09  ==8167== 
10  ==8167== 
11  ==8167== HEAP SUMMARY:
12  ==8167==     in use at exit: 0 bytes in 0 blocks
13  ==8167==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
14  ==8167== 
15  ==8167== All heap blocks were freed -- no leaks are possible
16  ==8167== 
17  ==8167== For counts of detected and suppressed errors, rerun with: -v
18  ==8167== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
19  $

This is the basic use of valgrind: its runs an application on our behalf while keeping precise track of its memory access. Therefore, it's in a perfect position to detect memory-related errors and report them to us when the application ends. We run valgrind with --leak-check=full --show-leak-kinds=all to enable our desired memory checking functionality, but that's just the tip of a giant iceberg. Many other modes of operation are possible, as detailed in the valgrind manual.

Breaking the (memory) bank

Let's see what valgrind reports after we introduce a memory leak at lines 07-10 in our test application.

ListingC
01  #include <stdlib.h>
02  
03  void fn () {
04    int len, *array;
05  
06    len = 8;
07    array = (int *) malloc (len * sizeof (int));
08  
09    // TODO: Free this array (currently leaving a leak for detection by valgrind)
10    // free (array);
11  }
12  
13  int main (int argc, char **argv) {
14    fn ();
15    exit (0);
16  }
01  $ gcc -g -o testapp testapp.c && valgrind --leak-check=full --show-leak-kinds=all ./testapp
02  ... (valgrind introductory text)
03  ==11987== HEAP SUMMARY:
04  ==11987==     in use at exit: 32 bytes in 1 blocks
05  ==11987==   total heap usage: 1 allocs, 0 frees, 32 bytes allocated
06  ==11987== 
07  ==11987== 32 bytes in 1 blocks are definitely lost in loss record 1 of 1
08  ==11987==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
09  ==11987==    by 0x40059C: fn (testapp.c:7)
10  ==11987==    by 0x4005BB: main (testapp.c:14)
11  ==11987== 
12  ==11987== LEAK SUMMARY:
13  ==11987==    definitely lost: 32 bytes in 1 blocks
14  ==11987==    indirectly lost: 0 bytes in 0 blocks
15  ==11987==      possibly lost: 0 bytes in 0 blocks
16  ==11987==    still reachable: 0 bytes in 0 blocks
17  ==11987==         suppressed: 0 bytes in 0 blocks
18  ==11987== 
19  ==11987== For counts of detected and suppressed errors, rerun with: -v
20  ==11987== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
21  $

The report shows valgrind easily catching our memory leak of 8 32-bit values, totalling 32 bytes in size as reported on line 13. Of course, in real life our memory errors will be both more numerous and more complex. We can adjust our test application to give valgrind a bit more to do.

ListingC
01  #include <stdlib.h>
02  
03  void fn1 () {
04    int len, *array;
05  
06    len = 8;
07    array = (int *) malloc (len * sizeof (int));
08  
09    // TODO: Free this array (currently leaving a leak for detection by valgrind)
10    // free (array);
11  }
12  
13  void fn2 () {
14    int i;
15  
16    for (i = 0; i < 1024; ++i) {
17      fn1 ();
18    }
19  }
20  
21  int main (int argc, char **argv) {
22    int exitcode;
23  
24    fn1 ();
25    fn2 ();
26  
27    // TODO: Initialize exitcode before referencing its value (currently leaving an error for detection by valgrind)
28    // exitcode = 0;
29    exit (exitcode);
30  }
01  $ gcc -g -o testapp testapp.c && valgrind --leak-check=full --show-leak-kinds=all ./testapp
02  ... (valgrind introductory text)
03  ==15791== Syscall param exit_group(status) contains uninitialised byte(s)
04  ==15791==    at 0x4EFC109: _Exit (_exit.c:32)
05  ==15791==    by 0x4E7316A: __run_exit_handlers (exit.c:97)
06  ==15791==    by 0x4E731F4: exit (exit.c:104)
07  ==15791==    by 0x4005F9: main (testapp.c:29)
08  ==15791== 
09  ==15791== 
10  ==15791== HEAP SUMMARY:
11  ==15791==     in use at exit: 32,800 bytes in 1,025 blocks
12  ==15791==   total heap usage: 1,025 allocs, 0 frees, 32,800 bytes allocated
13  ==15791== 
14  ==15791== 32 bytes in 1 blocks are definitely lost in loss record 1 of 2
15  ==15791==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
16  ==15791==    by 0x40059C: fn1 (testapp.c:7)
17  ==15791==    by 0x4005E5: main (testapp.c:24)
18  ==15791== 
19  ==15791== 32,768 bytes in 1,024 blocks are definitely lost in loss record 2 of 2
20  ==15791==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
21  ==15791==    by 0x40059C: fn1 (testapp.c:7)
22  ==15791==    by 0x4005BD: fn2 (testapp.c:17)
23  ==15791==    by 0x4005EF: main (testapp.c:25)
24  ==15791== 
25  ==15791== LEAK SUMMARY:
26  ==15791==    definitely lost: 32,800 bytes in 1,025 blocks
27  ==15791==    indirectly lost: 0 bytes in 0 blocks
28  ==15791==      possibly lost: 0 bytes in 0 blocks
29  ==15791==    still reachable: 0 bytes in 0 blocks
30  ==15791==         suppressed: 0 bytes in 0 blocks
31  ==15791== 
32  ==15791== For counts of detected and suppressed errors, rerun with: -v
33  ==15791== Use --track-origins=yes to see where uninitialised values come from
34  ==15791== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)
35  $

  • Line 14 in the report shows a 32 byte leak caused by invoking fn1 once. Note that valgrind is capable of providing a full stack trace for each leak incident, although it helps to enable your compiler's debug option (in this case, the gcc -g option) for this stack trace to be useful.
  • Line 19 in the report shows a 32768 byte leak caused by invoking fn1 1024 times. Here, we see that valgrind uses stack traces to group instances of the same leak into a single loss report.
  • Line 3 in the report shows that another type of error was detected: use of uninitialized memory. This type of error may or may not be harmless, but in most cases it's best to be safe and assign values to all memory locations before they are referenced.

Again, we've only just scratched the surface of the tools valgrind has to offer. In addition to leaks and use of uninitialized memory, it can also detect dangerous errors such as memory access outside of allocated blocks, double frees/mismatched frees, and overlapping block copies. Going beyond memory checks, valgrind includes tools for heap profiling, cache profiling, and thread debugging. By committing to check our applications with as many of these tools as possible, we can guard against a large number of possible bugs, thereby allowing our software development to move along at a much quicker pace than if bugs were detected by nothing more than the naked eye.
Notes
  • As mentioned, a working project with files shown here can be found in the valgrind-testapp project on GitHub.
  • Running valgrind by hand on our command-line is great, but including valgrind checks as part of an automated build process is even better. Build automation and continuous integration are topics for another article, but I can get you started with a word about the main tool I use to implement them: Jenkins.
 
What did you think of this article? Leave a comment or send us a note. Your feedback is appreciated!