Chapter Outline
Debugging
Types of Errors
Specification Errors
Preparing to Debug
Design Errors
Coding ErrorsA Bugged Program
Debugging with gdb
Code Inspection
Instrumentation
Controlled ExecutionStarting gdb
More Debugging Tools
Running sa Program
Stack Trace
Examining Variables
Listing the Program
Setting Breakpoints
Patching with the Debugger
Learning More about gdbLint: Removing the Fluff from Your Program
Assertions
Function Call Tools
Execution ProfilingProblems with assert
Memory DebuggingElectric Fence
Resources
purify
Checker
SummaryLecture Notes
Debugging
Software systems that misbehave in this way are said to contain bugs..
This chapter looks at software defects and consider some tools and techniques for tracking down specific instances of erroneous behavior.
Type of Error
Bugs usually arise from a small number of basic causes, each of which suggests a method of detection and removal.
Specification Errors
If a program is incorrectly specified, it will inevitably fail to perform as required.
Design Errors
Design errors involve misinterpreting of the specification.
Coding Errors
Of course, everyone makes typing errors. You can also mistranslate the design into code.
Preparing to Debug
Now let's look at computer-based debugging tools and techniques and look at ways of tracking down a very common form of bug, the coding error.
The five stages of debugging are:
![]()
A Bugged Program
Let's look at a sample program that contains bugs.
Unfortunately, the code is not very readable, there are no comments and the original programmer isn't available.
We start from the basic routine debug1.c.
![]()
Let's try to compile the program.
$ cc -o debug1 debug1.c
It compiles OK, with no reported errors or warnings.Before we run the program we add code to print out the results of sorting.
We call the new version debug2.c.
![]()
Now compile again and, this time, run the program.
$ cc -o debug2 debug2.c
The following is what happened on one of the author's system:
$ debug2array[0] = {4, neil}
But on the other machine the output was:
array[1] = {2, john}
array[2] = {3, bill}
array[3] = {1, alex}
array[4] = {-1, (null)}Segmentation fault
You have an out of range array error.If you increase the array size to 4096, as in the debug3.c program, both machines get a segmentation fault.
![]()
$cc -o debug3 debug3.c
$ debug3
Segmentation faultCode Inspection
It is often a good idea to re-read your program when it fails to run as expected.
![]()
There are tools that you can use to help with code reviews, the compiler being an obvious one. It will tell you if you have any syntax errors in your program.
![]()
Instrumentation
Instrumentation is the adding of code to aprogram for the purpose of collecting more information about the behavior of the program as it runs.
You can use the C preprocessor to selectively include instrumentation code.
You just need to recompile the program to include or exclude instrumentation code.
We can do this quite simply with constructs like:
![]()
We can compile the program with the compiler flag -DDEBUG to define the DEBUG symbol and include the extra code, or without to exclude it..
We can make more sophisticated use of a numeric debug macro, like this:
![]()
In this case, we must always define the DEBUG macro.
Alternatively, including the following lines eliminates the need to specify DEBUG on the command line in the case where no debugging is required:
![]()
There are several macros defined by the C preprocessor that can help with debug information.
These are macros that expand to give information about the current compilations.
![]()
Try It Out - Debug Information
Here's a program, cinfo.c, that prints information about its compilation date and time, if debugging is enabled:
![]()
When we compile this program with debug enabled (using -DDEBUG, we see the compilation information:
$ cc -o cinfo -DDEBUG cinfo.c
$ cinfo
Compiled: Feb 4 1996 at 14:01:08
This is line 7 of file cinfo.c
hello world
$How It Works
The C preprocessor part of the compiler keeps a track of the current line and file when it's compiling.
The symbols __LINE__, __FILE__, __DATE__, and __TIME__ are made available.
Debugging without Recompiling
A global variable as a debug flag, allow a -d option at the command line, allows the user to switch debugging on and off without recompiling.
Now you can intersperse the program code with things like this:
![]()
Controlled Execution
We can use a debugger to control the program's execution and view its state as execution proceeds.
Debugging with gdb
We'll use the GNU debugger, gdb, to debug this program.
Starting gdb
Let's recompile our example program for debugging and start gdb.
![]()
gdb has extense online help and the complete manual is available as a set of files which may be viewed with the info program, or from within emacs.
![]()
Running a Program
We can execute the program with the run command.
We pick up the program again at debug4.c, when the first of the memory access problems has been fixed.
![]()
The program runs, incorrectly, as before. gdb shows us the reason and location.
Stack Trace
The program has been halted in the sort function, at line 25 of the source file debug3.c.
We can see how we got to this positon by using the backtrace command:
![]()
You can see that sort was called from main at line 37 of the same file, debug3.c.
Examining Variables
The information printed by gdb when it stopped the program and in the stack trace shows up the values of function arguments.
The sort function was called with a parameter, a, that has the value 0x8001764. This is the address of the array.
The offending line, 25, is an assignment of one array element to another:
25 /* 25 */ a[j] = a[j+1];0
The print command shows us the contents of variables and other expressions.(gdb) print j
j has the value of 4. This means that the program has attempted to execute the statement:
$1 = 4a[4] = a[4+1];
The array that we have passed to sort, array, has only five elements, which will be indexed 0 through 4. So, this statement reads forn the non-existing array[5].We can look at the elements of the passed array by using an expression with print.
(gdb) print a[3]
$2 = {key = 1, data = "alex", '\000'}
(gdb)Listing the Program
We can view the source code of the program from within gdb by using the list command.
![]()
Subsequent uses of list will print out more of the code.
We see on line 22, the loop is set to execute while the variable j is less than n.
In this case, n is 5, so j will have the final value of 4, which is one too far.
A value of 4 causes a comparison of a[4] and a[5] and possibly a swap.
One solution to this particular problem is to correct the loop termination condition to be j < n-1.
Let's make that change, call the new program debug4.c, recompile and try again.
![]()
The program executes, but doesn't do the sort correctly!
Setting Breakpoints
To find out where the program is failing, we need to be able to see what it's doing as it runs.
We can stop the program at any point by setting breakpoints.
There are a number of commands used for setting breakpoints. These are list by gdb with help breakpoint:
![]()
Let's set a breakpoint at line 20 and run the program:
![]()
We can print out the array value and then allow the program to continue with the cont command.
We can have many breakpoints active at any time.
![]()
To print all five elements of array we can use:
![]()
When we allow the program to continue, we see successive alternations to array as execution proceeds:
![]()
We can use the display command to set gdb to automatically display the array whenever the program stops at a breakpoint:
![]()
We can use the commands command to simply display the data and continue to execute the program.
![]()
Now, when we allow the program to continue it runs to completion, printing the value of the array each time around the outer loop:
![]()
gdb reports that the program exits with an unusual exit code. Our program should have provided one with a statement like:
exit(0);
The outer loop doesn't execute as many times as expected. The culpit is the decrement of n on line 30:![]()
The line was a bad attempt tooptimize the program. One fix is to just remove the statement.
Patching with the Debugger
We can 'patch the program using the debugger. It uses a breakpoint with actions, we can try out a fix, called patch before changing the source code.
In this case, we need to break the program on line 30 and increment the variable n.
First we see what breakpoints and displays we have enabled using the info command:
![]()
We can either disable these or delete them entirely. If we disable thme, we retain the option to re-enable them at a later time if we need to:
![]()
The program runs to completion and prints the correct results.
Learning More about gdb
On systems that support hardware breakpoints, you can use gdb to monitor changes to variables in real time.
gdb is able to attach itself to programs that are already running.
More Debugging Tools
Programs like ctags, cxref and cflow work with source files and provide useful information about function calling and location.
Programs like prof and gprof provide information about which functions have been executed and for how long.
Lint: Removing the Fluff from Your Programs
The lint-like tool, lclint can provide useful code review comments.
Here's a sample output from lclint running on an early version of the example program that we debugged earlier:
![]()
It has also detected two real bugs in the following code fragment:
![]()
The lclint tool has determined that the variable s is used on line 20, but hasn't been initialized and the opertor & has been used in place of the more usual &&.
Function Call Tools
Three utilities, ctags, cxref and cflow, form part of the X/Open specification and, therefore, must be present on UNIX branded systems with software development capabilities.
ctags
The ctags program creates an index of functions.
For each function, you get a list of the places it's used, like the index of a book.
![]()
By default, ctags cretes a file, called tags in the current directory, which contains, for each function declard in any of the input source files, lines of the form:
![]()
Alternatively, using the -x option to ctags produces lines of this form on the standard output:
![]()
cxref
The cxref program analyses C source code and produces a cross-reference. It shows where each symbol is mentioned in the program.
Typing
$ cxref *.c *.h
on the author's machine produced the following output.![]()
cflow
The cflow program prints a function call tree, a diagram that shows which function calls which others, and which functions are called by them and so on.
Here's some sample output taken from a version of cflow (cflow-2.0) that is available on the Internet and maintained by Marty Leisner:
![]()
This sample tells us that main calls (among others) show_all_lists and that show_all_lists in turns calls display_list, which itself calls printf.
The -i option produces an inverted flow graph.
![]()
Executing Profiling
You can use execution profiling to track down performanace problems.
prof/gprof
The prof program (and its GNU equivalent, gprof) prints a report from an execution trace file that is produced when a profiled program is run.
A profiled executable is created by specifying the -pg flag to the compiler.
$ cc -pg -o program program.c
The program is linked with a special version of the C library and is changed to include monitoring code.The monitor data is written to a file in the current directory, mon.out (gmon.out for gprof).
![]()
The prof/gprof program reads this monitoring data and produces a report.
![]()
Assertions
Problems occur during the program operation that are related to incorrect assumptions, rather than coding errors.
For example, a function may be written with the understanding that its input parameters will be within a certain range.
If it's passed incorrect data, it might invalidate the whole system!
For these cases, where the internal logic of the system needs to be confirmed, X/Open provides the assert macro which can be used to test that an assumption is correct and halt the program if not.
![]()
The assert macro evaluates the expression and, if it's non-zero, writes some diagnostic information to the standard error and calls abort to end the program.
The header file assert.h defines the macro depending on the definition of NDEBUG.
If NDEBUG is defined when the header file is processed, assert does nothing.
![]()
Problems with assert
You must be careful that there are no side-effects in the assert expression.
Try It Out - assert
Here's a program,assert.c, that defines a function that must take a positive value.
Including the assert.h header file and a 'square root' function which checks that the parameter is positive, we can then write the main function:
![]()
Now, when we run the program, we see an assertion violation when we pass an illegal value:
![]()
How It Works
When we try to call the function, my_sqrt, with a negative number , the assertion fails.
If we recompile the program with -DNDEBUG, the assertion is compiled out and we a math error when we call the sqrt functon form my_sqrt.
![]()
Memory Debugging
If you write outside of an allocated block, you'll very likely corrupt the data structures used by the malloc library to keep track of allocations.
There are some tools available for detecting such problems.
ElectricFence
ElectricFence aims to halt the program at the point of memory corruption.
Test It Out - ElectricFence
Here's a program, efence.c, that allocates a memory block with malloc and then writes beyond the end of the block.
![]()
When we compile and run the program, we see no untoward behavior. But, we have likely corrupted part of memeory.
![]()
Now, we take the same program and link the ElectricFence library, libefence.a, we get an immediate response.
![]()
Running under the debugger pinpoints the problem:
![]()
How it works
ElectricFence replaces malloc and assoicated functions with versions that protect against illegal memory access.
Purify
purify is a commerical product that uses a proprietary object code insertion technique to modify programs and detect memory access problems.
Checker
Checker is a modified compiler back end and C library for Linux that is capable of detecting many of the problems the purify address, such as memory leaks.
Try It Out - Checker
Here's a program, checker.c, that allocates some memory, reads from an unitialized portion of that memory, writes beyond the end of it and then makes it inaccessible.
![]()
To use Checker, we simply have to replace our compile command with checkergcc, a driver program that takes care of invoking the correect compiler version and linking with special, Checkered, libraries.
When we run the program, we see lots of problems diagnosed:
![]()
To check for memory leaks when the program ends, we need to specify -D=end:
![]()
How It Works
We have modified our program by using the checkergcc compiler to include additional code to check each pointer reference as it happens.
Resources
Many of the utilities were taken from the Linux archive ftp://sunsite.unc.edu:/Linux.
Summary
This chapter looked at some debugging tools and techniques.
CS 248 - UNIX Programming Web Site Menu
Information | Syllabus | Schedule | Online "Lectures" | Projects | Quizzes | Web Board
Copyright © 2001 by James L. Fuller, all rights reserved.