Software Tools


Debugging With Sherlock

Comments by Edward K. Ream


Edward K. Ream is the author of the RED text editor, CPP (a C Preprocessor), the CSTAR language and compilers and Sherlock, all of which are in the public domain and available from the C Users Group Library. He received a masters degree in computer science from the University of Wisconsin in 1974 and is the owner of Sherlock Software. His computer interests include compiler implementation, language design and debugging and verification techniques.

Normally, writing debugging statements into your code is tedious. In addition, the code can produce huge amounts of unwanted output and slow programs to unacceptable levels.

I wrote a set of programming tools, called Sherlock, which tackles the problems caused by adding debugging code to programs. With Sherlock, debugging code can be enabled or disabled without recompiling and relinking your program. Inserted printf statements trace your functions and data structures exactly as you desire. You can insert heavy-weight assertions — detailed code to check your data structures — wherever you wish with no unwanted time penalty. Finally, Sherlock measures program performance at whatever level you desire.

Sherlock contains over 40 macros and dozens of hidden support routines, plus three stand-alone utilities. In particular, the SPP tool contains the front-end and parser of a full ANSI C compiler, with extensions to handle Microsoft C, Turbo C, MPW C, and Think C. There are more than three full programmer years worth of code in Sherlock.

I have contributed Sherlock to The C Users' Group Library. Sherlock is not protected by copyright and is in the public domain. The volume includes full source code.

Beyond DEBUG

Sherlock fixes two defects in DEBUG, a macro commonly used to disable debugging code. A typical definition of DEBUG is

#if DEBUG_ON == 1
 #define DEBUG(code) code
#else
 #define DEBUG(code)
#endif
The code argument is assumed to be one or more C language statements. If DEBUG_ON equals 1, DEBUG produces the code, while if DEBUG_ON is defined to be zero, it produces nothing at all.

A serious drawback to DEBUG is its "all or nothing" approach — all debugging code is enabled or none of it is. One can improve DEBUG by adding different "levels" of debugging, for example,

#if DEBUG_LEVEL == 2
 #define DEBUG2(s) s
 #define DEBUG1(s) s
#elif DEBUG_LEVEL == 1
 #define DEBUG2(s)
 #define DEBUG1(s) s
#else
 #define DEBUG2(s)
 #define DEBUG1(s)
#endif
DEBUGn is enabled only if DEBUG_LEVEL == n. Sherlock goes even further. It enables statements, either individually or in groups, using a much simpler and more general method.

Another problem with DEBUG is that it enables or disables debug code only at compile time. Sherlock enables and disables code at runtime by using debugging macros that manage a runtime symbol table. This Sherlock symbol table is initialized during program execution, either from a command line, dialog box, argument file, or Sherlock macros. Entries in the table are created automatically by the Sherlock macros themselves. Sherlock support routines (hidden routines called from the Sherlock macros) indicate whether individual symbols are currently enabled. They also perform other behind-the-scenes chores.

Sherlock Macros

A typical Sherlock macro might be defined as

#ifdef SHERLOCK
 #define TRACE(name, code)\
 if(sl_trace (name)) {code}
#else
 #define TRACEP(name, code)
#endif
The argument name is a C string that names a symbol. TRACE expands to code that calls sl_trace, a support routine that returns TRUE if the symbol is currently enabled in the Sherlock symbol table. For example, in the code

