Software Debugging Techniques: P. Adragna
Software Debugging Techniques: P. Adragna
P. Adragna
Queen Mary, University of London
Abstract
This lecture provides an introduction to debugging, a crucial activity in ev-
ery developer’s life. After an elementary discussion of some useful debugging
concepts, the lecture goes on with a detailed review of general debugging tech-
niques, independent of any specific software. The final part of the lecture is
dedicated to analysing problems related to the use of C++ , the main program-
ming language commonly employed in particle physics nowadays.
Introduction
According to a popular definition [1], debugging is a methodical process of finding and reducing the
number of bugs, or defects, in a computer program. However, most people involved in spotting and
removing those defects would define it as an art rather then a method.
All bugs stem from a one basic premise: something thought to be right, was in fact wrong. Due
to this simple principle, truly bizarre bugs can defy logic, making debugging software challenging. The
typical behaviour of many inexperienced programmers is to freeze when unexpected problems arise.
Without a definite process to follow, solving problems seems impossible to them. The most obvious
reaction to such a situation is to make some random changes to the code, hoping that it will start working
again. The issue is simple: the programmers have no idea of how to approach debugging [2].
This lecture is an attempt to review some techniques and tools to assist non-experienced pro-
grammers in debugging. It contains both tips to solve problems and suggestions to prevent bugs from
manifesting themselves. Finding a bug is a process of confirming what is working until something wrong
is found. Therefore, an algorithm good in every situation should not be expected: there is no silver bullet
for debugging. Experience and ingenuity are part of the quest for bugs, but also disciplined usage of
tools.
The importance of a method of finding errors and fixing them during the life-cycle of a software
product cannot be stressed enough. Testing and debugging are fundamental parts of programmer’s ev-
eryday activity but some people still consider it an annoying option. When not carried out properly,
consequences can be dreadful. In 1998, for example, a crew member of the guided-missile cruiser USS
Yorktown mistakenly entered a zero as data value, which resulted in a division by zero. The error cas-
caded and eventually shut down the ship’s propulsion system. The ship was dead in water for several
hours because a program didn’t check for valid input [3].
Bugs can also be very expensive. In 1999, the 125 million dollars Mars Climate Orbiter was
assumed lost by officials at NASA. The failure responsible for loss of the orbiter was attributed to a
failure of NASA’s system engineering process. The process did not specify the system of measurement
to be used on the project. As a result, one of the development teams used Imperial measurement while
the other used the metric system. When parameters from one module were passed to another, during
orbit navigation correction, no conversion was performed, resulting in the loss of the craft [4].
These two famous bugs, as others in history of software [5], should make the reader understand
the importance of finding errors in software: it is not just an unavoidable part in the development cycle
but vital part of every software system’s lifespan.
The lecture starts with a general introduction to debugging containing some useful concepts for
programmers approaching this subject for the first time. It goes on with a detailed review of general
debugging techniques, not bound to any specific kind of software. Since C++ is the main language
71
P. A DRAGNA
commonly employed in particle physics nowadays, the final part is dedicated to analysing problems
related to the use of this programming language.
The lecture contains several examples and source files, written in C++. They can be compiled and
run and provide a starting point for personal experimentation. Although the examples refer very often to
a Unix-like operating system, the underlying concepts and techniques are platform independent.
1. localising a bug,
2. classifying a bug,
3. understanding a bug,
4. repairing a bug.
This program contains a typical endless loop. The main function consists of a loop calling the
function c() as long as the variable x is lower than 100. c() is supposed to increment the variable x,
defined globally. Unfortunately, due to a poor naming convention, a new declaration in the main scope
cause x not to be incremented as expected, producing an endless loop. This example also illustrates the
danger of giving to variables in different scopes the same names: despite language standards, this can be
a big source of troubles.
2
72
S OFTWARE DEBUGGING TECHNIQUES
Noticing a bug implies testing. Testing should be performed with discipline and, when possible,
automatically, for example after each build of the code. In case of a test failure, the programmer must be
able to see what went wrong easily, so tests must be prepared carefully. This lecture will not cover the
basic of testing. Information on automatic software testing can be found in [6], [7]. Learning the basics
of automatic software testing is highly recommended.
– do not confuse observing symptoms with finding the real source of the problem;
– check if similar mistakes (especially wrong assumptions) were made elsewhere in the code;
– verify that just a programming error, and not a more fundamental problem (e.g. an incorrect
algorithm), was found.
3
73
P. A DRAGNA
4
74
S OFTWARE DEBUGGING TECHNIQUES
As far as gcc is concerned, optimisation level is identified by a number. For standard use, an op-
timisation level not higher then 2 is enough, since higher levels could contain experimental optimisation
which could generate bad code.
For more information about the various options supported by gcc, consult gcc manual [8].
5
75
P. A DRAGNA
number o f a r g u m e n t s . We u s e t h i s e x t e n s i o n h e r e t o p r e p r o c e s s
pmesg away . * /
# d e f i n e pmesg ( l e v e l , format , args . . . ) ( ( void ) 0 )
# else
v o i d pmesg ( i n t l e v e l , char * f o r m a t , . . . ) ;
/ * p r i n t a message , i f i t i s c o n s i d e r e d s i g n i f i c a n t enough A d a p t e d
f r o m [ 9 ] , p . 174 * /
# endif
# e n d i f / * DEBUG_H * /
e x t e r n i n t m s g l e v e l ; / * t h e h i g h e r , t h e more m e s s a g e s . . . */
i f ( level >msglevel )
return ;
v a _ s t a r t ( args , format ) ;
v f p r i n t f ( s t d e r r , format , args ) ;
va_end ( a r g s ) ;
# e n d i f / * NDEBUG * /
# e n d i f / * NDEBUG && __GNUC__ * /
}
Here, msglevel is a global variable, which is defined to control how much debugging is outputted.
Then pmesg(100, "Foo is %l\n", foo) can be employed to print the value of foo in case msglevel
is set to 100 or more. Note that all this debugging code from the executable can be removed by adding
-DNDEBUG to the preprocessor flags (sec. 3.2): for gcc, the preprocessor will remove it, and for other
compilers pmesg will have an empty body, so that calls to it can be optimised away by the compiler. This
trick was taken from the file assert.h (sec. 2.5).
2.4 Logging
Logging takes the concept of printing messages, expressed in the previous paragraph, one step further.
Logging is a common aid to debugging. Everyone who has tried at least once to solve some system-
related problems (e.g. at machine start-up) knows how useful a log file can be. Logging means automat-
ically recording information messages or events in order to monitor the status of your program and to
diagnose problems. It is heavily used by daemons and services, exactly because their failure can affect
the correct operation of the whole system. Logging is a real solution to the cout technique. It can even
form the basis of software auditing, that is the evaluation of the product to ascertain its reliability.
A great example of a logging service is the GNU/Linux syslog program, provided by every dis-
tribution. Studying the way syslog works provides a powerful example, not to mention the expertise to
solve problems with kernel, daemons and subsystems (like mail, news and web servers).
6
76
S OFTWARE DEBUGGING TECHNIQUES
– Layouts
– Appenders
– Categories
A Layout class controls the appearance of the output messages. log4cpp provides the user with
some predefined Layout classes, but the programmer may derive his own classes from the basic class
Layout to specify any style of output message wanted.
An Appender class writes the trace message out to some device. The messages have been format-
ted by a Layout object. Again, log4cpp provides the user with some standard classes to post messages to
standard output, a named file or a string buffer. The Appender class works closely with the Layout class,
and once again a personalised Appender class can be derived in order to log to a different channel: for
example a socket, a shared memory buffer or some sort of delayed write device.
A Category class does the actual logging. The two main parts of a Category are its Appenders
and its priority. Priority controls which messages can be logged by a particular class. When a Category
object is created, it begins with a default Appender to standard output and a default priority of none. One
or more Appenders can be added to the list of destinations for logging.
The priority of a Category can be set to
1. NOTSET
2. DEBUG
3. INFO
4. NOTICE
5. WARN
6. ERROR
7. CRIT
8. ALERT
9. FATAL / EMERG
in ascending order of importance level. FATAL and EMERG are two names for the same highest level
of importance. Each message is logged to a Category object. The Category object has a priority level.
The message itself also has a priority level as it wends its way to the log. If the priority of the message
is greater than, or equal to, the priority of the Category, then logging takes place, otherwise the message
is ignored. NOTSET is the lowest and if a Category object is left with a NOTSET priority, it will accept
and log any message.
Messages can be given any of these priorities except NOTSET. Therefore if a Category has been
set to level WARN, then messages with levels DEBUG, INFO and NOTICE will not be logged. Messages
set to WARN, ERROR, CRIT, ALERT, FATAL or EMERG will be logged.
7
77
P. A DRAGNA
As it can be seen, a message can be logged by using the member function log() with a priority.
The message would not be logged if its priority is lower than the priority of the Category. The debug
message is not recorded because the Category priority is set to INFO. Other examples can be found in
the cited paper.
8
78
S OFTWARE DEBUGGING TECHNIQUES
9
79
P. A DRAGNA
In large programs, adding breakpoints for every iteration of a loop is prohibitive. It is not necessary
to step through each one in turn: a technique known as binary split can greatly simplify the debugging
process. The technique consists in placing a breakpoint at the last line of the first half of the code, and
running the code. If the problem does not manifest itself, then the fault is likely to be within the second
half. From here, the procedure is repeated with the region where the problem is supposed to be, reducing
the area under test at each iteration. At the end, the method either leaves you with just one line, or a
sufficiently small routine that can be stepped through. A binary split [13] can limit the search area of a
1000 line program to just 10 steps!
Algorithm implementation errors are reasonably easy to track down with a debugger. Stepping
through, looking for an invalid state or bad data, is enough: the last statement to execute either is itself
wrong or at least points you at the problem. For more information, discussion and examples of how to
use a debugger (and in particular GDB, the GNU debugger), see [14], [15], [16].
3.2 Preprocessing
The C/C++ preprocessor is the program that expands macros, declares dependencies and drives con-
ditional compilation. All the preprocessor operations are performed at textual level. This can make
tracking down missing declarations difficult. It could also lead to semantic problems. If a preprocessing
problem is suspected, the preprocessor should be allowed to expand the file for examination. Since the
output of the preprocessor is just pure source code, debug can be done without any special tool: an editor
is enough!
A real example in the Unix domain is provided by the gcc compiler with the option -E. This option
makes gcc stop after the preprocessing stage without running the compiler. The output is preprocessed
source code, which is sent to the standard output. The output can be redirected to a file.
10
80
S OFTWARE DEBUGGING TECHNIQUES
In the first category there are, for examples, the libraries Memwatch and Electric Fence, while YAMD
and Valgrind falls in the second one. An interesting discussion about these tools is reported in [17]. This
lecture will discuss Electric Fence and Valgrind.
In this program, an array of 60 elements is created, but the program tries to fill it with 100 elements. As
exercise, it can be compiled with gcc -g -lefence -Wall -o memerror memerror.cpp to check
how Electric Fence spots the problem.
11
81
P. A DRAGNA
3.5 Valgrind
Valgrind [18] is a program which controls the execution of another program. The program can thus be
compiled without any special precautions. Valgrind checks every read and write operation on memory,
intercepting all calls to malloc/free new/delete. Valgrind detects problems like the usage of uninitialised
memory, reading from or writing to already freed memory and reading from or writing beyond the
borders of allocated memory blocks.
Valgrind tracks every byte of the memory with nine status bits: one for the accessibility and the
other eight for the content, if valid. As a consequence, Valgrind can detect uninitialised areas and does
not report false errors on bitfield operations. Valgrind can debug almost all dynamically linked ELF x86
executables without any need for modification or recompilation.
Here are some examples of Valgrind usage.
In this first example, an array of 60 elements is created. The program tries to fill it with 100
elements, which obviously ends in writing outside the boundaries of the allocated memory region. If
the program is compiled with the option -g to include debugging symbols, it can be run under valgrind
with the command valgrind --db-attach=yes --error-limit=no ./memerror, getting an output
similar to the following one, which clearly spots the problem in the program.
12
82
S OFTWARE DEBUGGING TECHNIQUES
The error does not cause a crash. The user has to give an argument as an input. If the input value
is not equal to 0.1 or 0.2, the value is not initialised. Unexpected results might come out.
If valgrind is run with valgrind --db-attach=yes --error-limit=no ./memerror the prob-
lem is spotted quite easily, since the first message underlines the use of an unitialised value:
The third and last example shows another typical error: returning a reference to a dynamically
allocated object.
The effect of a debugging session with valgrind is left as an exercise to the reader.
13
83
P. A DRAGNA
3.6.2 Example
Let’s see a simple example of strace usage. This is a complete program; it can be compiled with
g++ -o straceTest strace.cpp and run as it is. As a further exercise and application of the tech-
nique explained in 2.7, it is recommended studying the program carefully before starting playing with it
on a computer, trying to understand what the problem is only by visual examination.
This simple program tries to access a text file with the (hardcoded) extension tmp inside the current
directory. The name of the file to be opened must be given as a command line parameter. The program
attaches the suffix to the name and opens the file. To perform the exercise, a text file with a suitable name
(e.g. list.tmp) has to be create, using the command ls > list.tmp or with an editor of your choice.
Running the program will generate the error message Can’t open input file "list.tmp".
The program cannot find the input file, but that is pretty strange, because listing the content of the
directory will show the file right there. Obviously the program is affected by a bug.
As can be seen in the source code of listing 8, the programmer simply forgot to attach the extension
to the name. In this program there is actually a second, logical, bug. In an attempt to communicate the
failure of file opening, the programmer inserted an error message (note the usage of the unbuffered cerr
stream). But the error message is misleading, because the programmer hardcoded the extension of the
14
84
S OFTWARE DEBUGGING TECHNIQUES
file name in the message. It would have been wiser to print out just the name of the file to be read. In
this way, the problem could have been recognised immediately.
Let us see how to usefully employ strace to find the problem. Start strace with the command line
strace -o strace.out ./straceTest list. The output of strace is written into the file strace.out.
This is more convenient than receiving the output on screen. Scrolling down the list of system calls, the
point where the program attempts to read the file is market by the system call open.
Examining the list of system calls it can be seen immediately that the problem is with the name of
the file, because the parameter of the open function tells us the program is trying to open the file called
list, not list.tmp. but the file is not there, and an error value -1 is returned.
Conclusion
This lecture reviewed a set of useful debugging techniques. Good debugging skills are acquired through
experience, as a general theory of debugging does not exist. Therefore, the final remark for the reader
is to make a lot of experiments by himself, investigating deeply the reasons of program failures. This
way, the reader will gain an invaluable insight not only into all the nuisances software can cause, but
especially into the way it is built and runs: a key skill to catch bugs.
Acknowledgements
I would like to thank very much J. H. M. Dassen and I. G. Sprinkhuizen-Kuyper for letting me use some
of their material on debugging techniques [19]; P. F. Zema for useful technical comments and discussions
on GNU/Linux debugging, E. Castorina for a critical review of the lecture slides and E. Eisenhandler for
revising the manuscript in its pedagogical content. I would also like to thank F. Flückiger for providing
me the opportunity to present this lecture to CERN iCSC.
References
[1] Wikipedia contributors. Debugging [Internet]. Wikipedia, The Free Encyclopedia; 4 June 2007,
20:01 UTC [cited March 2, 2008]. Available from:
http://en.wikipedia.org/w/index.php?title=Debugging&oldid=129632693.
[2] T. Parr, Learn the essential of debugging, IBM developerWorks journal, December 2004.
Available from: http://www.ibm.com/developerworks/web/library/wa-debug.html.
[3] A. M. Hayashi, Rough sailing for smart ships, Scientific American, November 1998, Vol. 279,
Issue 5, p46.
[4] Mars Climate Orbiter Mishap Investigation Board Phase I Report, November 10, 1999.
Available from: ftp://ftp.hq.nasa.gov/pub/pao/reports/1999/MCO\_report.pdf.
[5] Prof. G. Santor’s list of famous computing bugs.
Available from: http://infotech.fanshawec.on.ca/gsantor/Computing/FamousBugs.
htm
[6] B. Jacobsen, Tools and Techniques, Lecture given at CERN School of Computing 2004, Vico
Equense, Italy. Available from: http://csc.web.cern.ch/csc/2004
[7] P. Tonella, Software Evolution and Testing, Lecture given at CERN School of Computing 2004,
Vico Equense, Italy. Available from: http://csc.web.cern.ch/csc/2004
15
85
P. A DRAGNA
[8] R. M. Stallman and the GCC Developer Community, Using the GNU Compiler Collection, The
Free Software Foundation. Available from: http://gcc.gnu.org/onlinedocs
[9] B. Kernighan, D. Ritchie, The C Programming Language, 2nd edition, (Englewood Cliffs, NJ:
Prentice Hall).
[10] M. Budlong, Logging and Tracing in C++ Simplified, Solaris Developer Technical Articles, August
2001. Available from: http://developers.sun.com/solaris/articles/logging.html
[11] J. Smith, Using log4cpp, Jefficus World, June 13 2004. See http://jefficus.usask.ca
[12] S. Goodwin, D. Wilson, Walking Upright, Linux Magazine, February 2003, Vol. 27, Issue 2, p76.
[13] S. Goodwin, The Pleasure Principle, Linux Magazine, June 2003, Vol. 31, Issue 6, p64.
[14] F. Rooms, Some advanced techniques in C under Linux.
Available from: http://telin.ugent.be/~frooms/publications/cleseng.pdf
[15] W. Mauerer, Visual Debugging with ddd, The Linux Gazette, December 2001, Issue 73.
Available from: http://linuxgazette.net/issue73/index.html
[16] R. M. Stallman et al., Debugging with GDB, The Free Software Foundation.
Available from: http://www.gnu.org/software/gdb/documentation/
[17] S. Best, Mastering Linux debugging techniques, IBM developerWorks journal, August 2002.
[18] Valgrind Developers, Valgrind Documentation. Available from: http://valgrind.org
[19] J.H.M. Dassen, I.G. Sprinkhuizen-Kuyper, Debugging C and C++ code in a Unix environment,
Universiteit Leiden, Leiden, 1999.
16
86