Debugging C Programs

 < Day Day Up > 

The C compiler is liberal about the kinds of constructs it allows in programs. In keeping with the UNIX philosophy that "no news is good news" and that the user knows what is best, gcc, like many other Linux utilities, accepts almost anything that is logically possible according to the definition of the language. Although this approach gives the programmer a great deal of flexibility and control, it can make debugging difficult.

Figure 10-4 on page 409 shows badtabs.c, a flawed version of the tabs.c program discussed earlier. It contains some errors and does not run properly. This section uses this program to illustrate some debugging techniques.

Figure 10-4. The badtabs.c program (The line numbers are not part of the source code; the arrows point to errors in the program.)


In the following example, badtabs.c is compiled and then run with input from the testtabs file. Inspection of the output shows that the TAB character has not been replaced with the proper number of SPACEs:

 $ gcc -o badtabs badtabs.c $ cat testtabs abcTABxyz $ badtabs < testtabs abc   xyz 

One way to debug a C program is to insert print statements at critical points throughout the source code. To learn more about the behavior of badtabs.c when it runs, you can replace the contents of the switch statement with

 case '\t':              /* c is a tab */     fprintf(stderr, "before call to findstop, posn is %d\n", posn);     inc = findstop(&posn);     fprintf(stderr, "after call to findstop, posn is %d\n", posn);     for( ; inc > 0; inc-- )         putchar(' ');     break; case '\n':              /* c is a newline */     fprintf(stderr, "got a newline\n");     putchar(c);     posn = 0;     break; default:                /* c is anything else */     fprintf(stderr, "got another character\n");     putchar(c);     posn++;     break; 

The fprintf statements in this code send their messages to standard error. Thus, if you redirect standard output of this program, it will not be interspersed with the output sent to standard error. The next example demonstrates the operation of this program on the input file testtabs:

 $ gcc -o badtabs badtabs.c $ badtabs < testtabs > testspaces got another character got another character got another character before call to findstop, posn is 3 after call to findstop, posn is 3 got another character got another character got another character got a newline $ cat testspaces abcTABxyz 

The fprintf statements provide additional information about the execution of tabs.c. The value of the variable posn is not incremented in findstop, as it should be. This clue might be enough to lead you to the bug in the program. If not, you might attempt to "corner" the offending code by inserting print statements in findstop.

For simple programs or when you have an idea of what is wrong with a program, adding print statements that trace the execution of the code can often help you solve the problem quickly. A better strategy may be to take advantage of the tools that Linux provides to help you debug programs.

gcc: Compiler Warning Options

The gcc compiler includes many of the features of lint, the classic C program verifier, and then some. (The lint utility is not available under Linux; use splint [secure programming lint; www.splint.org] instead.) The gcc compiler can identify many C program constructs that pose potential problems, even for programs that conform to the syntax rules of the language. For instance, you can request that the compiler report whether a variable is declared but not used, a comment is not properly terminated, or a function returns a type not permitted in older versions of C. Options that enable this stricter compiler behavior all begin with the uppercase letter W (Warning).

Among the W options is a class of warnings that typically result from programmer carelessness or inexperience (see Table 10-2). The constructs that generate these warnings are generally easy to fix and easy to avoid.

Table 10-2. gcc W options

Option

Reports an error when

Wimplicit

A function or parameter is not explicitly declared

Wreturn-type

A function that is not void does not return a value or the type of a function defaults to int

Wunused

A variable is declared but not used

Wcomment

The characters /*, which normally begin a comment, occur within a comment

Wformat

Certain input/output statements contain format specifications that do not match the arguments


The Wall option displays warnings about all the errors listed in Table 10-2, along with other, similar errors.

The program badtabs.c is syntactically correct: It compiles without generating an error. However, if you compile it ( c causes gcc to compile but not to link) with the Wall option, gcc displays several problems. (Warning messages do not stop the program from compiling, whereas error messages do.)

 $ gcc -c -Wall badtabs.c badtabs.c:47: warning: '/*' within comment badtabs.c:11: warning: return-type defaults to 'int' badtabs.c: In function 'main': badtabs.c:34: warning: control reaches end of non-void function badtabs.c: In function 'findstop': badtabs.c:40: warning: unused variable 'colindex' badtabs.c:49: warning: control reaches end of non-void function 

The first warning message references line 47. Inspection of the code for badtabs.c around that line reveals a comment that is not properly terminated. The compiler sees the string /* in the following line as the beginning of a comment:

 /* increment argument (current column position) to next tabstop * / 

However, because the characters * and / at the end of the line are separated by a SPACE, they do not signify the end of the comment to the compiler. Instead the compiler interprets all the statements including the statement that increments the argument through the string */ at the very end of the findstop function as part of the comment.

