7 Top Tips for Debugging C++
Today we have Greg Law with us. In this guest blog post, Greg will share a few top tips for debugging C++ code.
Greg (@gregthelaw) is the co-founder and CEO at Undo. He is a programmer at heart, but likes to keep one foot in the software world and one in the business world. Greg finds it particularly rewarding to turn innovative software technology into real business development. Greg has over 20 years of experience in both academia and innovative startup software companies.
Brian Kernighan famously said, “Everyone knows that debugging is twice as hard as writing a program in the first place. So if you’re as clever as you can be when you write it, how will you ever debug it?”. For me, this doesn’t just mean “keep it simple.” It also means debugging is central to programming – you cannot be a great programmer without being great at debugging. Hopefully, my favorite C++ debugging tips will help you be as clever and productive debugging your code as you are writing it in the first place.
- #1 Have a full kit of debugging tools
- #2 Conditional breakpoints
- #3 Watchpoints
- #4 User-defined debugging commands in Python
- #5 Pretty-print structures
- #6 Time Travel Debugging
- #7 Command find to search for a byte sequence
- Want more C++ debugging tips?
#1 Have a full kit of debugging tools
All else being equal, a better-equipped developer will avoid and resolve defects faster than one who is less well equipped. Here are 4 categories of debugging tools that I recommend every programmer should know how and when to use. I’ve included commercial and free open-source options for you to explore:
|Category||What it tells you||Example tools|
|Interactive debugger||Pause execution and explore “What’s my program doing?”||GDB, strace|
|Time travel debugger||Step backward and forward through execution time to see how your program arrived where it did.||UDB, rr, WinDbg|
|Dynamic code checkers||Analyze or instrument your code to check for buffer overflows and other defects.||Valgrind, ASan|
|Static code checkers||Analyze your code to determine whether there’s a risk of specific defects occurring.||Clang Analyzer and Clang-Tidy, Coverity, Cppcheck, IDE built-in linters|
In this ACCU talk, Dewang Li of Coverity and I go through these categories of tools and explain how they work under the hood.
CLion Debugger tips from Anastasia: CLion doesn’t only let you debug your code with GDB or LLDB backends. It also helps with other tools, including Valgrind and Sanitizers integration, as well as various options for static code analysis.
#2 Conditional breakpoints
A breakpoint lets you stop the execution of the program at a specific line or function in your code. Once your program hits a breakpoint, it waits for instructions from you to inspect or manipulate the application state, resume execution, etc.
To help debug more efficiently, I’m fond of using conditional breakpoints. Instead of pausing every time the breakpoint is reached (which can be tedious if the breakpoint is defined within a loop), I can define a condition for a breakpoint that stops the execution if met. For example, if variable “i” normally equals zero, maybe I want to break if “i” is not zero:
(gdb) break my_func if i!=0
You can write pretty much any condition you want in the programming language of the program you’re debugging, which makes conditional breakpoints very powerful and efficient. The condition can include a function call, the value of a variable, or the result of any GDB expression.
To learn more, check out this video tutorial, in which I explain conditional and various other breakpoint types, and how to use each of them in your debugging.
CLion Debugger tips from Anastasia: In CLion you can easily specify a condition that is checked each time a breakpoint is hit.
Like breakpoints, a watchpoint stops execution, but does so whenever the value of an expression changes, without having to predict a particular line of code where this may happen. Watchpoints are extremely helpful when debugging concurrency issues, like when trying to understand what thread or process is changing a shared resource. The expression may be as simple as the value of a single variable, or as complex as many variables combined by operators. Examples include:
- A reference to the value of a single variable.
An address cast to an appropriate data type. For example,
*(int *)0x12345678will watch a 4-byte region at the specified address (assuming an int occupies 4 bytes).
An arbitrarily complex expression, such as
a*b + c/d. The expression can use any operators valid in the program’s native language.
Here’s a blog post and video demonstrating how to use different types of watchpoints.
CLion Debugger tips from Anastasia: If you are wondering how to add a watchpoint in CLion, check out the webhelp.
#4 User-defined debugging commands in Python
I recommend tailoring your debugger to fit your project and team. In GDB, this can be done by creating user-defined commands in Python. You can do all kinds of smart things to help make detecting (and resolving) thorny bugs a breeze. Plus, there are lots of other tricks you can do to customize GDB to your particular project and debugging needs.
Not taking advantage of Python is something you may regret later – a missed opportunity to increase your debugging speed, not to mention your general quality of life! It’s a small investment in time that pays back quickly and, over time, significantly.
For example, you can automate a task like checking debugger output into source control and sharing it with your teammates. In this blog post, I show how the Python integration with GDB can be used to do exactly that.
CLion Debugger tips from Anastasia: Did you know that during a debug session, you can access the GDB/LLDB console directly from CLion? The tab shows the debugger’s output/error stream and lets you run GDB/LLDB commands.
#5 Pretty-print structures
Printing variables, structures, and classes is a big part of debugging. By default, the debugger might not print the value in a way that makes it easy for the developer to understand.
For example, when I print the
siginfo_t structure, the print command returns all the data in the structure, including expanding the unions it uses:
Messy and not easy to read!
Fortunately, GDB can be extended with “pretty-printer” functions. When GDB prints a value, it checks whether there is a pretty-printer registered for that value. If so, GDB uses it to display the value. Otherwise, the value prints in the usual way.
It takes a little coding up front to create the pretty-printer function, but I promise it will save you so much time staring at your computer screen. Here’s a video in which I demonstrate a quick way to pretty-print structures in GDB.
CLion Debugger tips from Anastasia: If you add custom pretty-printers to GDB, CLion will use them by default.
#6 Time Travel Debugging
Very often, you need to know what your program actually did, as opposed to what you expected it to do. This is why debugging typically involves reproducing the bug many times, slowly teasing out more and more information until you pin it down.
Time Travel Debugging takes away all that guesswork and trial and error – the debugger can tell you directly what just happened.
Free debuggers like GDB have built-in Time Travel Debugging capability. It works pretty well, but you have to be ready to sacrifice performance (a lot of it). Commercial, purpose-built time travel debuggers like UDB offer much faster Time Travel Debugging performance.
The process is like regular debugging except that you can step/continue back in time. Breakpoints and watchpoints work in reverse, which can help you, for example, to continue directly to a previous point in the program at which a specific variable changes. Reverse watchpoints can be incredibly powerful. I know of several instances of bugs that eluded a developer for months or even years that were solved in a few hours with the power of reverse watchpoints.
In this video, I demonstrate reverse debugging in GDB. Check it out: it shows you step by step (in reverse) how to trace a fault in my program.
#7 Command find to search for a byte sequence
Sometimes when you’re debugging, you need to find a particular sequence of bytes in the memory space of the program. Perhaps you want to see all the pointers to a specific object. So, every eight bytes in memory that corresponds to the byte sequence is an address you want to identify.
find command in GDB offers you a different kind of inspection into your program. All search values are interpreted in the programming language of the program. For example, the source language of hello.c is C/C++; so if we search for the string “Hello, world!”, it includes the trailing
GDB also provides information about the memory mappings of program processes, which helps you focus the search in certain segments of the program’s memory. The address of each match found is returned, as well as a count of the number of matches.
Want more C++ debugging tips?
I’ve shared seven tips for better C++ debugging productivity. Hopefully they help you out – sometimes little tweaks to your usual debugging habits and routines can make a big difference. And let’s share! I’d love to hear your top tips as well. You can reach me on Twitter or find me on LinkedIn.
Here are some resources to help you on your path to better debugging:
- GDBWatchpoint – Subscribe to my blog on undo.io
- Get GDB – free C/C++ debugger
- UDB – Commercial time travel debugger (free 30-day trial)
And from the JetBrains side, here are a few more resources for CLion:
Thank you, Greg, for sharing these useful tips!
And a question to our audience: what is your favorite debugger tip? Please share in the comments!