Debugging C Programs


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 any construct 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 27-4 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 27-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 as follows:

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 discover 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 language's syntax rules. For instance, you can request that the compiler report a variable that is declared but not used, a comment that is not properly terminated, or a function that 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 27-2). The constructs that generate these warnings are generally easy to fix and easy to avoid.

Table 27-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 of the errors listed in Table 27-2, along with other, similar errors.

The example 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 identifies several problems. (Warning messages do not stop the program from compiling, whereas error messages do.)

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


The fourth warning message references line 46, column 25. 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 of the statementsincluding the statement that increments the argumentthrough the string */ at the very end of the findstop function as part of the comment. 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. A warning message references this line because the colindex variable is never used. Removing its declaration eliminates the warning message.

The final warning message, referencing line 47, 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 to find a return statement before reaching the end of the function. This 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. These warnings usually result when programs are written in different C dialects or when constructs 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 Debuggers

Many debuggers are available to tackle problems that evade simpler debugging methods such as print statements and compiler warning options. These debuggers include gdb, kdbg, xxgdb mxgdb, ddd, and ups, all of which are available on the Web (refer to Appendix B). Such high-level symbolic debuggers enable you to analyze the execution of a program in terms of C language statements. They 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 (which 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 memoryhence 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 allow 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 whenever it dumps core. You can use a symbolic debugger to read information from a 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

This section explains how to use the GNU gdb debugger. Other symbolic debuggers offer a different interface but operate in a similar manner. To take full advantage 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 tablea 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 is a good idea to use the g option even when you are releasing software. Including debugging symbols makes a binary a bit larger. 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 0 or 02 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.


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 the 0 option and almost certainly with the 02 option. The 03 option often includes experimental optimizations: It may not generate correct code in all cases.


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

$ gcc -g tabs.c -o tabs


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 call the debugger. Instead, you must call the debugger and then specify the input file when you start 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 Red Hat Linux (6.3.0.0-1.114rh) Copyright 2004 Free Software Foundation, Inc. ... (gdb) list 2       /* standard output while maintaining columns */ 3 4       #include        <stdio.h> 5       #define         TABSIZE         8 6 7       /* prototype for function findstop */ 8       int findstop(int *); 9 10      int main() 11      { (gdb) list 12      int c;           /* character read from stdin */ 13      int posn = 0;    /* column position of character */ 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-- ) (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 0x8048454: 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. In this example, 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 Reading symbols from shared object read from target memory...done. Loaded system supplied DSO at 0x28a000 Breakpoint 1, findstop (col=0xbf93ae78) at tabs.c:41 41      retval = (TABSIZE - (*col % TABSIZE)); (gdb) print *col $1 = 3 (gdb) backtrace #0  findstop (col=0xbf93ae78) at tabs.c:41 #1  0x080483ed in main () at tabs.c:20 (gdb)


You can examine anything in the current scopevariables and arguments in the active function as well as global variables. 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 you give the up command. (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 which argument to use with up.)

(gdb) up #1  0x080483ed 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, so 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 facilitate debugging. 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 userdefined  Userdefined 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 even more information. The following listing shows the commands in the class data:

(gdb) help data Examining data. List of commands: append  Append target code/data to a local file call  Call a function in the program delete display  Cancel some expressions to be displayed when program stops delete mem  Delete memory region disable display  Disable some expressions to be displayed when program stops disable mem  Disable memory region disassemble  Disassemble a specified section of memory display  Print value of expression EXP each time the program stops ... print  Print value of expression EXP printobject  Ask an ObjectiveC object to print itself printf  Printf "printf format string" ptype  Print definition of type TYPE restore  Restore the contents of FILE to target memory 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 ... (gdb)


The following command 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. For instance, 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; see Figure 27-5) also provides a GUI to gdb. Unlike xxgdb, ddd can display complex C structures and the links between them in graphical form. This display makes it easier to see errors in these structures. Otherwise, the ddd interface is very similar to that of xxgdb.

Figure 27-5. The ddd debugger


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 1028).




A Practical Guide to Red Hat Linux
A Practical Guide to Red HatВ® LinuxВ®: Fedoraв„ў Core and Red Hat Enterprise Linux (3rd Edition)
ISBN: 0132280272
EAN: 2147483647
Year: 2006
Pages: 383

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