Debugging Techniques

Aims

To enable students to:

  1. recognise common errors

  2. develop techniques to find and eliminate bugs

  3. practice error detection and correction

Recognise common errors

Invariably programs will contain errors. Some source estimate the rate being one for every ten lines of code - in a project running to hundreds of thousands of lines of code this represents a serious issue.

Errors take many forms. The hardest to resolve are those generated in the design phase. Compilers can not pick these up, and frequently neither will developers. These are usually the realm of product architects who have knowledge of how all the modules making up a complex program interact. Such problems can include failure to meet the specified requirements, missing invocations of parts of a program, etc.

Poor source code layout can produce undesirable behaviour at run time. In code that is not indented it is frequently difficult to get the start and end of loops to match up, resulting in the wrong portions of code being repeated. Even code that has indents can present similar difficulties if they are not used in a consistent fashion.

Many compilers produce different code depending on the mode they are operating in. When developing applications, compilers will frequently produce what is termed 'debug' programs that contains extra information required by debuggers to allow portions of the code to be stepped through a line at a time within the development environment. In such situations compilers will frequently initialise variables with sensible values, provide padding around arrays to catch memory overruns, etc. This behaviour can lull developers into a false sense of security as these extras will be removed when the program is compiled in 'release' mode. For example, the compiler may set integer variables to zero which will allow the following snippet of code to behave as expected:

int iBig = 100;
int iSmall;

while iSmall < iBig
{
    iSmall++;
}

When compiled in release mode the initialisation of iSmall will not take place, instead the variable will contain whatever data was previously in that location of memory, it could be a zero or ten thousand.

Likewise, if the compiler provides buffer zones around arrays the following code will execute without adverse in debug mode, but may have undesirable consequences for released code.

char szBuffer [5];

strcpy (szBuffer, "012345");

Compilers which provide buffer zones around arrays do so to enable debug routines to detect when a memory overwrite has occurred, but the onus is on developers to call these check routines - frequently they do not.

Develop techniques to find bugs and eliminate bugs

Several methods should be employed in bug hunting.

First, check the design of the program to be produced matches the requirements laid down, if in doubt verify with the customer.

Always use consistent indentation within a program to ensure loops, logical statements and the such are correctly structured.

Verify all variables that need to be are initialised before use. Truly defensive programming requires all variables to be provided with initial values.

Use any debug functions provided by the compiler to track down memory overwrites - such things are often difficult to visibly spot in the code and may not manifest themselves in the area of the program where the error occurs.

Make full use of the facilities provided by object orientation. Re-use code wherever possible, the less new code in a project the lower the chance of a bug creeping in.

Object orientation also allows small, free standing classes to be developed that can be tested to destruction in isolation. Objects that are known to be bug free can be combined together into a whole with greater confidence than elements which are untested.

If a bug occurs, often the easiest way to find it is via the debugger, a tool designed to allow developers to step through the execution of their programs a line of source code at a time.

Another technique is termed the 'binary chop'. This is used to help narrow down the portion of a program the error occurs in. The process involves a series of iterations, each time the amount of the program executed is chopped in half and a note made of whether the error occurred.

If an object oriented approach is taken to programming, each class may contain debug functions that allow their current state to be recorded. Such facilities allow their state to be recorded throughout the execution of a program, indicating what conditions give rise to an error.

In very large and complicated programs code should be included to allow the flow of execution through a program to be recorded, for example whenever a function is entered and left its name is logged in a file. As in debug functions within classes this permits the state of a program prior to its failure to be determined.

Practice error detection and correction

The following code will display the numbers between 0 and 10, verify this is the case.

#include <iostream.h>

void main ()
{
    int iCnt;

    while (iCnt < 100);{iCnt = iCnt + 1;}

    cout < iCnt;
}

Add debug functions to the stack class produced in the exercises for the lecture on classes that will permit its state to be recorded to a state file. Information logged should include the current stack depth and an indication of the last function executed.

Download