Debug technique in C++ programming
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?”
Debugging tools
This category from this link
Category | Whait 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 |
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:
1
(gdb) break my_func if i!=0
Watichpoints
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 *)0x12345678 will 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 (see Languages).
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.
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.
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.
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.
The 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 ‘\0’.
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.
Some useful link
Debug C++ code using Visual Studio