Debugging


Debugging with Macro Wrappers

William Smith


William Smith is the engineering manager at Montana Software, a software development company specializing in custom applications for MS-DOS and Windows. You may contact him by mail at P.O. Box 663, Bozeman, MT 59771-0663.

Identification and localization of software bugs are the hardest parts of the debugging process. Testing your code can show the presence of bugs but cannot guarantee the absence of bugs. Embedding debugging code in your program is a good way to expose bugs that may not appear during testing. Once you identify a bug, you must localize it by finding the offending lines of code that are causing the problem. One way to help localize bugs is to use the __FILE__ and __LINE__ macros that the ANSI standard defines. The __FILE__ macro expands to a string constant containing the name of the current source file. The __LINE__ macro expands to an integer constant containing the current line in the source file.

Using these macros inside debugging macro wrappers for functions and C keywords can help pin down the whereabouts of elusive bugs. Macros have another advantage. Using conditional compilation, they make it easy to either incorporate or remove the debugging code from a program without any modification to the program. Using macro wrappers for functions is straightforward and a common practice. Macro wrappers for C keywords such as return and default is novel and not as well known. I am going to show you how to create and use debugging macros for both C keywords and standard library functions. These macros can help pin down the exact location of problems in your code.

Recursive Macro Wrappers

The first challenge is to get the __FILE__ and __LINE__ macros embedded into your code without having to make significant changes to your existing code. Using recursive macros is a solution. A recursive macro is a macro that contains the name of the macro in the macro definition. You can do this for macro names that expand to C keywords or functions. By taking advantage of this, you can create macros with the same names as functions or C keywords that already exist in your code. This allows you to incorporate debugging features into your code without having to modify the code. At most, you may have to add an #include statement to your program to incorporate the macro definitions.

Recursive macros also provide for easy removal of debugging statements from your code. Since the debugging code can add significant overhead to your program, the ability to remove the debugging statements and create a release version of your program is important.

In general, the debugging statements added with recursive macros check for error conditions and then generate a message with the location of the error identified with the file name and line number. Common error conditions checked for are out-of-range arguments passed to functions and return values of functions that indicate a failure condition.

Macro for return Keyword

It is customary to use the return value from a function to indicate success or failure. It is also a common practice to ignore this value. This is especially true when generating a return value is not the major purpose of the function. Adding code to check for failure conditions every time a function is called can be expensive in terms of space and time. A macro wrapper for the return keyword is a handy way to turn this checking code on and off. Macros allows you to create a debugging version of your program and expose failed conditions. As well as a debugging tool, a macro wrapper for the return keyword also allows for the installation of central error processing.

Listing 1, DBG_RTRN.H, is an include file that defines a macro replacement for the return keyword. This macro inserts code at every return statement. This code checks to see if the return argument is equal to EXIT_FAILURE and sends a message to stderr if this condition is true. Instead of stderr, you could just as well write the message to a file or display a message in a window for programs with an interactive user interface. The standard library include file <stdlib.h> defines the value of EXIT_FAILURE. You may find you use a different value to indicate failure. This macro has the side effect of trapping and displaying messages for possible valid return values that may happen to correspond to EXIT_FAILURE.

Many functions return pointers instead of a return condition code. You may find it useful to trap a null pointer return value as an error code in some situations as opposed to an EXIT_FAILURE return value.

Notice that the return macro in Listing 1 is a multi-line macro consisting of more than one statement. It is worth noting that the safe way to create multiple statement macros is to enclose the statements in a do-while construct that executes only once. To ensure that the macro executes only once, make sure that the expression in the while statement evaluates to 0, such as while (0). Another caveat about the return macro is that it requires you to enclose the return argument in parentheses. Without the parentheses, the macro will not expand.

Monitoring Available Stack Space

Macro replacement of the return keyword is in general a handy way to insert code that your program will execute right before control exits the function. A useful example is to insert stack-checking code. For environments where memory is at a premium, you may want to size you stack so that it is not any larger than absolutely necessary. A macro wrapper for the return keyword facilitates the ability to check for the minimum free available stack space in a program.

Listing 3, DBG_STAK.H, contains a macro to do just this. This macro for the return keyword inserts a call to the function check_min_stackavail. This function is in Listing 2, DBG_STAK.C. check_min_stackavail makes a call to the Microsoft C function stackavail and keeps track of the minimum amount of stack available in a static variable, _MinStack-Avail. To check the amount of unused or surplus stack space in your program, run your program, exercise all the features, and then upon exit check this static variable. The function get_min_stackavail in Listing 2 retrieves this value. The goal is to set the stack size so the value returned by get_min_stackavail is as small as you feel is safe.

