Bugs Nevermore!
"Once upon a midnight dreary, while I pondered, weak and weary . . . "
Does this line from Edgar Allen Poe's "The Raven" sound like the introduction to one of your program debugging sessions? If so, this article is for you. It presents C language source code for a powerful and easy-to-use symbolic debugger that could save you hours and days of time.
To use the debugger you merely press a debug key, your debug statements begin to execute, and the results appear in a full-screen virtual window. You can see every function call and variable you select as they change in the virtual window, with out disrupting your programs screen displays. Press the key again, the debugger disappears, and you are back to your original screen. Your program then executes as if the debugger never existed -- until you press the key again.
As each debug statement executes, the results appear on the bottom row of the screen. The results of the 23 previously executed debug statements appear on the rows above it. The debugger waits while you review this information. You then press a key to cause your program to resume execution. The entries on the screen are then pushed up by the execution of the next debug statement, the results of which appear on the bottom row of the screen.
Listing 1 contains the header file, DEBUG.H. To use the debugger, you code the macros defined in the header file into your program at the points you wish to debug. A macro statement is defined for each C data type. Each macro statement takes two arguments. The first argument is a text string of your choice, typically a function or variable name, or information about the program status at that point in its execution. The second argument must be a variable of the type expected by the macro. For example, TC() is the first macro definition in the header file (lines 14-15). TC means trace a character." Its first argument, user_text, is a null-terminated character string of your choice. Its second argument, character_variable, is the name or value of a character variable. The macro execution statements you code will only be compiled if you include the #define DEBUG statement either in the header file as shown or in your program.
The DEBUG statement controls the C language conditional compilation feature. This feature gives you a simple way to suppress your debugging statements without deleting them from your program-you just delete the #define DEBUG statement. Alternatively, some C compilers allow you to make an entry on the command line that will define the DEBUG statement at compile time. That way you don't need to modify your source program to include and exclude the DEBUG statement; you just modify the command line that executes the compiler.
The macro statements defined in DEBUG.H contain another feature --"they test the value of a variable called trace_sw (line 6). If trace_sw is ON the debugger will single-cycle through your debug statements as they are executed, showing the results of each debug statement on the bottom of the screen. If trace_sw is OFF, the debugger will not execute. This means that your program will execute at its normal speed, without the overhead of the debugger.
Listing 2 contains the debugger, DEBUG.C. (DEBUG.C is available from the COMPUTER LANGUAGE Bulletin Board Service and CompuServe account or on disk via the COMPUTER LANGUAGE Users Group --"see page 4.)
The debugger contains the functions called by the macros in DEBUG.H. Each function is designed to format a variable of a specific C data type into a string in an array called user_data so that it can be displayed. For instance, the macro TC () calls the function t_c() (lines 101-111 in DEBUG.C). This function formats a character variable and substitutes literals for the characters BLANK and NULL since they are invisible. t_c() then calls the function display_trace(), passing to it two arguments --"your text string and the user_data array, which contains the variable in string format.
displaytrace() performs a number of actions. This function: I Initializes the trace table the first time it is executed. I Uses movmem() to bump the trace table up by 160 bytes -- 80 bytes for the number of characters that can be displayed in a row on the screen plus 80 bytes for their video attribute bytes. This frees up the last row of the trace table so that it can accept the new trace entry. movmem() is a fast memory move function provided with the Lattice C compiler. You could write a similar function in assembly language, if your compiler package does not provide a similar function. I Initializes the last row of the trace table. I Moves your text string into the last row of the trace table. I Moves the formatted variable from user_data into the last row of the trace table. display_trace () then calls an assembly routine CRTBUF() (line 95) to save your video screen from the video buffer into a save area. It saves the video buffer so that the trace table display does not disrupt your screen. The CRTBUF() source listing and description were published in my article "Assembly Video Routines for your PC" (COMPUTER LANGUAGE, Jan. 1986, pp. 49-58).
Next, display_trace() calls CRTBUF() again (line 96) to move the trace table to the video buffer. This causes the trace table to be displayed on the screen. It then calls wait() (defined in lines 53-56) to cause the debugger to pause until you press a key. This allows you to view the results of your last 23 debug statements on the screen at your leisure. display_trace() then calls CRTBUF() again (line 98) to replace your video screen from the save area back into the video buffer. Your screen is then displayed and under the control of your program until the next debug statement is executed, at which time it is again saved.
By means of the trace table format, the debugger divides the trace table screen display into two 40-character columns. The left-hand column contains the text you supply as the first argument of the macro call. The right-hand column contains the value of the variable or constant you supply as the second argument.
DEBUG.C contains an additional function that facilitates the use of the debugger. The inkey() function (lines 35-5l) allows you to turn the debugger on and off by pressing the key of your choice. inkey() is designed for use with the IBM PC and compatibles. The value of the key that controls the trace is #defined as CONTROL_KEY (line 12). If you desire, you can change this to any other key value. The values of the keys are described in the appendix to the IBM PC BASIC manual.
If you want to be able to turn the trace on and off with the press of a key, you must use inkey() in your program modules in place of getchar() or any of the other C language functions that access the key-board buffer --"at least at the points in your program that you want to trace. Alternatively, you could code getchar(), etc., as macros in DEBUG.H which would call inkey(). Then, when you removed the #include DEBUG.H from your program, the getchar(), etc., from the standard library would be called again. inkey() does a bdos() call (line 46) to get the key value entered at the keyboard.
bdos() is an interface to the operating system provided with the Lattice C compiler.
If your compiler package does not provide such a function, you could code one in assembly language using DOS interrupt 0x2l.
The IBM PC keyboard functions return an extended code for certain keys or key combinations that cannot be represented in standard ASCII code, such as the function keys, etc. In these cases, a null character (ASCII code 000) is returned as the first character of a two-character set. The inkey() function uses a typedef structure defined in DEBUG.H to process this two-character set. This structure is called TKB (lines 34-38). It consists of two character fields: char1 and char2.
If the first bdos() call in inkey() returns a binary zero into char], then bdos() is called again to return the extended code into char2. inkey() tests the values returned in char] and char2 to control the execution of the trace.
inkey() toggles the variable trace_sw on and off when you press the debug key (CONTROL_KEY) you have established.
As long as trace_sw is ON, your debug statements will continue to execute, and you will continue to see the trace table on the screen.
Of course, you do not need to use inkey() to turn the trace on and off. You can toggle the trace at any point in your program by assigning trace_sw a value of ON or OFF. In this case, you will still
need the inkey() within the wait() function to cause the debugger to pause so that you can view the trace table on the screen.
Listing 3 contains a sample driver program designed to illustrate the use of the debugger. main() (lines 44-52) contains an important feature. The printf() message "Press F10 to start trace" (line 47) is one of the statements used to control the start of the trace. The second statement, inkey() (line 48) then causes the program to pause until you press a key.
If you press function key 10 (the debug key in this example), then your debug statements will be executed as they are encountered. If you press another key instead, such as return, then its value will be discarded and your program will execute normally, bypassing the debug statements. You can use this method to start the trace in any module of your program.
When you code it into main (), it causes the debug statements in all of your modules to be executed.
In the sample driver program, main () executes func1() (line 49). func1() illustrates how to code all of the debug macros. For example, TU() (line 37) traces an unsigned integer. Some compilers provide different memory models. TU() may be used with the small memory model on the IBM PC to trace the value of a pointer, as shown in the example.
func2() (lines 9-23) illustrates how to use the debugger to debug a function. The first debug macro, TS() (line 15), causes an entry to be placed in the trace table announcing that the function has been entered. The second macro, TI() (line 19), causes the value of the integer variable "small_number" to be entered into the trace table each time the for loop iterates.
The third macro, TL() (line 20), causes the value of the long integer "big_number" to be entered in the trace table each time the for loop iterates. The final macro, TS() (line 22), causes an entry to be placed in the trace table announcing that the function is being exited.
On the IBM PC, you can use the hardware print screen function to print off the
current debug screen for further study. In addition, you could modify the debugger to write the trace to a file that you could then print and review at the end of your test.
Figure 1 is an example of the full-screen trace table output displayed by the debugger. It is a print of the debug statements executed by the sample driver program. The left column shows the free-form text you can associate with each debug statement. The right column shows the values of the variables at the time the trace was executed.
Figure 1
Trace table. ..Press any key to return. . .
Press F10 to suppress trace
enter func1 -----------------------------
Character test A
Integer test 123
Long test 456789
Unsigned test 16875
Float test 12.978000
Double test 123456.789012
String test hello, world
exit func1 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
enter func:2 -----------------------------
small__number: 0
big_number: 0
small_number: 1
big__number: 10
small_number: 2
big_number: 30
small_number: 3
big_number: 60
small__number: 4
big number: 100
exit func:2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The debugging technique presented in this article can also be used in other languages. In fact, this debugger was first written in Pascal. It is a fast, flexible time-saver. Once you start to use it you will look back on those dreary nights without the debugger, and you, too, will quote, "Nevermore! "
Thomas Webb is the founder of Micro research, a data processing consulting firm located in Fountain Valley, Calif.
Artwork: Kyle Houbolt
This is the CoCo OS-9 "C" header file only.