Example 4: Hints on Debugging C Programs

Here are some hints for those of you new to the C programming language. These are not elegant solutions to your problems, these are quick & dirty ways to find out what is going on. Real programmers use symbolic debuggers; they use programs like gdb, dbx, and adb to run their applications. If you get the chance, learn to use gdb; it is a powerful debugger for UNIX systems, but it is not nearly as nice as the windows-based graphical debuggers you have used in the Macintosh and Windows environments. You will find that debuggers are amazing tools, but when you get into real-world programming, you often are trying to debug systems with many different programs that are talking to each other over a network or through shared memory, and you cannot use a normal debugger at all. So, you will often fall back on these ridiculously rudimentary techniques.

Therefore, I am not going to talk about debuggers here; I will talk about simple, quick, easy, incredibly inelegant solutions to many of your problems.

Printf

First off, you cannot trust printf. Generally speaking, by the time the stuff gets printed out on the screen, the program is actually well ahead of the printf. This means that say you have the following code:
	while (something) {
	    printf("top of loop \n");
	    func1();
	    printf("after func1\n");
	    func2();
	    printf("after func2\n");
	}
The program output always ends after "top of loop" and then prints out "Segmentation violation" and dies. This leads you believe that the problem is in func1, but for the life of you, you cannot find anything wrong in func1. The problem is that printf takes its time to print things to the screen, so that it only runs when there is a lot of stuff to print -- it queues up work to do and then prints out a whole bunch of lines at once. Therefore, the problem could very well be in func2, but you were misled by trusting printf. Here is how to fix it so that you can trust printf:
	main()
	{
	    /* variable declarations */

	    setbuf(stdout, NULL);

	    /* first instruction of the program */
	}
Insert the setbuf function call at the beginning of main, immediately after your variable declarations. This tells the print library to turn off buffering for stdout, which is the output stream that printf goes to. You can do this for any FILE * if you want: stdout, stderr, or any file that you fopen.

So now, you CAN trust printf; the last thing printed WILL be the last thing successfully executed.

__LINE__

There are several nice features of the C preprocessor. One of the more useful ones that I have used is the __LINE__ variable (that is two underscore characters on either side of LINE; total of four underscores). This is not a C variable, it is a preprocessor variable. The C preprocessor will go through and replace this variable with the line number on which it occurs before compiling the file. Therefore, suppose you have something like the following at the top of your file:
	#define HI	printf("line %d\n",__LINE__);
You can go through your code and pepper it with "HI" statements so that you know exactly when something happened. Note that this means you have to have turned off buffering on stdout, as described above, or you have to explicitly flush the output, as follows:
	#define HI	{ printf("line %d\n",__LINE__); fflush(stdout); }
