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 OS X 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 12-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 12-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 discover the problem quickly. A better strategy may be to take advantage of the tools that Mac OS X 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 OS X; 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 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 12-2). The constructs that generate these warnings are generally easy to fix and easy to avoid.

Table 12-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 12-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 identifies 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 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 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 to find 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 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, as well as a number of graphical debuggers, that are available from 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. Apple's Xcode provides a graphical front end to gdb.

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 stored in the /cores directory and named core.pid. 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, OS X does not save core files automatically. You can change this behavior by editing /etc/hostconfig. Add the line COREDUMPS=YES, remove other lines containing COREDUMPS, and reboot. You can use the ulimit builtin to allow core files to be saved for a single shell session. 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

This section explains how to use the GNU gdb debugger from the command line. Because gdb is the back end of Xcode's graphical debugger, you can also use it directly from Xcode. 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 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.


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 with O2. The O3 setting 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, 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 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 6.1-20040303 (Apple version gdb-384) (Mon Mar 21 00:05:26 GMT 2005) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB.  Type "show warranty" for details. This GDB was configured as "powerpc-apple-darwin". (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 0x2cac: 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: /Users/alex/book/12/tabs < testtabs Breakpoint 1, findstop (col=0xbffffa00) at tabs.c:41 41      retval = (TABSIZE - (*col % TABSIZE)); (gdb) print *col $1 = 3 (gdb) backtrace #0 findstop (col=0xbffffa00) at tabs.c:41 #1 0x00002c0c 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 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 which argument to use with up.)

(gdb) up #1 0x00002c0c 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; thus 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 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 dump -- Dump target code/data to a local file enable display -- Enable some expressions to be displayed when program stops enable mem -- Enable memory region inspect -- Same as "print" command mem -- Define attributes for memory region output -- Like "print" but don't put in value history and don't print newline print -- Print value of expression EXP print-object -- Ask an Objective-C 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 Type "help" followed by command name for full documentation. Command name abbreviations are allowed if unambiguous. (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. The most widely used under Mac OS X is the one built into Xcode. Others include the GNU ddd debugger program (www.gnu.org/software/ddd).




A Practical Guide to UNIX[r] for Mac OS[r] X Users
A Practical Guide to UNIX for Mac OS X Users
ISBN: 0131863339
EAN: 2147483647
Year: 2005
Pages: 234

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