| Home | Programming | Personal | Email me | ||||
|
Table of Contents1. How to Start 1.1 Thinking about Programming 1.2 Structured Programming 2. Compiling Programs 2.1 Compiler Errors 2.2 Linker Errors 3. Runtime Errors 4. Debugging Tools 4.1 The assert Macro4.2 Print Statements 4.3 Debuggers 4.4 Lint 4.5 Walk through 5. General Tips 5.1 Finding Bugs 5.2 Determining the Causes of Bugs
This document explains how to write computer programs that work and that are understandable to other intelligent beings! This two attributes are not independent! In general, programs that other programmers can not understand do not work very well. (Not to mention the fact that they are maintenance nightmares!) Writing structured programs (structured code and data!) helps greatly in debugging the code. Here is a quick review of some of the features of a structured program.
1. How to StartThe most common types of mistakes when programming are:
Let's take these in order 1.1 Thinking about ProgrammingWhen a real programmer (or programming team) is given a problem to solve, they do not immediately sit down at a terminal and start typing in code! They first design the program by thinking about the numerous ways the problem's solution may be found. One of the biggest myths of programming is that: The sooner I start coding the sooner a working program will be produced. This is NOT true!! A program that is planned before coding will become a working program before an unplanned program. Yes, an unplanned program will be typed in and maybe compiled faster, but these are just the first steps of creating a working program! Next comes the debugging stage. This is where the benefits of a planned program will appear. In the vast majority of the time, a planned program will have less bugs than an unplanned program. In addition, planned programs are generally more structured than unplanned programs. Thus, finding the bugs will be easier in the planned program.
So how does one design a program before coding? There are several
different techniques. One of the most common is called top-down
design. Here an outline of the program is first created. (Essentially,
one first looks at the general form of the Top-down design divides the program into sub-tasks. Each sub-task is a smaller problem that must be solved. Problems are solved by using an algorithm. Functions (and data) in the program implement the algorithm. Before writing the actual code, take a very small problem and trace by hand how the your chosen algorithm would solve it. This serves several purposes:
Only when you are confident that you understand how the entire program will look should you start typing in code. 1.2 Structured ProgrammingWhen a program is structured, it is divided into sub-units that may be tested separately. This is a great advantage when debugging! Code in a sub-unit may be debugged separately from the rest of the program code. I find it useful to debug each sub-unit as it is written. If no debugging is performed until the entire program is written, debugging is much harder. The entire source of the program must be searched for bugs. If sub-units are debugged separately, the source code that must be searched for bugs is much smaller! Storing the sub-units of a program into separate source files can make it easier to debug them separately. The ADT and OOP paradigms also divide programs into sub-units. Often with these methods, the sub-units are even more independent than with normal structured code. This has two main advantages:
Sub-units are generally debugged separately by writing a small driver program. Driver programs set up data for the sub-task that the sub-unit is supposed to solve, calls the sub-unit to perform his sub-task, and then displays the results so that they can be checked. Of course, all the following debugging methods can be used to debug a sub-unit, just as they can be used to debug the entire program. Again, the advantage of sub-units are that they are only part of the program and so are easier to debug than the entire program at once! 2. Compiling Programs
The first step after typing in a program (or just a
sub-unit of a program) is to compile the program.
The compiling process converts the source code file you typed in into machine
language and is stored in an object file. This is known as
compiling. With most systems, the object file is automatically
linked with the system libraries. These libraries contain the code
for the functions that are part of the languages library. (For example, the
C libraries contain the code for the 2.1 Compiler ErrorsEvery language has syntax rules. These rules determine which statements are legal in the language and which are not. Compiler programs are designed to enforce these rules. When a rule is broken, the compiler prints an error message and an object file is not created. Most compilers will continue scanning the source file after an error and report other errors it finds. However, once an error has been found, the compiler made be confused by later perfectly legal statements and report them as errors. This brings us to the first rule of compiler errors:
Succeeding errors may disappear when the first error is removed. So, if later error messages are puzzling, ignore them. Fix the errors that you are sure are errors and re-compile. The puzzling errors may magically disappear when the other true errors are removed.
If you have many errors, the first error may scroll off the screen. One
solution to this problem is to save the errors into a file using
redirection.
One problem is that errors are written to
The compiler is just a program that other humans created. Often the error messages it displays are confusing (or just plain wrong!). Do not assume that the line that an error message refers to is always the line where the true error actually resides. The compiler scans source files from the top sequentially to the bottom. Sometimes an error is not detected by the compiler until many lines below where the actual error is. Often the compiler is too stupid to realize this and refers to the line in the source file where it realized something is wrong. The true error is earlier in the code. This brings us to the second rule of compiler errors:
In C (and C++), do not forget that the A useful technique for finding the cause of puzzling compiler errors is to delete (or comment out) preceding sections of code until the error disappears. When the error disappears, the last section removed must have caused the error. The compiler can also display warnings. A warning is not a syntax error; however, it may be a logical error in the program. It marks a statement in your program that is legal, but is suspicious. You should treat warnings as errors unless you understand why the warning was generated. Often compilers can be set to different warning levels. It is to your advantage to set this level as high as possible, to have the compiler give as many warnings as possible. Look at these warnings very carefully!. Remember that just because a program compiles with no errors or warnings does not mean that the program is correct! It only means that every line of the program is syntactically correct. That is, the compiler understands what each statement says to do. The program may still have many logical errors! An English paper may be grammatically correct (i.e., have nouns, verbs, etc. in the correct places), but be gibberish. 2.2 Linker ErrorsThe linker is a program that links object files (which contain the compiled machine code from a single source file) and libraries (which are files that are collections of object files) together to create an executable program. The linker matches up functions and global variables used in object files to their definitions in other object files. The linker uses the name (often the term symbol is used) of the function or global variable to perform the match. The most common type of linker error is an unresolved symbol or name. This error occurs when a function or global variable is used, but the linker can not find a match for the name. For example, on an IBM AIX system, the error message looks like this:
0706-317 ERROR: Unresolved or undefined symbols detected:
Symbols in error (followed by references) are
dumped to the load map.
The -bloadmap:<filename> option will create a load map.
.fun
This message means that a function (or global variable) named
There are also bugs related to the linker. One difficult bug to uncover occurs when there are two definitions of a function or global variable. The linker will pick the first definition it finds and ignores the other. Some linkers will display a warning message when this occurs (The AIX linker does not!) Another bug related to linking occurs when a function is called with the wrong arguments. The linker only looks at the name of the function when matching. It does no argument checking. Here's an example: File: x.c
File: y.c
These types of bugs can be prevented by using prototypes. For example, if the prototype:
is added to y.c the compiler will catch this error. Actually, the best idea is to put the prototype in a header file and include it in both x.c and y.c. Why use a header file? So that there is only one instance of the prototype that all files use. If a separate instance is typed into each source file, there is no guarantee that each instance is the same. If there is only one instance, it can not be inconsistent with itself! Why include it in x.c (the file the function is defined in)? So that the compiler can check the prototype and ensure that it is consistent with the function's definition. (Additional note: C++ uses a technique called name mangling to catch these type of errors.) 3. Runtime ErrorsA runtime error occurs when the program is running and usually results in the program aborting. On a UNIX/Linux system, an aborting program creates a coredump. A coredump is a binary file named core that contains information about the state of program when it aborted. Debuggers like gdb and dbx can read this file and tell you useful information about what the program was doing when it aborted. There are several types of runtime errors:
4. Debugging ToolsMany methods of debugging a program compare the program's behavior with the correct behavior in great detail. Usually the normal output of the program does not show the detail needed. Debugging tools allow you to examine the behavior of the in more detail. 4.1 The
| ||||||