Compiling with the Wall option can be very helpful when you are debugging a program. After you remove the SPACE between the characters * and /, badtabs produces the correct output.

The next few paragraphs discuss the remaining warning messages. Although most do not cause problems in the execution of badtabs, you can generally improve a program by rewriting those parts of the code that produce such warnings.

Because the definition of the function main does not include an explicit type, the compiler assumes type int, the default. This results in the warning message referencing line 11 in badtabs.c, the top of the function main. An additional warning is given when the compiler encounters the end of the function main (line 34) without seeing a value returned.

If a program runs successfully, by convention it should return a zero value; if no value is returned, the exit code is undefined. Although many C programs do not return a value, this oversight can cause problems when the program is executed. When you add the following statement at the end of the function main in badtabs.c, the warning referencing line 34 disappears:

 return 0; 

Line 40 of badtabs.c contains the definition for the local variable colindex in the function findstop. The warning message referencing that line occurs because the colindex variable is never used. Removing its declaration eliminates the warning message.

The final warning message, referencing line 49, results from the improperly terminated comment discussed earlier. The compiler issues the warning message because it never sees a return statement in findstop. (The compiler ignores commented text.) Because the function findstop returns type int, the compiler expects a return statement before reaching the end of the function. The warning disappears when the comment is properly terminated.

Many other W options are available with the gcc compiler. The ones not covered in the Wall class often deal with portability differences; modifying the code causing these warnings may not be appropriate. The warnings usually result from programs that are written in different C dialects as well as from constructs that may not work well with other (especially older) C compilers. The pedantic-errors option turns warnings into errors, causing a build to fail if it contains items that would generate warnings. To learn more about these and other warning options, refer to the gcc info page.

Symbolic Debugger

Many debuggers are available to tackle problems that evade the simpler debugging methods such as print statements and compiler warning options. These debuggers include gdb, kdbg, xxgdb mxgdb, ddd, and ups, which are available from the Web (refer to Appendix B). All are high-level symbolic debuggers that enable you to analyze the execution of a program in terms of C language statements. The debuggers also provide a lower-level view for analyzing the execution of a program in terms of the machine instructions. Except for gdb, each of these debuggers provides a GUI.

A debugger enables you to monitor and control the execution of a program. You can step through a program line by line while you examine the state of the execution environment.

Core dumps

A debugger also allows you to examine core files. (Core files are named core.) When a serious error occurs during the execution of a program, the operating system can create a core file containing information about the state of the program and the system when the error occurred. This file comprises a dump of the computer's memory (it was previously called core memory hence the term core dump) that was being used by the program. To conserve disk space, your system may not save core files automatically. You can use the ulimit builtin to enable core files to be saved. If you are running bash, the following command allows core files of unlimited size to be saved to disk:

 $ ulimit -c unlimited 

The operating system advises you when it dumps core. You can use a symbolic debugger to read information from the core file to identify the line in the program where the error occurred, to check the values of variables at that point, and so forth. Because core files tend to be large and take up disk space, be sure to remove these files when you no longer need them.

gdb: Symbolic Debugger

The following examples demonstrate the use of the GNU gdb debugger. Other symbolic debuggers offer a different interface but operate in a similar manner. To make full use of a symbolic debugger with a program, you must compile the program with the g option, which causes gcc to generate additional information that the debugger uses. This information includes a symbol table a list of variable names used in the program and their associated values. Without the symbol table information, the debugger cannot display the values and types of variables. If a program is compiled without the g option, gdb cannot identify source code lines by number, as many gdb commands require.

