Previous Section Table of Contents Next Section

Look for Known Gotchas

If you have split the code into sections with goals and identified the real meaning of each variable, and nothing has jumped out as being incorrect, you can proceed to choosing inputs and walking through the code. First, however, you can quickly scan the code for a few "gotchas" without getting into the nitty-gritty details.

Loop Counters

Loop counters are often used to index into arrays. In languages that have zero-based arrays, notice if the check to exit a loop uses <= in the comparison, as opposed to <. Code such as the following


for (index = 0; index <= MAX_COUNT; index++) {

    j = array[index];

}


might be correct, but the comparison index <= MAX_COUNT is suspicious. Normally, with a zero-based array, it should be index < MAX_COUNT, so the loop would not iterate when index was equal to MAX_COUNT.

As previously mentioned, some loops logically have multiple loop counters, which can be specified in an obvious way:


for (j = 0, k = 0; j < MAX_SIZE; j++, k+= 2) {

    // loop body

}


or partly by hand:


k = 0;

for (j = 0; j < MAX_SIZE; j++) {

    // loop body

    k += 2;

}


or entirely by hand:


j = 0;

k = 0;

while (true) {

    if (j >= MAX_SIZE)

        break;

    // loop body

    j++;

    k+= 2;

}


These three code examples look the same, but the difference is that in the second and third examples, if a continue were added somewhere within the section marked "loop body", it would skip the code that modifies the loop counters. In the second example, j would be updated, but k would not be updated. In the third example, neither j nor k would be updated.

In the third example, k increments by 2 each time through the loop. Normally, this will not in itself be a bug; the normal case is to increment by 1, so if someone has gone to the trouble to increment by 2, he or she probably has a good reason. However, make a mental note that k is incremented in an unusual way.

Be aware of code that modifies the loop counter within the loop. This is usually done for a reason and (hopefully) is accompanied by a comment, but it makes it more difficult for you to think through what really happens during execution of the loop-especially if the modification is only done in certain cases (depending on the contents of the data being looped through):


for (p = 0; p < buffer_size; p++) {

    if (buffer[p] == '\') {

        // it's an escape character, skip to next one

        p++;

    }

    // loop body

}


In this example, no continue statement exists after p++; the main loop body is still executed.

Same Expression on Left- and Right-Hand Side of Assignment

The same variable or expression sometimes appears on the left- and right-hand side of assignment statements that are near each other. This can happen when the variable's value is used to calculate the value of another variable, and then the first variable is marked as empty, deleted, invalid, and so on. Typically, there is a step where the variable is used, and a step where the variable is modified (in the following example, it's cleared):


total += array[m];

array[m] = 0;


In this situation, passing a variable to a function can be the logical equivalent of having it appear on the right-hand side of an assignment statement-it's the step where the variable is used:


dump_contents(current_record);    // use

current_record.valid = -1;        // clear


The bug occurs if the two statements are swapped; the variable is cleared before it is used:


array[m] = 0;

total += array[m];    // already 0!!


Another case where this happens is in code that's used to swap two variables, that has a standard form:


temp = var1;

var1 = var2;

var2 = temp;


It is easy to make a mistake with those lines-either in the ordering or in what variable appears where.

Check Paired Operations

Many operations that do something in a program have a corresponding "undo" operation, which must be properly paired.

One common example is memory allocation, especially temporary memory allocated by a function. All the temporary memory that a function allocates needs to be freed before the function exits, no matter under what condition it exits.

Some languages do not have explicit memory allocation and deallocation, but certain other operations still must be paired up: acquiring and releasing locks, adding to and subtracting from reference counts, and so on. Code such as the following


process_record(record * rec) {

    acquire_lock(rec);

    if (somethingabout(rec)) {

        return 1;

    // rest of code

    release_lock(rec);

    return ret_val;

}


does not always properly pair up a call to acquire_lock(rec) with a call to release_lock(rec). In general, in each place that the first part of a paired operation is done, you must look to ensure that the second part is always done no matter what code path is followed.

Function Calls

Function calls can be difficult to walk through because the code inside the function is not right in front of you. In the best case, you have the code for the function available, but usually, you have to trust the documentation.

A properly written function modifies only the variables that it is supposed to modify. A call to the function can be treated like a single assignment statement, although it's one that can modify multiple variables and do more complicated modifications of arrays and structures.

When you look at code that calls a function, the main thing to check is that the parameters are passed correctly. Most compilers and interpreters catch an argument of the wrong type being passed, but not the wrong argument of the correct type.

One way to pass the wrong argument is when it is an index into an array. Because every element of the array has the same type, you can pass the right type, wrong argument just by botching the index. Because the index is likely to be of a common type (typically, something that can hold an integer value), this is not difficult to do. For example, in code such as the following


call_func (struct_a, pointer_b, array[q]);


it is likely that if struct_a or pointer_b are of the wrong type to pass as parameters to the function, the compiler will complain. But if q is an integer and array[q] was really supposed to be array[r] or array[s], the compiler won't know the difference.

Return Values

Although many functions manipulate structures that are passed in to them, for many others, the return value is what it's all about-the only permanent result of the function's execution. Therefore, all the careful code that has been written and walked through will be for nothing if the function returns an incorrect value.

The most basic mistake is simply returning the wrong variable. For example, returning a temporary pointer instead of the one you want, as shown in the following code:


record * find_largest(record list[]) {

    record * current_record;

    record * largest_record;

    // code to find largest_record

    return current_record;

}


This code probably meant to return largest_record. Because both variables are of the same type, the compiler has no way of knowing that the code is semantically incorrect.

Some functions have multiple return statements. Returning from a function at the point where the result has been found is often easier than having to check if there is still more work to do, as shown in the following:


def is_word(s):

    done = 0

    return_value = 0

    if len(s) == 0:

        return_value = 0;

        done = 1

    if done == 0:

       # some code that might set return_value to 0 or 1

    if done == 0:

        # some more code that might set return_value

    return return_value


It might be cleaner to have the return statement at each point where the return_value was set, instead of using the variable done to avoid the remaining code. So, the first part of the function would look like this:


def is_word(s):

    if len(s) == 0:

        return 0

    # function continues...


If you have multiple return statements, make sure that every path through the code hits one. You don't want code that looks like the following:


def calculate_average(l):

    if len(l) == 0:

        return 0

    # more code

    if count > 0:

        return total/count


The problem with this code is that it might exit the function without hitting a return statement at all. Many languages won't allow this for functions defined as returning a certain type, but in the previous example, written in Python, the function returns the built-in value None, which is presumably not what you want.

Finally, make sure the data being returned is still valid. Do not return a pointer to storage that has already been freed!

Code That Is Similar to an Existing Error

If you find a particular error that looks like it could be repeated somewhere else in the code, search for other locations where the error might have been made. Bugs do repeat themselves; this can be because code is duplicated, or because the original programmer tended to make the same mistake, or because of a misunderstanding about how the code worked (where the programmer was trying to consistently do the right thing, but wound up consistently doing the wrong thing).

For example, if you see the likely error


for (j = 0; k < MAX; k++)


you should probably search for other for loops that fit the same pattern to ensure that the same mistake was not made elsewhere (especially if it looks like sections of code were cut and pasted within the program).

Similarly, if you discover that the code calls a function with the arguments in the incorrect order, you must check other places where the function is called. If a boundary error is discovered in the access to an array, check other places where the array is accessed.

    Previous Section Table of Contents Next Section