Debugging Options
Bugs, alas, are as inevitable as death and taxes. To accommodate this inescapable reality, you can use GCC's -g and -ggdb options to insert debugging information into your compiled programs to facilitate debugging sessions. In addition, GCC also has a number of options to make code-profiling sessions easier and more productive.
The -g option can be qualified with a 1, 2, or 3 to specify how much debugging information to generate. The default level is 2 (-g2), which generates extensive symbol tables, line numbers, and information about local and external variables. All of this information is stored inside the binary. Level 3 debugging information includes all of the level 2 information plus all of the macros defined in the source code. Level 1, in contrast, generates just enough information to create backtraces and stack dumps. A backtrace is the history of the function calls that a program makes. A stack dump is a listing, usually in raw hexadecimal format, of the contents of a program's execution environment, primarily the CPU registers and the memory allocated to it. Note that level 1 debugging information does not generate debugging information for local variables or line numbers.
If you intend to use the GNU Debugger, gdb (covered in Chapter 8, "Debugging"), using the -ggdb option creates extra information that eases the debugging chore under gdb. However, it will also likely make the program impossible to debug using other debuggers, such as the DBX debugger common on the Solaris operating system. -ggdb accepts the same level specifications as -g, and they have the same effects on the debugging output.
Using either of the two debug-enabling options will, however, dramatically increase the size of your binary. Simply compiling and linking the simple hello.c program I used earlier in this chapter resulted in a binary of 4089 bytes on my system. The resulting sizes when I compiled it with the -g and -ggdb options may surprise you:
$ gcc -g hello.c -o hello $ ls -l hello -rwxr-xr-x 1 kwall users 10275 May 21 23:27 hello $ gcc -ggdb hello.c -o hello $ ls -l hello -rwxr-xr-x 1 kwall users 8135 May 21 23:28 hello
As you can see, the -g option increased the binary's size nearly three times, while the -ggdb option doubled its size! Despite the size increase, I recommend shipping binaries with standard debugging symbols (created using -g) in them in case someone encounters a problem and wants to try to debug your code for you.
Additional debugging options include the -p and -pg options, which embed profiling information into the binary. This information is useful for tracking down performance bottlenecks in your code and for developing a general picture of a program's performance. -p adds profiling symbols that the prof program can read, and -pg adds symbols that the GNU project's prof incarnation, gprof, can interpret. The -a option counts how many times each block of code (such as functions) is entered.
-save-temps saves the intermediate files, such as the object and assembler files, generated during compilation. These files can be useful if you suspect that the compiler is doing something unusual with your code or if you want to examine the generated code to see if it can be hand-tuned for better performance.
If you are interested in seeing how long the compiler takes to do its work, consider using the -Q option, which causes GCC to display each function as it compiles along with some statistics about how long each compiler pass takes. For example, here is the output when compiling the trusty hello.c program:
$ gcc hello.c main time in parse: 0.020000 time in integration: 0.000000 time in jump: 0.000000 time in cse: 0.000000 time in loop: 0.000000 time in cse2: 0.000000 time in branch-prob: 0.000000 time in flow: 0.000000 time in combine: 0.000000 time in regmove: 0.000000 time in sched: 0.000000 time in local-alloc: 0.000000 time in global-alloc: 0.000000 time in sched2: 0.000000 time in shorten-branch: 0.000000 time in stack-reg: 0.000000 time in final: 0.000000 time in varconst: 0.000000 time in symout: 0.000000 time in dumpt: 0.000000
The displayed times may vary on your system. This information is mostly of interest to compiler writers, but, if you ever get curious about what the compiler is doing, you know how to find out.
Finally, as mentioned at the beginning of this chapter, GCC allows you simultaneously to optimize your code and to insert debugging information. Optimized code presents a debugging challenge, however, because variables you declare and use may not be used in the optimized program, flow control may branch to unexpected places, statements that compute constant values may not execute, and statements inside loops will execute elsewhere because the loop was unrolled. My personal preference, though, is to debug a program thoroughly before worrying about optimization. Your mileage may vary.
Tip - "Optimize later" does not mean "ignore efficiency during the design process." Optimization, in the context of this chapter, refers to the compiler magic I have discussed in this section. Good design and efficient algorithms have a far greater impact on overall performance than any compiler optimization ever will. A highly optimized bubble sort, for example, will never be as fast as a quick sort, except for very small data sets. If you take the time up front to create a clean design and use fast algorithms, you may not need to optimize, although it never hurts to try.