tip: Always use g

It can be helpful always to use the g option even when you are releasing software. Including debugging symbols makes a binary a bit bigger. Debugging symbols do not make a program run more slowly, but they do make it much easier to find problems identified by users.


tip: Avoid using optimization flags with the debugger

Limit the optimization flags to O or O2 when you compile a program for debugging. Because debugging and optimizing inherently have different goals, it may be best to avoid combining the two operations.


The following example uses the g option when creating the executable file tabs from the C program tabs.c, discussed at the beginning of this chapter:

 $ gcc -g tabs.c -o tabs 

tip: Optimization should work

Turning optimization off completely can sometimes eliminate errors. Eliminating errors in this way should not be seen as a permanent solution, however. When optimization is not enabled, the compiler may automatically initialize variables and perform certain other checks for you, resulting in more stable code. Correct code should work correctly when compiled with at least O and almost certainly O2. The O3 setting often includes experimental optimizations so it may not generate correct code in all cases.


Input for tabs is contained in the file testtabs, which consists of a single line:

 $ cat testtabs xyzTABabc 

You cannot specify the input file to tabs when you first call the debugger. Specify the input file once you have called the debugger and started execution with the run command.

To run the debugger on the sample executable, give the name of the executable file on the command line when you run gdb. You will see some introductory statements about gdb, followed by the gdb prompt [(gdb)]. At this point the debugger is ready to accept commands. The list command displays the first ten lines of source code. A subsequent list command displays the next ten lines of source code.

 $ gdb tabs GNU gdb 4.18 ... (gdb) list 4       #include        <stdio.h> 5       #define         TABSIZE         8 6 7       /* prototype for function findstop */ 8       int findstop(int *); 9 10      int main() 11      { 12      int c;          /* character read from stdin */ 13      int posn = 0;   /* column position of character */ (gdb) list 14      int inc;        /* column increment to tab stop */ 15 16      while ((c = getchar()) != EOF) 17              switch(c) 18                      { 19                      case '\t':              /* c is a tab */ 20                              inc = findstop(&posn); 21                              for( ; inc > 0; inc-- ) 22                                      putchar(' '); 23                              break; (gdb) 

One of the most important features of a debugger is its ability to run a program in a controlled environment. You can stop the program from running whenever you want. While it is stopped, you can check the state of an argument or variable. For example, you can give the break command a source code line number, an actual memory address, or a function name as an argument. The following command tells gdb to stop the process whenever the function findstop is called:

 (gdb) break findstop Breakpoint 1 at 0x804849f: file tabs.c, line 41. (gdb) 

The debugger acknowledges the request by displaying the breakpoint number, the hexadecimal memory address of the breakpoint, and the corresponding source code line number (41). The debugger numbers breakpoints in ascending order as you create them, starting with 1.

After setting a breakpoint you can issue a run command to start execution of tabs under the control of the debugger. The run command syntax allows you to use angle brackets to redirect input and output (just as the shells do). In the following example, the testtabs file is specified as input. When the process stops (at the breakpoint), you can use the print command to check the value of *col. The backtrace (or bt) command displays the function stack. The example shows that the currently active function has been assigned the number 0. The function that called findstop (main) has been assigned the number 1:

 (gdb) run < testtabs Starting program: /home/mark/book/10/tabs < testtabs Breakpoint 1, findstop (col=0xbffffc70) at tabs.c:41 41      retval = (TABSIZE - (*col % TABSIZE)); (gdb) print *col $1 = 3 (gdb) backtrace #0  findstop (col=0xbffffc70) at tabs.c:41 #1  0x804843a in main () at tabs.c:20 (gdb) 

You can examine anything in the current scope variables and arguments in the active function as well as globals. In the next example, the request to examine the value of the variable posn at breakpoint 1 results in an error. The error is generated because the variable posn is defined locally in the function main, not in the function findstop:

 (gdb) print posn No symbol "posn" in current context. 

