Debugging techniques

In this section a number of debugging techniques from reading manuals to using tools are described.

Using the compiler's features

A good compiler can do a good deal of static analysis of your code: the analysis of those aspects of a piece of code that can be studied without executing that code.

Static analysis can help in detecting a number of basic semantic problems such as type mismatches and dead code.

For gcc (the GNU C compiler) there are a number of options that affect what static analysis gcc does and what results will be shown. There are two types of options:

Warning options

gcc has a great number of warning flags. Most have the form -Wphrase. You should pick ones relevant to you at the start of coding and put them into your Makefile (use the implicit rules, and put them in the CFLAGS variable). Note that -Wall does not switch on all warnings. It enables a set of warnings that gcc's developers consider useful under nearly all circumstances. In addition to -Wall we recommend at least the following warnings when writing new code: -Wshadow -Wpointer-arith -Wcast-qual -Wcast-align -Wstrict-prototype [1] As an example: The following code will result in a warning because the possibility exists that the function returns without returning a value: foo(int a) { if (a > 0) return a; }

Optimisation flags

gcc also supports a number of optimisations. Some of these trigger gcc to do extensive flow analysis of your code, resulting in for example dead code removal. For normal use, we recommend -O2. Do not use higher optimisation levels unless you know what you are doing; the higher levels can contain experimental optimisations which could generate bad code. Also note that on some systems, enabling optimisation makes debugging using a debugger virtually impossible.

For full documentation of these options, see the chapter `GNU CC Command Options' in [GCC].

The RTFM technique

RTFM stands for Read The Fine Manual. Make sure you take the time to find relevant documentation for the task at hand, i.e. the documentation of the tools (not only the compiler, but also make, the preprocessor and the linker), libraries and algorithms you are expected to use, such as [CPP][GCC][MAKE]. Often you do not need to know everything in the documentation, but you do need to be aware what documentation is relevant and what its purpose is. You should at the very least browse through it; hopefully this will give you a feeling of deja-vu where needed, so you know where to look.

In examining documentation, you should distinguish between tutorials and reference documentation.

A tutorial is a document intended to teach, mostly by example. In a tutorial, conveying ideas is often more important than literal truth. For example, this document is a tutorial.

Reference documentation assumes that you are familiar with its topic, and that you are looking for a definite answer to a specific question. Good reference documentation is exhaustive and enables you to find your answer quickly, through meta-information, such as a table of contents, an index (often several ones) and cross-references. Online hypertext is a convenient format for reference documentation.

Make sure that your reference documentation is up to date and accurate. Never mistake a tutorial document for a reference document; tutorials can never (and thus, should never) be used as authoritative documentation.

In the section called Documentation formats in Appendix A, we discuss a number of different documentation formats and how to handle them. Especially the Info documentation and the man-pages are very important.

printf() debugging

printf debugging is our term for a debugging technique we encounter all too often. It consists of ad hoc addition of lots of printf (C) or cerr or cout (C++) statements to track the control flow and data values in the execution of a piece of code.

This technique has strong disadvantages:

If you consider using printf debugging, please check out the use of assertions (see the section called Assertions: defensive programming) and of a debugger (see the section called The debugger); these are often much more effective and time-saving.

There are some circumstances where printf debugging is appropriate. If you want to use it, here are some tips:

Here is a nice way to do it. File debug.h:

#ifndef DEBUG_H
#define DEBUG_H
#include <stdarg.h>

#if defined(NDEBUG) && defined(__GNUC__)
/* gcc's cpp has extensions; it allows for macros with a variable number of
   arguments. We use this extension here to preprocess pmesg away. */
#define pmesg(level, format, args...) ((void)0)
#else
void pmesg(int level, char *format, ...);
/* print a message, if it is considered significant enough.
      Adapted from [K&R2], p. 174 */
#endif

#endif /* DEBUG_H */
        

File debug.c:

#include "debug.h"
#include <stdio.h>

extern int msglevel; /* the higher, the more messages... */

#if defined(NDEBUG) && defined(__GNUC__)
/* Nothing. pmesg has been "defined away" in debug.h already. */
#else
void pmesg(int level, char* format, ...) {
#ifdef NDEBUG
	/* Empty body, so a good compiler will optimise calls
	   to pmesg away */
#else
        va_list args;

        if (level>msglevel)
                return;

        va_start(args, format);
        vfprintf(stderr, format, args);
        va_end(args);
#endif /* NDEBUG */
#endif /* NDEBUG && __GNUC__ */
}
        

Here, msglevel is a global variable which you have to define, that controls how much debugging output is done. You can then use pmesg(100, "Foo is %l\n", foo) to print the value of foo in case msglevel is set to 100 or more.

Note that you can remove all this debugging code from your executable by adding -DNDEBUG to the preprocessor flags: 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 assert.h; see the next section.

Assertions: defensive programming

If you take a careful look at your code, you'll notice that in every part (say, function or loop) you make a lot of assumptions about the other parts.

Say you write your own power function [3] that takes an integer argument e, but implicitly assumes this argument is positive [4] .

assertions are expressions that should evaluate to true at a specific point in your code; well-known examples are pre- and post-conditions for functions. If an assertion fails to evaluate to true, you have found a problem (possibly in the assertion, but more likely in your code). It makes no sense to execute after an assertion failure.

Writing down assertions means making your assumptions explicit. In C and C++, you can #include<assert.h>, and write the expression you want to assert as the argument to assert, e.g. assert(e > 0). See assert(3).

With the assert macro, your program will be aborted as soon as an assertion fails, and you will get a message stating that the assertion expression failed at line l of file f.

assert is a macro; you can remove all assertion checking from your executable by compiling it -DNDEBUG. You should of course do this only when you release your program to users (and even then only when you are convinced there are no more bugs in it, or when execution speed is of primary concern).

ANWB debugging

`ANWB debugging' is based on a simple principle: the best way to learn things is to teach them.

In `ANWB debugging' you find a, preferably innocent and willing, bystander and explain to her how your code works [5] . This forces you to rethink your assumptions, and explain what is really happening; often you find the cause of your problems this way.

Code grinding (code walk through)

A similar technique to ANWB debugging is to print your code, leave your terminal, and go to the cafetaria and do some serious caffeine and sugar intake while reading (and annotating) your code carefully.

Notes

[1]

See the gcc man-pages or info about the meaning of these warnings.

[2]

It is possible to force the buffers to be emptied by using the function flush, or C++'s << endl.

[3]

Bad example. You should use pow(3), which is probably better tested and faster.

[4]

Even if this is documented in comments, we consider it here as implicit, because no tool can determine or check this for you automatically.

[5]

The name derives from the ANWB, the Dutch organisation that helps with car trouble. They maintain a communication system along the highways. In the extreme case you can't find anyone willing, you could consider going to a highway and use them to explain your problems to.