2022 September 6,

CS 311: Debugging Ideas

Graphics programs written in C can be hard to debug. Sometimes they show useful information, but sometimes they just show a blank screen, and sometimes they just crash. Here are a few remarks about debugging, in increasing order of sophistication.

First, What is a Seg Fault?

A segmentation fault or seg fault occurs when your program attempts to access memory that is forbidden to it. The operating system detects the illegal memory access and shuts down your program. But why does it happen? In my experience, there are two main causes.

The first cause is array overruns. Suppose that you declare an array with 100 elements, and then you accidentally try to access the 200th element. The C compiler does very few checks for such errors. Even if it did more checks, it is not possible to detect all such errors at compile time. Anyway, if the array overrun causes your program to leave its allocated memory, then you get a seg fault. (If the array overrun does not cause your program to leave its allocated memory, then you don't get a seg fault, but your program probably doesn't work correctly.)

The second cause is dereferencing a bad pointer. For example, if you declare a pointer variable but forget to initialize it, then its value is garbage, and dereferencing the pointer might cause you to leave allocated memory. Also, many functions that return a pointer use the convention that NULL (the address 0) is returned when an error occurs. If you fail to check for null pointers, then you might accidentally attempt to access memory location 0, which is always invalid.

Printing

The simplest debugging tactic is adding print statements to your code. To do this well, define a function as follows, near the top of your main.c.

void debug(const char *message) {
    fprintf(stderr, "%s", message);
    fflush(stderr);
}

If you're debugging a program that crashes, then sprinkle statements like debug("made it to here") throughout your code, and see which ones produce output before the crash. By iterating this process, you can pinpoint the exact line of code that causes the crash.

The key trick here is fflush. It forces the print statement to finish before your program moves on to the next line of code. Without it, your program might crash after the print statement but before the message is actually printed, giving you the wrong idea about where the crash happens.

Even if you're not interested in crashes, you might edit debug to have it print the values of variables that you're interested in. Here's an example:

void debugInt(int integer, const char *message) {
    fprintf(stderr, "%d %s", integer, message);
    fflush(stderr);
}

Testing Your Code Against Mine

When you're debugging a function, wouldn't it be great if you could run your version of the function and my version of the function, and compare the answers? Well, you can! Somewhat.

Here is my version of the first project of the course: triangle rasterization in software. I don't give you the source code. I give you a compiled version that runs on macOS on Intel processors. Most of the CS department lab computers are macOS on Intel, so you can run my code on them, if you can't run it on your own computer. The 363mainDebugging.c file gives you three examples of how to debug your code against mine.

Some of the functions in our graphics engine evolve over time, so that they lose backward compatibility (for example, meshRender). Also, some of the functions produce graphics that are not easily compared to your graphics (for example, meshRender). However, the functions in vector.c, matrix.c, isometry.c, and camera.c tend to be well-isolated and backward-compatible. So you should be able to test those, at least.

C Debugger

A debugger is a program that allows you to inspect your program in memory as it is running. In a sense, it's the print-out-variable-values idea, which I explained above, made more powerful and convenient. See my Debugger Tutorial.