More Debugging Tips
Volume Number: 12
Issue Number: 11
Column Tag: Programming Techniques
Simple Yet Effective Bug Detection
Detecting and preventing common programming errors
By Lloyd Chambers, Senior Manager/Software Development,
Note: Source code files accompanying article are located on MacTech CD-ROM or
source code disks.
Introduction
Most programmers are familiar with polling versus notification. Polling is when
you continually scan for events to process until you find something of interest;
notification is when you are told that there is something important to attend to, and
unless there is something to attend to, you can use the time for something else. Polling
is inefficient and wastes resources; notification is a far superior approach because you
only expend effort as needed.
While programmers agree that polling is undesirable, few of them would be
willing to admit that’s what they do everyday when they debug code-they poll for bugs
until they find one by running the debugger, reading code, etc. This article discusses
how to eliminate the old, inefficient polling paradigm of finding bugs with a new
paradigm of bug notification.
This new bug-notification paradigm doesn’t apply to all types of bugs, but it does
apply to a certain class of bugs that can be systematically eliminated while imposing
little or no additional burden on the programmer. The three types of programming
errors commonly responsible for a large number of bugs are: (1) invalid routine
parameters, (2) failure to detect errors, and (3) memory leaks.
This article is divided into four major areas of interest:
• how to set up a debugging system;
• how to create and use asserts;
• how to create debugging versions of system calls;
• how to detect memory leaks.
You will want to read this article from start to finish as later techniques build on
earlier ones.
This article avoids discussing techniques that require significant changes in
coding style, extra work on the part of programmer or more than beginner-level
concepts to use properly. Other techniques can be useful but offer limited “bang for
the buck,” especially for beginning programmers. The techniques discussed here will
be invaluable to all programmers regardless of experience.
The techniques discussed in this article could apply to any environment, though
the implementations shown are for C and C++ environments. The provided sample code
should be easy to retrofit into existing C or C++ projects. Complete implementations
are not provided with this article, but enough functionality is provided to be quite
useful “as is.”
Setting up a debugging system
Before moving into specific debugging techniques, we need to discuss how
debugging code in general should be implemented. There are two general requirements
debugging code must meet-it must be able to detect and report programming errors,
and it must be easy to remove it from the product without altering source code.
Debugging code must be able to report programming errors. A reporting system
can be simple or elaborate. At Symantec, we have chosen a simple system that is
suitable for use in any type of code whether it be an application, INIT, driver, etc.
More elaborate reporting systems can be devised, but they may then become limited in
their applicability to certain types of code and may themselves interfere with the
ability to debug code. Consequently, our basic reporting mechanism is a debugger
break. Though it is primitive, it does the job, and it is simple, non-intrusive, and
applicable to all types of code. Note that a high-level debugger which allows you to step
through code is not a system to detect bugs- rather it is a tool you use to poll for bugs.
The second requirement of a bug detection system is that it can be removed from
the final product so that it causes no run-time overhead. This leads to the building of a
“debug” version and a “non-debug” version. The debug version may run more slowly
and will be larger due to the additional debugging code; however, the debugging code
disappears when a non-debug version is produced. When these two requirements are
met, the programmer can detect bugs, yet the end-user realizes full performance from
the product. It is important to realize that the mechanisms discussed here are focused
on detecting programmer errors, not runtime errors that can reasonably be expected
to occur. As such, all the mechanisms discussed are appropriate for a debug, testing
version targeted at helping the programmer eliminate bugs from code. The mechanisms
discussed here are not intended as an error handling mechanism for your program;
those sorts of things must be handled using other techniques, and ultimately may
involve reporting problems to the end-user.
The DEBUG flag
Compiling a debug or non-debug program is best done using a compiler flag. We
define a flag called DEBUG. In most programming environments, you can define such a
flag in a prefix file which will be included for all source files. With multiple projects,
you can have each project prefix file include a shared file which defines the DEBUG
flag. In this way, you can exercise global control at build time over whether a debug or
non-debug version is built, even if your product consists of dozens of modules. The
DEBUG flag should be defined as 0 for debugging to be off, and 1 if it should be on:
#define DEBUG 1 // debugging on
All debugging code not intended for the final project should be dependent upon this
flag. Debugging macros should be defined to go away when DEBUG is off. Code that is
debugging only should be conditionally compiled. For example:
#if DEBUG
#define DebugMsg( msg ) { DebugStr( msg ); }
#else
#define DebugMsg( msg ) {/*nothing*/}
#endif
#if DEBUG
// debugging code here
#endif
In addition, other more specific compiler flags may be dependent on the DEBUG
flag. In general, if you have a specific debugging module, you’ll want to use a flag for it
which is dependent on the DEBUG flag. This is useful to selectively enable or disable
certain debugging capabilities without disabling other debugging capabilities. By
default, it could be the same as the DEBUG flag, but could also be set differently. For
example:
#if DEBUG
#define USE_DEBUG_TRAPS 1 // on or off as desired
#else
#define USE_DEBUG_TRAPS 0 // always off when DEBUG off
#endif
Creating and using Asserts
Now that we’ve discussed how to set up a debugging system, let’s move on to the
basic building blocks most debugging code will use. Verifying program requirements is
done using “assert” calls and related variants. Asserts allow the programmer to
require and flag run-time conditions that do not meet program requirements. For
example, a routine that takes a pointer may want to require that it be non-nil. Asserts
are used in a number of programming environments and will differ from the
implementation discussed here.
Asserts may be very simple or quite complex. They are best implemented as
macros so that they can be compiled out of the code in a non-debug version. Note that a
macro may call a routine, or may use inline code, depending on its complexity. Here
are the asserts and related variants we commonly use at Symantec:
// break with message if condition is false (zero)
Assert( condition, failureMessage )
// break with message if false; append number to it
AssertNum( condition, failureMessage, theNumber )
// break into debugger with message
DebugMsg( message )
// break with message if true (non-zero)
DebugMsgIf( condition, message )
// break with message; append number to it
DebugNum( message, theNumber )
// break with message if true; append number to it
DebugNumIf( condition, message, theNumber )
For example, if you want to verify that a pointer is non-nil, you would write the
following assert:
Assert( thePointer != nil,
“\pRoutineName: nil pointer”);
You can also use higher level asserts which perform a fair amount of computation
to verify the assertion. In general, more complex asserts verify the integrity of a
program entity. The following asserts break into the debugger with the specified
message plus additional useful information when there is a problem:
// break if the Handle is invalid
AssertHandleIsValid( theHandle, failureMessage )
// break if the Handle is invalid or not a resource
AssertResourceIsValid( theResource, failureMessage )
// break if the address is invalid
// or not aligned to the specified boundary
AssertAddressIsValid( addr, failureMessage )
AssertAddressIsValidAlign( addr, alignBy, failureMsg )
AssertAddressIsValidAlign2( addr, failureMsg )
AssertAddressIsValidAlign4( addr, failureMsg )
// break if ‘fsSpec’ is not a valid FSSpec
// also displays FSSpec and describes why it is invalid
AssertSpecIsValid( fsSpec, failureMsg )
// break if ‘refNum’ is not a valid ref num
AssertFileRefNumIsValid( refNum, failureMsg )
// break if ‘upp’ is not a valid universal proc ptr (PPC)
AssertUPPIsValid( upp, failureMsg )
// break if ‘string’ is too long or too short
AssertStringIsValid( string,
minLength, maxLength, failureMessage )
// break if ‘desc’ is invalid
AssertAEDescIsValid( desc, failureMsg )
// break if err is anything other than ‘noErr’ or a cancel
// also displays the error code
AssertNoErr( err, failureMsg );
DebugIfErr( err, failureMsg );
How to declare Asserts and other debugging routines
Debug code should be defined to compile in when DEBUG is on, and compile to
nothing when DEBUG is off. In the following example, the thing to notice is that when
DEBUG is off, the macro is defined in such a way that no code is generated by the