Tools

In this section a number of tools relating to debugging and analysing your programs are described.

The editor

Using an editor suitable for coding can make life easier for you. A good programmer's editor should offer features like

A version management system

Even for small programming jobs, it is useful to archive your source code (including associated makefiles, scripts, documentation etc.) using a version management system like [RCS].

For large programming projects (e.g. GCC, the GNU C compiler, GNOME, the GNU Object Model Environment, and Mozilla) version management is essential. Such projects often use CVS ([CVS]), a version management system with parallel development and internetworking support.

The debugger

A debugger is a program that allows you to peep into a program's execution or to do a post-mortem from a core dump. It can answer questions like

and it can be used to

In order to make effective use of a debugger, you should compile the code you want to debug with debugging information enabled. This will allow the debugger to translate machine addresses and values to a human-readable form.

For the GNU tools, this means you should give the -g or -ggdb options to gcc.

In some environments, you cannot debug optimised code; in others, you can, but it can be confusing. If you cannot use a debugger on optimised code in your environment, do not use optimisation.

gdb is the GNU debugger. It is quite powerful, but unfriendly because of it's terminal interface. If it is available to you, we recommend using a front end for gdb, such as ddd ([DDD]) or xxgdb. Such a front end does not add to the power of the debugger, but makes it much easier to use. Especially, ddd is very useful if you need to visualise a data structure.

Consider the program buggy.cc:

#include <stdio.h>
#include <malloc.h>

typedef struct elt {
	int data;
	struct elt* next;
} elt;

int main(int argc, char* argv[]) {
	int i;
	elt* list = NULL;
	elt* p = NULL;
	for (i = 1; i < argc ; i++) {
		p = (elt *) malloc(sizeof(elt));
		p->data = (int) argv[i][1]; /* Fails with empty argument */
		p->next = list;
		list = p;
	}
	while(list != NULL) {
		p = list;
		list = list->next;
		free(p->next); /* Oops... should be p */
	}
	exit(0);
}
      

Using Electric Fence (see next subsection) it is possible to force a segmentation fault on this code. For plain gdb, here's a short example session:

ultra5 jdassen 16:30 /home/kuyper/debug > g++ -g buggy.cc -lefence
ultra5 jdassen 16:30 /home/kuyper/debug > gdb ./a.out
GNU gdb 4.17
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "sparc-sun-solaris2.5"...
(gdb) run
Starting program: /home/kuyper/debug/./a.out
Program exited normally.
(gdb) set args foo bar baz
(gdb) run
Starting program: /home/kuyper/debug/./a.out foo bar baz
  Electric Fence 2.0.5 Copyright (C) 1987-1995 Bruce Perens.
Program received signal SIGSEGV, Segmentation fault.
0x10d30 in main (argc=4, argv=0xeffff3e4) at buggy.cc:21
list = list->next;
(gdb) where
#0  0x10d30 in main (argc=4, argv=0xeffff3e4) at buggy.cc:21
(gdb) p list
$1 = (elt *) 0xef507ff8
(gdb) p *list
$2 = {data = 97, next = 0xef503ff8}
(gdb) quit
The program is running.  Exit anyway? (y or n) y
      

Memory allocation debugging tools

As discussed earlier in the section called C and C++ specific problems, one of the causes of problems with C and C++ code, is the policy of requiring explicit allocation and deallocation of dynamic storage (malloc(2) and free(2) (C) or new and delete (C++)). As C and C++ rely heavily on pointers, this is a very common cause of problems.

Pointer problems can be quite difficult to detect, as they often show up far away from the cause of a problem. Fortunately, there are a number of tools to help you deal with them; see the section called Memory allocation debugging tools for an extensive list.

We will focus on memory corruption and incorrect pointer use (i.e. those problems that cause core dumps and strange behaviour) rather than on memory leaks. Memory leaks are not a problem in most cases. They waste resources, but often this is only a problem with software that should run continuously (such as X servers and Unix daemons) [1] . The discussion also applies to related problems, such as dealing with out of bounds addressing of arrays.

One memory allocation debugging tool we recommend is Electric Fence by Bruce Perens. It is a library that overrides the default malloc and free and new and delete. By default, it allocates memory at the end of a memory page (which makes it memory hungry). This strongly increases the chances of an OS trap as soon as an attempt is made to address memory not allocated through malloc (e.g. attempting to access memory that has been freed already, or following a pointer into hyper-space). You can use it by simply adding -lefence to your link line. Running the program with ddd (or gdb) and investigating the calls where the segmentation faults occur, helps you to find the memory errors causing code (pointers or array bounds). See efence(3) for documentation. See also the example in the previous section.

Another tool is Checker. The Checker tool uses it's own version of gcc, checkergcc to include boundary checks in your code. It is probably better than Electric Fence, but less easy to use as it requires all libraries you use to be compiled with checkergcc themselves.

System call tracers

A system call tracer is a program that allows you to see what system calls (including parameters and return values) a process makes. It allows you to examine problems at the boundary between your code and the operating system. In a Unix system, programs cannot access the hardware or the network directly; all such access takes place through library calls. Also, most resource allocation (e.g. memory, semaphores etc.) is done through library calls.

You can run a system call tracer on a regular binary. Thus, it can be a useful tool if some proprietary binary-only program, say Netscape, gives an uninformative error message, like `file not found'. You can use a system call tracer to see what files Netscape tries to access, and whether or not it succeeds in accessing them.

Because a system call tracer has no knowledge about the origin of binaries, it cannot tell you where in your code a certain system call is made; you will have to reconstruct this from the trace, or use printf debugging (see the section called printf() debugging).

Under Linux, you can use strace(1). Most system call tracers can be attached to a running process, so you do not have to trace the whole execution of a program.

Profilers

A profiler is a tool that administrates the times of the running program needed for each procedure call. Such a tool is very important when the efficiency of a program has to be improved. In a unix environment the profiler gprof can be used when compiling with gcc or g++ using the option -gp. Running the program will result in a file gmon.out, which will be used by gprof to give information about the time spent in different routines. See the manpages of gprof.

Notes

[1]

Localising a memory leak is quite difficult. We recommend using Purify if you have access to it; we are not aware of a freeware tool that comes close to it for tracking memory leaks. In C++ memory leaks can be prevented by carefully writing and using class destructors: they should free all memory allocated by the class in destruction.