void foo (int a)
{
 TRACE("foo", printf("%d/n, a));
}
the name argument is "foo" and the code argument is,

printf("%d\n", a);
The TRACE macro is deceptively simple. The printf statement is executed only if the symbol foo is enabled. Otherwise nothing happens.

TRACE can disable code either at compiletime or runtime. If SHERLOCK is not defined, the TRACE macro produces no code at all. More importantly, the code produced by TRACE remains disabled unless explicitly enabled at runtime.

TRACEP is one of several variations on TRACE. TRACEP prints the symbol before executing the code. For example, the effects of the two macros

TRACEP (" foo", printf ("%d\n" ,a));
TRACE("foo",printf("foo: %d\n",a));
are identical.

Sherlock can enable and disable symbols singly or in groups — the details don't matter here. What does matter is that

An Important Optimization

As previously defined, the TRACE macro calls sl_trace to search the Sherlock symbol table every time TRACE is executed. After using Sherlock for a while, I discovered a method to define TRACE so the symbol table is searched only once. The improved definition is

#ifdef SHERLOCK
 #define TRACE(name, code) \
 {static void * h = NULL; \
 if (sl_trace (&h, name) ) {code}}
#else
 #define TRACE(name, code)
#endif
This version of TRACE defines a separate handle h for each macro call. The first time TRACE is called, the handle h is NULL and sl_trace searches for the symbol in name. An entry is created and tracing is disabled if the proper entry does not already exist. In any case, sl_trace changes h to point to the proper symbol table entry. Finally, sl_trace returns TRUE if the symbol is currently enabled. The second time TRACE is called, sl_trace uses h to access the symbol table without searching. Using a handle to the symbol table entry insures that sl_trace will always return the current tracing status of the symbol. Sherlock macros are very fast because the symbol table is only searched the first time a macro is called.

Evaluating Sherlock

Sherlock is easy to use. Adding another TRACE macro is trivial. You don't have to write new argument handling code, and no new variables need to be defined. In addition, Sherlock includes a utility called SPP which inserts Sherlock macros throughout any kind of C source code, from K&R C to ANSI C — handy when using Sherlock in an existing program.

Sherlock is flexible. When using a command-line environment, I enable the symbol foo simply by adding ++foo to the command line. In effect each symbol defines a separate debugging switch. Groups of symbols can be enabled or disabled using wildcard characters like * and ?. A Sherlock initialization macro strips these Sherlock arguments from the command line so they do not interfere with the application's argument processing logic.

The Macintosh version of Sherlock gets Sherlock arguments either from a dialog box, an MPW command line, or an argument file. I typically add hundreds of TRACE macros to a large program. The more TRACE macros I add, the more flexibility I obtain.

Sherlock is fast. TRACE macros do not slow my programs significantly because Sherlock's support routines don't do much searching. In practice I use TRACE to speed up my debugging code. Indeed, I use TRACE to disable heavy-weight assertions, which perform complex checks on large data structures. For example, suppose a routine called check_s checks a data structure s. After creating or modifying s, insert the following macro,

TRACE (" chec k_s",
 ASSERT(check s(s));
The ASSERT macro, which is part of the Standard C library, writes an error message and aborts if its argument does not evaluate to TRUE. It shares both of DEBUG's failings: it is an all-or-nothing macro and can disable code only at compile time.

Using ASSERT by itself would be impractical because check_s takes too much time. Sherlock lets me add ASSERT macros without worrying how slow they are.

TRACE produces no unwanted output. Since Sherlock macros are disabled by default, printf statements may be inserted anywhere without worrying about how much output they produce. Just as Sherlock encourages heavy-weight assertions, Sherlock encourages very sophisticated print routines. Because traces are so easy to enable, I use traces more often and can justify (to myself) the extra work involved in making them sophisticated.

Finally, Sherlock produces fine-grained statistics. Sherlock support routines gather statistics and store them in the Sherlock symbol table. A Sherlock macro prints all symbols and their associated statistics in alphabetical order. I can quickly and accurately measure, to the millisecond, how any section of my code performs. If I need more accuracy, I need only add another Sherlock macro.

Conclusion

Sherlock takes a very different approach to debugging. It can be of value to many C programmers. Once you use Sherlock you may well wonder how you ever programmed without it. I think that Sherlock is more flexible and easier to use than any other profiling tool.

C Users Group Library
Volume 355 (MS-DOS)
Volume 356 (Macintosh)
1601 W. 23rd St., Ste. 200
Lawrence, KS 66046
(913) 841-1631