One thing I get asked a lot — almost daily, in fact — is: hey, why are you so amazing
…at identifying bugs related to undefined behaviour in C?
This tutorial is all about using Valgrind as part of your development workflow. Valgrind is an amazing tool for debugging, and I’ll start off by showing you what it actually does just as a standalone tool. From there I’ll show you how to use it in a systematic way to find errors via a debugger. Finally, you’ll see how you can actually add it to your code, so that you can catch runtime errors that might otherwise be concealed by the logic of your code.
So if, like me, you spend most of your waking life writing, maintaining and debugging embedded C code, then it’s time for you to crack open a console and put on your learning hat, and discover a few tricks that will make your life a great deal easier.
What are all these tools and concepts?
What is undefined behaviour?
This post assumes a basic level of knowledge about C, the standards that govern it, and the concept of undefined behaviour... but if you're new to these concepts, here's a quick summary and some references.
Unlike other languages languages (for example, Java), C programs are not required to keep runtime information about array bounds or whether memory accesses are valid. Neither are they required to initialise data to default values (except in very specific cases). If a programmer is not diligent about these things, their program can do something that is completely invalid — that is, undefined behaviour.
Undefined behaviour in a running C program means nothing less than: it is no longer possible to reason about your program. It simply isn’t. Cries of
but it caaaaan’t be doing that! or
x just be the last value?
I didn’t even have monkeys living in the server to begin with! mean nothing in the face of undefined behaviour.
This makes debugging very, very hard.
If you want to know more about undefined behaviour, refer to:
- John Regehr’s A Guide to Undefined Behavior in C and C++
- LLVM’s What Every C Programmer Should Know About Undefined Behavior
- This Stack Overflow answer to Undefined, unspecified and implementation-defined behavior (in C and C++)
What is Valgrind? What is Memcheck?
Valgrind is not a single tool, but rather a set of tools for checking memory errors, cache usage, heap usage and other runtime behaviours, usually in C programs. This post focuses on Memcheck, a tool for identifying invalid or incorrect use of memory (stack or heap).
I am actually going to use the terms "Valgrind" and "Memcheck" interchangeably, since Memcheck is the default tool Valgrind uses when you run the command
valgrind. Just be aware that there are other tools in there too.
The bugs that I’ve found using Valgrind are the worst of the worst, straight out of the C hall of shame. We’re talking about bugs that:
- only appear on one person’s machine
- seem to happen randomly, even in the same environment
- don’t cause crashes, just give you the wrong output
- crash, but the stack trace looks totally wrong (
How did it crash there? I changed code somewhere else entirely!)
- only occur at certain optimisation levels
- only occur with newer compiler versions
Valgrind works by running your executable on a synthetic processer, and whichever tool you’ve selected inserts its own instrumentation code as it runs. You don’t need to recompile with Valgrind, or link with special libraries, or even run debugging builds (although it’s almost always the case that you should). Valgrind runs are significantly slower than normal runs though: about 50 times slower.
But since it cuts your debugging time down by a factor of about a thousand, it’s probably worth it.