The up command changes the active function to the caller of the currently active function. Because main calls the function findstop, the function main becomes the active function when the up command is given. (The down command does the inverse.) The up command may be given an integer argument specifying the number of levels in the function stack to backtrack, with up 1 having the same meaning as up. (You can use the backtrace command to determine the argument to use with up.)

 (gdb) up 1  0x804843a in main () at tabs.c:20 20                              inc = findstop(&posn); (gdb) print posn $2 = 3 (gdb) print *col No symbol "col" in current context. (gdb) 

The cont (continue) command causes the process to continue running from where it left off. The testtabs file contains only one line; the process finishes executing and the results appear on the screen. The debugger reports the exit code of the program. A cont command given after a program has finished executing reminds you that execution of the program is complete. The debugging session is then ended with a quit command.

 (gdb) cont Continuing. abc     xyz Program exited normally. (gdb) cont The program is not being run. (gdb) quit $ 

The gdb debugger supports many commands that are designed to make debugging easier. Type help at the (gdb) prompt to get a list of the command classes available under gdb:

 (gdb) help List of classes of commands: aliases -- Aliases of other commands breakpoints -- Making program stop at certain points data -- Examining data files -- Specifying and examining files internals -- Maintenance commands obscure -- Obscure features running -- Running the program stack -- Examining the stack status -- Status inquiries support -- Support facilities tracepoints -- Tracing of program execution without stopping the program user-defined -- User-defined commands Type "help" followed by a class name for a list of commands in that class. Type "help" followed by command name for full documentation. Command name abbreviations are allowed if unambiguous. (gdb) 

As explained in the instructions following the list, entering help followed by the name of a command class or command name will display more information. The following lists the commands in the class data:

 (gdb) help data Examining data. List of commands: call -- Call a function in the program delete display -- Cancel some expressions to be displayed when program stops disable display -- Disable some expressions to be displayed when program stops disassemble -- Disassemble a specified section of memory display -- Print value of expression EXP each time the program stops enable display -- Enable some expressions to be displayed when program stops inspect -- Same as "print" command output -- Like "print" but don't put in value history and don't print newline print -- Print value of expression EXP printf -- Printf "printf format string" ptype -- Print definition of type TYPE set -- Evaluate expression EXP and assign result to variable VAR set variable -- Evaluate expression EXP and assign result to variable VAR undisplay -- Cancel some expressions to be displayed when program stops whatis -- Print data type of expression EXP x -- Examine memory: x/FMT ADDRESS Type "help" followed by command name for full documentation. Command name abbreviations are allowed if unambiguous. (gdb) 

The following requests information on the command whatis, which takes a variable name or other expression as an argument:

 (gdb) help whatis Print data type of expression EXP. 

Graphical Symbolic Debuggers

Several graphical interfaces to gdb exist. The xxgdb graphical version of gdb provides a number of windows, including a Source Listing window, a Command window that contains a set of commonly used commands, and a Display window for viewing the values of variables. The left mouse button selects commands from the Command window. You can click the desired line in the Source Listing window to set a breakpoint, and you can select variables by clicking them in the Source Listing window. Selecting a variable and clicking print in the Command window will display the value of the variable in the Display window. You can view lines of source code by scrolling (and resizing) the Source Listing window.

The GNU ddd debugger (www.gnu.org/software/ddd) also provides a GUI to gdb. Unlike xxgdb, ddd can graphically display complex C structures and the links between them. This display makes it easier to see errors in these structures. Otherwise, the ddd interface is very similar to that of xxgdb.

Unlike xxgdb, ups (ups.sourceforge.net) was designed from the ground up to work as a graphical debugger; the graphical interface was not added after the debugger was complete. The resulting interface is simple yet powerful. For example, ups automatically displays the value of a variable when you click it and provides a built-in C interpreter that allows you to attach C code to the program you are debugging. Because this attached code has access to the variables and values in the program, you can use it to perform sophisticated checks, such as following and displaying the links in a complex data structure (page 870).

     < Day Day Up > 


    A Practical Guide to LinuxR Commands, Editors, and Shell Programming
    A Practical Guide to LinuxR Commands, Editors, and Shell Programming
    ISBN: 131478230
    EAN: N/A
    Year: 2005
    Pages: 213

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net