Macro for default Keyword

The return keyword is probably the most powerful and useful keyword to replace with a debugging macro. Other keywords such as default, break, and continue can come in handy in specific situations. I have found a general macro wrapper for the default keyword to be a powerful method of exposing elusive bugs.

It may not seem apparent at first how to use the default label for debugging. The default keyword appears inside switch statements. I recommend that every switch statement have a default label. You should use a default label even inside switch constructs that you are sure have individual cases to account for all the expected situations. This is the exact situation where this technique can yield important information. Adding the lines

default: break;
to switch statements that appear not to need them usually does not slow down or increase the size of your compiled program. The default keyword in a switch statements allows you to use a debugging macro for the default keyword.

The default macro can trap unexpected conditions. These situations are ones that only arise if memory is corrupt, an array has run over its bounds, a variable is not initialized, or the code behaves in some other way that you did not intend. This technique has helped me identify bugs that did not appear in any other way. It has helped me find memory overwrites, heap corruption problems, and just plain programmerinduced errors. Eventually, these bugs would have surfaced, but not nearly as quickly.

Listing 4, DBG_DFLT.H, is an include file that contains the macro wrapper for the default keyword.

Because of the colon after the default keyword, the macro has to pull a trick. It adds an extra case label. The macro must do this to avoid a dangling-colon syntax error. I use the INT_MIN value for the case. I hate actually inserting code that may modify the intended behavior of a program, but I could not find a better way. I tried creating a unique dummy label using the __LINE__ macro and the token-pasting preprocessor operator (##). I found, however, that this trick only worked on an older version of a popular compiler. The ANSI Standard says the pasting takes place before the expansion of the macro token. Most compilers pride themselves in strict conformance to the Standard.

You will probably have to resort to inserting an extra case label. If you know of another way to create a macro wrapper for the default keyword without resorting to modifying your original code, let me know. Or if you know of a way to create a unique label name at compile time, let me know.

Macro Wrappers for Functions

Nearly every Standard C library function is a potential candidate for wrapping with debugging code. The debugging code can check for appropriate ranges of the arguments passed to the function. Obviously, I cannot list possible macros for them all. A particularly sensitive set of functions are the math functions. I also find localizing errors generated by the math functions difficult. The built-in matherr debugging function will trap errors and generate an error message, but you cannot use matherr to indicate where in your program the error came from. [And matherr is not in the Standard C library. — pjp]

By encapsulating the math functions in macros, you can incorporate debugging instrumentation. I have found the flexibility and power of this method superior to using matherr. The main advantage to the macro method is that, by using the __FILE__ and __LINE__ macros, it exposes the location in the source code where the offending function call occurred.

Listing 5, DBG_MATH.H, contains macros for five different math functions. Let these macros inspire you to come up with your own for situations that are most troubling for you. I also use macro wrappers to check for null pointer arguments. Some of the string functions and I/O stream functions are sensitive to null pointers as arguments.

An Example

The following code is a program that demonstrates the macros in Listing 1, Listing 4, and Listing 5. If you define the macro NDEBUG, debugging macro replacement does not occur.

#include <float.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#include <dbg_rtrn.h>
#include <dbg_math.h>
#include <dbg_dflt.h>

int main( void )
     {
     acos( -2.0 );
     asin( 2.0 );
     log( 0.0 );
     log10( -1.0 );
     sqrt( -1.0 );
     switch( 2 )
          {
          default:
               break;
          case 1:
               break;
          }
     return ( EXIT_FAILURE );
     }
The __FILE__ macro upon expansion can take up a lot of space if the compiler doesn't merge identical string literals. You may want to create a static variable with single-module scope that contains the file name and use this in place of the __FILE__ macro. This is especially true if the debugging code ends up inserting the __FILE__ macro in many places in your code. Adding a definition like the following in you code and making the necessary changes to the debugging macros that use __FILE__ can help if your compiler limits the amount of data space your program can have.

static char _FileName[] = __FILE__;

Conclusions

Debugging is a form of problem solving that requires a creative touch. The first step is to identify the problem. The next step is to localize it. Debugging macros wrappers for C keywords and functions help with both steps. Once you have identified and localized a problem, the solution usually follows quickly. The macros presented here are a collection of novel and useful techniques to aid the debugging process.