The fflush function call tells the printing library to immediately print the contents of the buffer. Once you have this set up, you can place these little "HI" statements throughout the problem-areas of your code and learn exactly where things went wrong:
	main(int argc, char *argv[])
	{
	    /* variable declarations removed */

	    if (argc != 3) {
		printf("error: usage: %s   \n",
		    argv[0]);
		exit(1);
	    }

	    HI

	    inFileString = argv[1];
	    outFileString = argv[2];

	    HI

	    inFilePtr = fopen(inFileString, "r");

	    HI

	    if (inFilePtr == NULL) {
		printf("error in opening %s\n", inFileString);
		exit(1);
	    }

	    HI

	    outFilePtr = fopen(outFileString, "w");

	    HI

	    if (outFilePtr == NULL) {
		printf("error in opening %s\n", outFileString);
		exit(1);
	    }

	    HI
When you run the program, you will see a stream of "line XXX" statements. The last one that prints is the last thing that executed correctly, so this should help you home in on the thing that caused the SEGFAULT.

Pointers

Pointers represent possibly the single largest source of errors in writing C programs. Some of the problems arise because people are a bit unsure about when to use the * operator, other problems arise because people forget that the pointers are uninitialized, or have unexpected values (NULL pointers usually cause the most grief), and then even more problems arise because the pointers reference locations that are no longer valid (for instance, locations on the stack). I will address each of these in turn.

What is the point of the * operator and when should it be used?

The * operator is used in both pointer declarations and in normal expressions. There is a fairly simple way to determine what a pointer is and how to use it; when you declare a pointer, it looks something like the following:

	int *ip;
You can always tell what something is from the way it was declared:
	int *                     ip;           (ip is an int *)
	int                      *ip;           (*ip is an int)
The original declaration says that ip is a pointer to an integer. From then on, when you use the expression
	ip
it is evaluated to mean a pointer. The variable ip contains a number that is interpreted to be the address of a storage location holding an integer value. If you want to see the contents of that storage location, you use the * operator. From the original declaration, if we look at both the * and the ip together, we see that the combination is an int:
	int *ip;
	int                      *ip;           (*ip is an int)
Therefore the expression
	*ip
evaluates to the contents of the storage location that ip points to, i.e. an integer.

Note that this even extends to pointer-pointers:

	char **cpp;

	char **                  cpp            (cpp is a pointer to a char-pointer)
	char *                  *cpp            (*cpp is a char-pointer)
	char                   **cpp            (**cpp is a character)
The expression
	cpp
evaluates to a pointer referencing another pointer. The expression
	*cpp
evaluates to the pointer that cpp references. And the expression
	**cpp
evaluates to the character that *cpp references (or the first character in the string, if appropriate).

When is a pointer initialized properly?

A pointer is initialized properly when it contains the location of what you expected it to. For instance, if you expect the pointer to reference some structure or a string somewhere in memory, then it should contain some large number, and it should definitely NOT contain the value zero. For instance, the following is an error:

	char *cp;
	printf("%s\n", cp);
We are using the pointer cp before we have initialized it to anything. Similarly, the following is an error:
	char *cp;
	strcpy(cp, "some string");
This is an error because cp does not yet refer to a valid storage location. When we attempt to copy bytes into the storage location that cp points to, we send the bytes into limbo. Most likely, cp points to memory address 0 or a random location. Memory address 0 is actually good (cp == NULL) because the program will die as soon as we attempt to copy anything into location 0. If the pointer initially points out into the wild blue yonder (as it probably does if the pointer is an automatic variable--see the Crash Course on C), then it might accidentally point to a memory location that we do have permission to overwrite, in which case we're trampling on good data. This problem is extremely hard to detect; generally, the program simply dies, but it dies much later than spot at which you overwrite good data. In some systems, the problems could show up minutes, hours, or even years later. Or perhaps the program never dies, and the only weirdness is that every third Tuesday, people living in towns that begin with the letter V are charged $100 per minute for their long-distance phone calls.

So when you are programming your RiSC assembler, there are points at which you initialize the pointers label, opcode, arg1, arg2, etc. (or whatever you chose to call them). Every time through the loop, these pointers are re-initialized to have new values. Therefore, every time through the loop--every time you read a new line from the assembly-code file--these point to new places. And some may have been set to the value NULL to indicate that such an item was not present on the assembly-code line (for instance, the line did not have an initial label). You do NOT want to try to print these values out before testing them. The best solution:

	if (label != NULL) {
	    printf("label = %s\n", label);
	} else {
	    printf("label = NULL\n");
	}
	if (opcode != NULL) {
	    printf("opcode = %s\n", opcode);
	} else {
	    printf("opcode = NULL\n");
	}
	if (arg1 != NULL) {
	    printf("arg1 = %s\n", arg1);
	} else {
	    printf("arg1 = NULL\n");
	}
	...
In general--do not attempt to use a pointer (i.e. print it out, copy from it, copy into it) until you know for sure that it is valid.

All of a sudden, the data I'm pointing to changed, and I know I didn't do it.

I have mentioned before that there is a huge difference between statically-defined data objects and automatic variables. In the following example, every variable that begins with an upper-case letter happens to be statically-defined, every variable beginning with a lower-case letter is an automatic variable. Note that the code below contains at least one flaw.

	#include <stdio.h>

	short ProgramCounter;
	char  Labels[100][16];
	short Addresses[100];
	int   NumValidLabels;

	short
	get_label_address(s)
	char *s;
	{
	    int i;

	    for (i=0; i<NumValidLabels; i++) {
		if (strlen(Labels[i]) == 0) {
		    printf("problem: empty label in the Labels array.\n");
		    printf("  either NumValidLabels is wrong, or we \n");
		    printf("  messed up somewhere.\n");
		    return -1;
		}
		if (strcmp(Labels[i], s) == 0) {
		    return Addresses[i];
		}
	    }
	    return -1;
	}

	char *
	read_a_line_of_input(fp)
	FILE *fp;
	{
	    char line[128];
	    static int LineCount=0;

	    if (fgets(line, 128, fp) == NULL) {
		return NULL;
	    } else {
		LineCount++;
		return line;
	    }
	}

	FILE *OutFilePtr;

	main(int argc, char *argv[])
	{
	    FILE *infileptr;
	    char *cp;

	    if (argc != 3) { 
		printf("error: usage: %s <assembly-code file> <machine-code file>\n", argv[0]); 
		exit(1);
	    }

	    infileptr = fopen(argv[1], "r");
	    if (infileptr == NULL) {
		printf("error: can't open file %s", argv[1]);
		perror("fopen");
		exit(1);
	    }

	    OutFilePtr = fopen(argv[2], "w");
	    if (OutFilePtr == NULL) {
		printf("error: can't open file %s", argv[2]);
		perror("fopen");
		exit(1);
	    }

	    while ((cp = read_a_line_of_input(fileptr)) != NULL) {
		/* do stuff */
	    }
	}
The four variables at the top of the program are all global variables, visible to all functions. The OutFilePtr variable just above the main function is also global, but it is only visible to the main function (and any functions that may be below main). Each of the lower-case variables in the various functions are automatic variables and are destroyed as soon as the function exits. Note that the main function is not destroyed until the program terminates, so for all intents and purposes, the automatic variables in main are static because they will never go away -- however, they are only visible inside the main function. Similarly, the LineCount variable in the read_a_line_of_input function is declared as static, and is allocated on the heap; it will never get destroyed until the program terminates, and the variable is only visible to the function read_a_line_of_input.

Here is the flaw. The read_a_line_of_input function has an automatic-storage variable called line that it reads data into (via fgets). Then it returns the address of the data buffer. Since the buffer is allocated on the stack, it will be wiped out. Therefore, you cannot trust data returned from this function. When you point to it, the data might look good at first, but very soon the data will change out from under your feet. (technically speaking, there is a window of opportunity in which the data will be valid, and I can go into that later if anyone is interested)

How do you fix this flaw? Oe solution: make the line variable static by moving it outside of the function in which it is used. Fom that point on, it is a global variable, and it will not go away when the stack is purged. It will also be visible to any other function in the program, but hopefully the program is written so that other functions keep their hands off.

By the way, the get_label_address function works correctly and is not flawed. You will need something similar to this in your assembler.

Hope this helps,
Prof. Jacob