Debugging with the GNU Debugger gdb
When running your programs gives unexpected behavior, one often wants
to probe your program state and behavior more interactively. The GNU
debugger, gdb, powerfully enables us to step inside the
write–compile–run cycle so we can fix errors more quickly.
A Debugging Philosophy
A philosophy of debugging begins with the observation that a "bug" is some program behavior that runs counter to our expectations. (The reading on testing reminds us we must first be clear on what those expectations are.) In any case, expectation-violating behavior is generally caused by our assumptions. In particular, at least one of our assumptions (another word for expectations) is not being met. Thus, it is the programmer's task to first generate a set of potentially relevant assumptions being made about the program, and then to eliminate each of them from suspicion.
This philosophy is analogous to detective work and the scientific process. A detective wrangles a large set of suspects, so as not to miss the likely culprit, and then works to find incriminating clues while eliminating some individuals from further consideration. Similarly, a diligent scientist will concoct many hypothetical theories to explain some behavior and then conduct experiments to eliminate certain theories as inconsistent with observed behavior.
What is GDB?
The GDB manual tells us:
The purpose of a debugger such as GDB is to allow you to see what is going on "inside" another program while it executes—or what another program was doing at the moment it crashed.
GDB can do four main kinds of things (plus other things in support of these) to help you catch bugs in the act:
- Start your program, specifying anything that might affect its behavior.
- Make your program stop on specified conditions.
- Examine what has happened, when your program has stopped.
- Change things in your program, so you can experiment with correcting the effects of one bug and go on to learn about another.
An Example
Many common errors in C programs arise from the use of pointers and addresses. As an instructive case, we simply and subtly modify a program from the reading introducing pointer parameters.
/* Demonstrate incorrect use of pointer (address) parameters to functions to
change variables
*/
#include
void addressAsParameter (double* addressParameter)
{
printf("value of valueParameter at start of addressAsParameter: %lf\n",
*addressParameter);
*addressParameter = 543.21;
printf("value of valueParameter at end of addressAsParameter: %lf\n",
*addressParameter);
}
int main ()
{
double* number;
addressAsParameter (number);
printf ("value of number after addressAsParameter completed: %lf\n",
*number);
}
As it turns out, this code compiles successfully, but when run it produces a segmentation fault.
Launching GDB
While one can run GDB on any executable, the most useful information is garnered from a program compiled with debugging symbols included. That means you get helpful information about the names of your functions and variables, as well as the corresponding source code line numbers during program execution.
To include such debugging symbols, one provides the -g
flag to the gcc compiler, like so:
gcc -g -o address-param address-param.c
Alternately (and more appropriate for this class), you can add
the -g flag to the CFLAGS variable in
your Makefile. For example:
# Set appropriate compiler flags
CFLAGS=-Wall -std=gnu99 -g
Your build process would then be the same, using make.
make address-param
gcc -I/home/walker/Myro/include/MyroC -Wall -std=gnu99 -g -L/home/walker/Myro/lib -lm -lMyroC -lbluetooth -ljpeg -leSpeakPackage -o address-param address-param.c address-param.c address-param.c: In function 'main': address-param.c:21:23: warning: 'number' is used uninitialized in this function [-Wuninitialized]
It is very important to note that adding debugging symbols to your compiled programs is not free. The executable files are bigger (taking more disk), use more more memory when run, and can be noticeably slower. Thus it is good practice not to enable debugging symbols when you are shipping programs to be used in general scenarios long after development.
However, for this class it is perfectly reasonable to build with
debugging symbols as your default. Thus, leaving the -g
flag in your class Makefile is fine.
Now that we can build debuggable programs, we look at two ways to launch the debugger, from the command line and within Emacs.
From Terminal
Invoking the gdb program simply requires you provide the name of the compiled executable (not the source code file):
gdb address-param
GNU gdb (GDB) 7.4.1-debian Copyright (C) 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>... Reading symbols from /tmp/address-param...done. (gdb)
This shows us the GDB program (not address-param) is
running and waiting for us to give it a command. GDB presents the
prompt (gdb).
From Emacs
Because GDB will interact with Emacs to show you where in your program code the debugger is currently operating, many developers prefer to use GDB from within Emacs.
First, you need to be sure you have compiled your executable. Once you have done that, with your source code file as the active buffer, navigate to the Tools menu and select Debugger (GDB)....
The command window will active and present you with prompt that looks
remarkably similar to the command we used directly within terminal,
with the exception of an additional annotate flag that
signals GDB to use additional output for Emacs to utilize:
Run gdb (like this): gdb --annotate=3 address-param
Assuming the default executable name matches, you may simply press Enter to issue the command to launch GDB.
You should now have a new buffer open in the same Emacs window for the GDB session containing the same content we saw above. While it is relatively straightforward to arrange to see both the GDB and your source buffers in the same window, we'll see soon that Emacs will do this for you automatically.
Using GDB for Basic Debugging
There are four basic commands essential to doing the most common bug fixes.Running your program
Now that we have GDB running, we need to tell it to run our
program. This is accomplished with the run
or r (abbreviated) command. (We omit
the (gdb) prompt in the remaining examples.)
run
Starting program: /tmp/address-param address-param Program received signal SIGSEGV, Segmentation fault. 0x000000000040051c in addressAsParameter (addressParameter=0x0) at address-param.c:9
It might not be comforting now, but it's good to know GDB seems to run into the same problem we saw earlier. (As you might imagine, it can be very inconvenient when "Heisenbugs" disappear the moment you try to inspect them.) Let's examine the meaning of this initial output.
The first line simply tells us GDB is running our program. The next line tells us quite a bit more:
- Program received signal SIGSEGV, Segmentation fault.
- A segmentation fault happens when a program accesses an invalid memory address. GDB informs us that the operating system sent the SIGSEGV "signal", which corresponds to this error. The default recourse is to immediately stop the program.
- 0x000000000040051c
- This is an address in memory written in a hexadecimal base, which is more convenient than decimal and more compact than binary. In this case, it represents (unhelpfully in our case), the address of the information about the function containing the offending program instruction. (More on that later.)
- in addressAsParameter (addressParameter=0x0)
-
This more conveniently tells us the particular function being run
when the fault occured,
addressAsparameter. In addition, the values of any function parameters are given. BecauseaddressParameteris a pointer, GDB displays the address in hexadecimal form. Other parameter types, likeintordoubleare printed in their natural, intuitive format. - at address-param.c:9
- Finally, this helpful bit informs us exactly which line of code (in which file) contains the offending expression.
If you launched GDB from within Emacs, you should find that your window is now split between GDB (on the top) and your source code file (on the bottom). Moreover, Emacs displays an arrow next to the offending line of code and has moved your cursor to that line.
Printing values
One of the simplest capabilities of GDB is also the most powerful:
you can interactively probe the values of variables or the results of
certain expressions with a print command,
abbreviated p.
In our example above, there's not much going on in the expression beginning on line 9:
printf("value of valueParameter at start of addressAsParameter: %lf\n",
*addressParameter);
printf takes a format string, which we have given it,
and the type signifier %lf matches the expected type
double pointed to by addressParameter;
therein lies the problem.
What address does addressParameter refer to? The
invocation shown above tells us, but if it did not, we would want a
way to directly inquire of any variables or expressions. We do this
with the print or p command, which takes as
an argument a C expression, evaluating and printing the value of the
result. In our case,
print addressParameter
$1 = (double *) 0x0
This command shows us the type (double*) and value
(0x0), which is hexadecimal for zero or
the NULL pointer) of the address. The special
address NULL cannot be dereferenced by any program in C,
which leads to the segmentation fault.
Why did this happen?
Backtrace
As most program problems occur within functions called from other functions called from yet other functions (and so on). It is often very useful to know the complete chain of calls giving rise to the current context. This chain is called the backtrace or stack trace. Like the stacks of trays in the dining hall, it is called such because every time we enter a new function all the bookeeping information gets stacked on top of the current context. Leading to deeper and deeper (or taller and taller, depending on your perspective) stacks.
The GDB manual explains it this way:
The call stack is divided up into contiguous pieces called stack frames, or frames for short; each frame is the data associated with one call to one function. The frame contains the arguments given to the function, the function's local variables, and the address at which the function is executing.
When your program is started, the stack has only one frame, that of the function
main. This is called the initial frame or the outermost frame. Each time a function is called, a new frame is made. Each time a function returns, the frame for that function invocation is eliminated. If a function is recursive, there can be many frames for the same function. The frame for the function in which execution is actually occurring is called the innermost frame. This is the most recently created of all the stack frames that still exist.Inside your program, stack frames are identified by their addresses. A stack frame consists of many bytes, each of which has its own address; each kind of computer has a convention for choosing one byte whose address serves as the address of the frame. Usually this address is kept in a register called the frame pointer register (see $fp) while execution is going on in that frame.
GDB assigns numbers to all existing stack frames, starting with zero for the innermost frame, one for the frame that called it, and so on upward. These numbers do not really exist in your program; they are assigned by GDB to give you a way of designating stack frames in GDB commands.
GDB allows us to query the whole stack at its current context with
the backtrace or (abbreviated) bt command.
Continuing the example,
backtrace
#0 0x000000000040051c in addressAsParameter (addressParameter=0x0) at address-param.c:9 #1 0x000000000040057d in main () at address-param.c:21
This tells us that the innermost frame
is addressAsParameter as we saw before. Below that, we
see frame 1 was main, which
called addressAsParameter at line 21.
In some circumstances, this location (the call
from main) may be a useful place to go snooping for
answers.
Navigating the stack
The debugger has captured all of the current program state, which means we can probe variables active in other functions on the stack. How do we get there?
A common way to proceed through the stack is by navigating from the
innermost to the outermost frame. Unfortunately, our analogy of
dining hall trays breaks down because to move up is to
move toward the outermost frame, while down is to move
toward the innermost frame.
In our example, we move up one frame (the default argument) to
reach main
up
#1 0x000000000040057d in main () at address-param.c:21
If you're operating GDB within Emacs, you should see that the cursor
of your source code changes (along with the display arrow) to the
line calling the function from whence you just came
(addressAsParameter), which reads
addressAsParameter (number);
We can now use the print command to tell us the value of
the variable number, which we had used as a parameter.
p number
$2 = (double *) 0x0
Not surprisingly (parameters in one context should match their values
in another), we see that the double* pointer is
also NULL or 0x0.
The proximal cause of the bug is dereferencing a NULL
pointer in addressAsParameter The ultimate cause is
failing to initialize number with a sensible value
in main. If we had compiled with all warnings turned on,
and paid heed to them, we likely could have avoided this
issue. However, the next section points out the deeper issues.
Epilogue
Early in the semester it will be common for us to pass addresses into procedures. However, one will more rarely be declaring pointer variables. The proper way to have written this code was given in the reading introducing pointer parameters.
When we declare a pointer variable, we only get space for storing an
address (i.e., a pointer to a double). We
do not get a valid memory address in which to store
a value (i.e., a double). Thus, the proper paradigm now
is to declare the double
double number;
and find its address with the ampersand operator, &.
addressAsParameter (&number);
Summary reference
| GDB Command | Abbreviation | Result | Manual Entry |
|---|---|---|---|
run [args] |
r [args] |
Run program (using command line
arguments [args] if given)
|
Running |
backtrace |
bt |
Print a backtrace of the entire stack | Backtrace |
print expr |
p expr |
Print the evaluation of expr |
Data |
up [n] |
Move n frames up the stack; if no argument, n defaults to 1. | Selection | |
down [n] |
Move n frames down the stack; if no argument, n defaults to 1. | Selection |
In addition to these basic after-the-fact tools, before running your program it can be very useful to set breakpoints, which allow you to suspend execution before the program crashes and step through bit by bit as you investigate. You can even set watchpoints so that the program is suspended when an expression's value changes.
Emacs provides useful graphical versions of these capabilities natively, but the GDB manual also provides useful, direct commands for doing this:
Inside GDB, your program may stop for any of several reasons, such
as a signal, a breakpoint, or reaching a new line after a GDB command such as
step. You may then examine and change variables, set new
breakpoints or remove old ones, and then continue execution. Usually,
the messages shown by GDB provide ample explanation of the status of
your program—but you can also explicitly request this
information at any time.
- Breakpoints: Breakpoints and watchpoints
- Continuing and Stepping: Resuming execution
- Skipping Over Functions and Files
PostScript: A final "note"
Finally, to truly understand the debugging process, you must sing the GDB song.
