Debugger Tips: 8 ways breakpoints can save your next software projectEveryone who has used a debugger knows what breakpoints are, but when combined with other debugger features breakpoints become much more powerful tools. In this article, I will talk about how to combine these features and give examples of how such combinations can be used to solve some real world problems. In essence, this is a list of tips and tricks - some using familiar features in innovative ways, and some using lesser-known features of which you may not already be aware.
To start with, a large proportion of the embedded industry still uses what boils down to printf debugging. For small or simple programs, this method can work well. However, when a system grows in size and complexity, this can become a serious road block to fixing bugs quickly.
For instance, I visited one company that had a program that took over an hour to re-link. They were still trying to debug with printf, but due to the rebuilds required, they were able to track down just a couple of problems per day at best. More complex problems were taking days or even weeks to track down.
A slow build might not be the only reason that printf debugging could kill your productivity: You may do initial development on a desktop machine which has fast output, but when you try to port to your embedded system it may only have a slow serial port for output (or perhaps no output mechanism at all other than turning a light on and off). If it is slow enough, dumping even a little printf output may make the system non-functional.
Tip #1. printf Breakpoints: In spite of these issues, there are good reasons that printf is so commonly used: It's simple, it works most of the time, and most importantly, developers are used to it. So the first use of breakpoints will be to replicate the familiar approach of printf, but from within the debugger. This gets you most of the advantages of printf with much more flexibility. To emulate printf debugging:
1) Make a breakpoint where you want to print some information about your program
2) Add commands to that breakpoint which will print that information
3) As the last command in the breakpoint, resume execution of the program
The result will be a printf-style log of the output of your program, only now printed out from within the debugger. This approach has several advantages over traditional printf debugging:
* The debugging output is available from within your debug session, and does not depend on some other target output mechanism which may be slow, unreliable, or not available.
* If the program goes into a bad state, then printf output can be lost or corrupted. With a debugger, you know that the output you see is from the actual state of the target, and there is no chance that the target has corrupted or dropped it.
* It is possible to add and remove logging without needing to recompile your program.
* It is possible to print information about any part of the program from within the breakpoint. With printf debugging, you need to have programmatic access to the variable you are interested in, which may require creating a new interface just to get this data.
The problem with this emulated printf approach is that in some environments your program can end up running significantly more slowly than it would using normal printf. This is because hitting and resuming from a breakpoint can be an expensive operation.
Whether this is true of your system or not depends on the speed of your debug connection, the speed of your debugger and its ability to debug your program, and the speed of your printf output mechanism. However, if hitting and resuming from breakpoints is slower than printf there are still some things that you can do to make use of this approach:
1) Check to make sure that it actually matters. Many bugs will still reproduce, they may just need a few more seconds of run time. If the slower runtime it isn't actually a problem you don't need to do anything about it.
2) Don't turn the printf-breakpoints on until your program has reached the point you want to start inspecting. There's a good chance point 1 will apply for a smaller chunk of your program.
3) Depending on what you are looking for, you can make a small change in your program to introduce a code path that is only executed when the case you are interested in happens. Set your printf breakpoint there. For instance:
You can also combine all three of these approaches. For instance, if you have a top level event loop you could set a breakpoint there that determines if some interesting condition is true. If it is, then that breakpoint would then enable all of the printf breakpoints that you have set but left disabled throughout the rest of the system. If the event loop is slowed down enough by this breakpoint, then encode the condition into the event loop as in the third approach that I listed above.
Tip #2. Call Stack Breakpoints: Moving beyond printf debugging, there are a number of other techniques that are possible when combining different debugger features. Related to printf breakpoints are "call stack breakpoints". In this case, instead of just printing out a particular variable when a breakpoint is hit, we ask the debugger to dump out a full call stack. Later, analyze the call stack log to see if there is anything unexpected going on.
I used this technique recently when I suspected that a particular function was being called in an unexpected way, but I didn't know how or why. To track the problem down, I set a breakpoint on the function in question that would dump out the call stack and resume. When I looked through the output, one call stack stood out because it was significantly deeper than the others. Taking a closer look, I realized that it was being called recursively, though I still didn't understand quite why.