Author : LUPG
Page : 1 Next >>
Table Of Contents:
Why Use A Debugger?
Invoking the "gdb" Debugger
Running A Program Inside The Debugger
Setting Breakpoints
Stepping A Command At A Time
Printing Variables And Expressions
Examining The Function Call Stack
Attaching To an Already Running Process
Debugging A Crashed Program
Getting More Info About Debugging
Why Use A Debugger?
This might sound silly, but I've heard of many programmers that claim they do not need a debugger. They simply don't create bugs. Well, one thing is sure - either they've no idea what they are saying, or they just never put their code to real test. Or maybe they're indeed as gifted as they claim. Unfortunately, most of us tend to have bugs in our code. We could use printing commands to test our code, or we could use a debugger. Many times our code might seem to work correctly, because we didn't test it under enough scenarios. Other times we know there's a bug, but by just reading the code we don't notice it is there. Thus, we should develop a habit of launching a debugger when we get into trouble. It shouldn't come instead of making an effort to write correct code, to add many tests in the code for invalid function arguments, NULL pointers, etc. But when we're in trouble, it's probably our best shot.
The explanations given here are specific to the "gdb" debugger, since there are no real standards regarding the activation and usage of debuggers, but once you know what features to expect from a debugger, it's not too hard to adapt your knowledge to different debuggers.
Invoking the "gdb" Debugger
Before invoking the debugger. make sure you compiled your program (all its modules, as well as during linking) with the "-g" flag. Otherwise, life will be tough. Lets compile the "debug_me.c" program, and then invoke "gdb" to debug it:
gcc -g debug_me.c -o debug_me
gdb debug_me
Note that we run the program from the same directory it was compiled in, otherwise gdb won't find the source file, and thus won't be able to show us where in the code we are at a given point. It is possible to ask gdb to search for extra source files in some directory after launching it, but for now, it's easier to just invoke it from the correct directory.
Running A Program Inside The Debugger
Once we invoked the debugger, we can run the program using the command "run". If the program requires command line parameters (like our debug_me program does), we can supply them to the "run" command of gdb. For example:
run "hello, world" "goodbye, world"
Note that we used quotation marks to denote that "hello, world" is a single parameter, and not to separate parameters (the debugger assumes white-space separates the parameters).
Setting Breakpoints
The problem with just running the code is that it keeps on running until the program exits, which is usually too late. For this, breakpoints are introduced. A break point is a command for the debugger to stop the execution of the program before executing a specific source line.We can set break points using two methods:
Specifying a specific line of code to stop in:
break debug_me.c:9
Will insert a break point right before checking the command line arguments in our program (see the file supplied with this tutorial).
Specifying a function name, to break every time it is being called:
break main
this will set a break point right when starting the program (as the function "main" gets executed automatically on the beginning of any C or C++ program).
Stepping A Command At A Time
So lets see, we've invoked gdb, then typed:
break main
run "hello, world" "goodbye, world"
Then the debugger gave something like the following:
Starting program: /usr/home/choo/work/c-on-unix/debug_me
warning: Unable to find dynamic linker breakpoint function.
warning: GDB will be unable to debug shared library initializers
warning: and track explicitly loaded dynamic code.
Breakpoint 1, main (argc=1, argv=0xbffffba4) at debug_me.c:9
9 if (argc < 2) { /* 2 - 1 for program name (argv[0]) and one for a param. */
(gdb)
Note that you won't always get the warnings i got - it just goes to show you how lousy my system setup is. In any case, these warnings are not relevant to our code, as we do not intend to debug any shared libraries.
Now we want to start running the program slowly, step by step. There are two options for that:
"next" - causes the debugger to execute the current command, and stop again, showing the next command in the code to be executed.
"step" - causes the debugger to execute the current command, and if it is a function call - break at the beginning of that function. This is useful for debugging nested code.
Now is your time to experiment with these options with our debug program, and see how they work. It is also useful to read the debuggers help, using the command "help break" and "help breakpoints" to learn how to set several breakpoints, how to see what breakpoints are set, how to delete breakpoints, and how to apply conditions to breakpoints (i.e. make them stop the program only if a certain expression evaluates to "true" when the breakpoint is reached).
Printing Variables And Expressions
Without being able to examine variables contents during program execution, the whole idea of using a debugger is quite lost. You can print the contents of a variable with a command like this:
print i
And then you'll get a message like:
$1 = 0
which means that "i" contains the number "0".
Note that this requires "i" to be in scope, or you'll get a message such as:
No symbol "i" in current context.
For example, if you break inside the "print_string" function and try to print the value of "i", you'll get this message.
You may also try to print more complex expressions, like "i*2", or "argv[3]", or "argv[argc]", and so on. In fact, you may also use type casts, call functions found in the program, and whatever your sick mind could imagine (well, almost). Again, this is a good time to try this out.
Examining The Function Call Stack
Once we got into a break-point and examined some variables, we might also wish to see "where we are". That is, what function is being executed now, which function called it, and so on. This can be done using the "where" command. At the gdb command prompt, just type "where", and you'll see something like this:
#0 print_string (num=1, string=0xbffffc9a "hello") at debug_me.c:7
#1 0x80484e3 in main (argc=1, argv=0xbffffba4) at debug_me.c:23
This means the currently executing function is "print_string", at file "debug_me.c", line 7. The function that called it is "main". We also see which arguments each function had received. If there were more functions in the call chain, we'd see them listed in order. This list is also called "a stack trace", since it shows us the structure of the execution stack at this point in the program's life.
Just as we can see contents of variables in the current function, we can see contents of variables local to the calling function, or to any other function on the stack. For example, if we want to see the contents of variable "i" in function "main", we can type the following two commands:
frame 1
print i
The "frame" command tells the debugger to switch to the given stack frame ('0' is the frame of the currently executing function). At that stage, any print command invoked will use the context of that stack frame. Of-course, if we issue a "step" or "next" command, the program will continue at the top frame, not at the frame we requested to see. After all, the debugger cannot "undo" all the calls and continue from there.
Attaching To an Already Running Process
It might be that we'll want to debug a program that cannot be launched from the command line. This may be because the program is launched from some system daemon (such as a CGI program on the web), and we are too lazy to make it possible to run it directly from the command line. Or perhaps the program takes very long time to run its initialization code, and starting it with a debugger attached to it will cause this startup time to be much much longer. There are also other reasons, but hopefully you got the point. In order to do that, we will launch the debugger in this way:
gdb debug_me 9561
Here we assume that "debug_me" is the name of the program executed, and that 9561 is the process id (PID) of the process we want to debug.
What happens is that gdb first tries looking for a "core" file named "9561" (we'll see what core files are in the next section), and when it won't find it, it'll assume the supplied number is a process ID, and try to attach to it. If there
Page : 1 Next >>