Last updated at Wed, 13 Dec 2017 21:42:20 GMT
Who Should Read This?
Have you ever wondered why your code doesn’t work? Do you ever find yourself puzzled by the way someone else’s program works? Are you tired of spending night after tearful night poring over the same lines of code again and again, struggling to maintain your sanity as it slips away? If this sounds like you or someone you know, please seek help: use a debugger.
What Is a Debugger?
For those of you that have never used a debugger:
- I’m so sorry
- Please read on
A debugger is a program that is able to attach to other running processes in order to examine their execution. The method by which it achieves this varies by debugger/operating system, and is beyond the scope of this post.
As with programming languages, text editors (seriously there’s way too many), and hipster clothing styles, the modern developer has many choices when it comes to debuggers. In this post, we will be discussing the GNU Debugger (commonly known as GDB).
Why GDB?
While not quite as old as bugs themselves, GDB has been around for 30 years (though it’s still actively developed), making it one of the oldest debuggers in common use today.
GDB is capable of debugging programs in many languages, however in this post,
we will be focusing on debugging C programs.
As was the way of the world at the time of its creation, GDB is a command line tool. While this might be a turn off for some, I believe it is where much of GDB’s power comes from compared to a graphical debugger.
The goal of this post is to introduce you to GDB’s basic features (the ones you’ll be using most), as well as a few more advanced features that you may not use often, but will make you feel really cool when you do.
Getting Started
The remainder of this post assumes you are using OS X or some flavor of Unix/Linux, as well as a passing familiarity with C.
You will need both GDB and GCC
Note: if you do not have one or both of these programs, there are plenty of resources online which cover installing them. Just google “install gcc
Checking Your Version
GDB
Running
$ gdb -v
will produce a long paragraph of output, but the version number should be apparent near the top. I’m using 7.9.1 for this post, however older versions will likely work.
GCC
Running
$ gcc --version
may also produce a long message, but again, the version number should be easy to pick out. I’m using 5.3.0, though most versions should be fine.
Something to Debug
To try out our debugger, we’re going to need something to debug!
Let’s use this program taken from Wikipedia:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
size_t foo_len (const char *s)
{
return strlen (s);
}
int main (int argc, char *argv[])
{
const char *a = NULL;
printf ("size of a = %d\n", foo_len (a));
exit (0);
}
This program defines a function foo_len
which is essentially a wrapper around the strlen
function from string.h
, then tries to use the function to determine the length of a nonexistent string. I’m sure that’s gonna work out well.
Now let’s get debugging!
The Basics
Compile an Example
First things first, let’s compile the program:
$ gcc -o example -g example.c
-o
specifies the name of the executable to create (example
in this case)-g
tells the compiler to also include debug information that helps GDB do a
better job
That should run without a hitch, since there are no compiler errors in the program (although if we were smart we would have enabled more).
Let’s run the executable with
$ ./example
and see what happens. As expected….
Segmentation fault
On the bright side, at least we have something to debug now!
Debugging It
Since we compiled our code with the -g
flag, we’re all set to debug. To start, run
$ gdb example
at which point a long mess of startup information will be thrown at you. The only lines you really need to pay attention to are the last few, which sometimes tell you if there was an error starting GDB. You should now see a prompt that looks something like
(gdb)
which is where we’ll enter our debugging commands.
Also note that the program is not yet executing; we have merely loaded it into GDB.
To run the program, type run
at the GDB prompt and press enter.
(gdb) run
Aside: GDB is smart enough to know what command you want if you use an abbreviation. In almost all cases, one or two letters is sufficient. For example, you can simply type r
and press enter instead of typing run
.
You might see some extraneous output, but the important stuff is at the bottom. You should see something like this:
Program received signal SIGSEGV, Segmentation fault.
0x00007fff914a4152 in strlen () from /usr/lib/system/libsystem_c.dylib
Obviously, this message isn’t very useful. Notice however that we have not returned to the shell prompt, but rather, we are back at the friendly GDB prompt: although the program has terminated, GDB itself is still running!
This is extremely useful because now we can use the power of GDB to see just what happened in our program.
What Went Wrong?
To start investigating our segfault, let’s run the where
command in GDB.
(gdb) where
You should see something along the lines of
#0 0x00007fff914a4152 in strlen () from /usr/lib/system/libsystem_c.dylib
#1 0x0000000100000f15 in foo_len (s=0x0) at example.c:7
#2 0x0000000100000f47 in main (argc=1, argv=0x7fff5fbff430) at example.c:14
If you haven’t figured it out yet, the where
command prints out the current stack of the executing program. We can see that the call to strlen is at the top (since that was the function in which the segfault occurred), followed by the call to foo_len
. By looking at #1
, we can see that the argument to foo_len
is 0x0
which will definitely cause a segfault when passed to strlen. Looks like we found our bug with just a few simple commands!
Let’s assume for a moment that we didn’t realize that s
was a string and so we didn’t realize that it being 0 would cause problems. We’ll have to poke around a bit more!
Navigating the Stack
When the program is in its frozen state, not only can we see what functions are on the stack, we can also move between them. Use the commands up
and down
to go up and down the stack. If you’re not sure which direction is which, you can always use where to see the order of the stack frames. Going up
while take you down the list displayed by where
. Since we can’t do much in strlen
(no debug information is available for it), let’s look at foo_len
.
(gdb) up
Now we’re in foo_len
‘s stack frame, as indicated by the message displayed when we go up
. GDB also shows us what line in the function we’re on. We can still see that the value of s
is 0, but what if we’ve forgotten that it’s a string? We can determine the type of a variable by using the ptype
(short for print type) command:
(gdb) ptype s
which gives us
type = const char *
If there was any doubt that s
was a string before, it should be clear now. Obviously it’s a problem if s
is NULL
.
More Exciting Stuff
GDB has already proved its usefulness by helping us figure out the source of a simple segfault, but it’s capable of much more. Let’s try something more challenging.
Note: to exit GDB, type exit
or press ctrl-d
. It may prompt you to confirm, in which case just type y
and press enter.
The New Code
Fire up your editor save this code into example2.c
#include <stdio.h>
#define ARR_LEN 10
// calculate some value
int calculate(int x, int y) {
return x + y;
}
int main() {
int i, j, result;
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
result = 0;
for (i = 0; i < ARR_LEN; i++) {
j = i + 1;
result = calculate(arr[i], arr[j]);
printf("%d\n", result);
}
return 0;
}
This program defines an array of the integers 1-10, and is supposed to loop over them and print the sum of each two adjacent elements. To compute the sum of two numbers, it (unnecessarily) calls the calculate function.
This code isn’t a huge step up from the previous example in terms of complexity, but it will allow us to explore several more features of GDB.
Compile and Run
As before, compile the code with
$ gcc -o example2 -g example2.c
and give it a whirl with
$ ./example2
You should see something like
3 5 7 9 11 13 15 17 19 -741187446
Hmmm something seems fishy! Your output may not look exactly the same as mine (since we’ve triggered the dreaded UNDEFINED BEHAVIOR), but the last number should be significantly off.
Note: it’s also possible that running the code resulted in a segfault on your machine. In any case, you should still be able to follow along.
If you looked over the code carefully, you’ll probably be able to the error, but let’s fire up GDB and see what’s really happening.
Debugging It (Again!)
This time around, since we didn’t get a segfault, we can’t use the same trick as before. Instead we’ll have to employ breakpoints.
BreakWhats?
A breakpoint is a debugging concept that represents a point in the execution of a program where the debugger should “pause” the program and allow the programmer to input debugger commands. This is useful because it allows us to examine the state of the program at arbitrary points during its execution, rather than just when the program crashes.
Let’s load the program into GDB and see breakpoints in action.
Setting Breakpoints
To set a breakpoint in GDB, we will use the break
command. break
has several variations, but the two that are most useful are break <function>
and break <filename>:<line number>
. The first allows us to break on all invocations of a particular function. The second allows us to break on an arbitrary line of one of our source files. Let’s use the second one now.
But wait! How are we supposed to remember what line we wanted to break on? Fortunately, GDB can help. We can use the list
command to examine the source file around our current location in it.
(gdb) list
Running list
(or l
) again will print the next 10 lines, and so forth. You can specify a line number after list
to print 10 lines centered on the given line.
It seems likely that the problem is occurring on the line containing the function call, since everything else seems fairly trivial. By using list
we can discover that line 22 is the one we want. To put a breakpoint there, we run:
(gdb) break example2.c:22
GDB will helpfully report that the breakpoint has been set. With our breakpoint set, let’s run the program.
Using Breakpoints
(gdb) run
Soon after we start the program, GDB tells us that we have reached breakpoint 1 and gives us back the prompt.
We can use list
to remind us what code we’re looking at:
(gdb) list
17 result = 0; 18 19 for (i = 0; i < ARR_LEN; i++) { 20 21 j = i + 1; 22 result = calculate(arr[i], arr[j]); 23 printf("%d\n", result); 24 25 } 26
Getting Information
We can guess that the error probably has something to do with one of the variables being set improperly, so let’s use the info locals
command. The info
command accepts many different arguments and is very powerful. With the locals
argument, GDB will display the name and value of all local variables. Let’s try it now:
(gdb) info locals
i = 0
j = 1
result = 0
arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
Everything looks good, as we expected. Let’s move on to the next iteration and see if anything shows up. To do this, we can use the continue
command, which tells GDB to resume the execution of the program until the next breakpoint.
(gdb) continue
Notice that 3 gets printed out (since the first printf
was encountered) and GDB tells us that we’re back at breakpoint 1 on line 22. If we do info locals
again, we’ll see that everything seems normal. If we remember back to our initial run of the program, we didn’t see any out of the ordinary behavior until the last number was printed.
It’d be great if we could skip right to that iteration, and in fact we can! Many GDB commands — including continue
— accept a number as an argument which will modify their behavior. In the case of continue
, the number n
tells GDB to ignore the breakpoint until the n
th time it’s hit. We’ve already hit the breakpoint twice, so we want to skip it 7 times. Therefore, we’ll run
(gdb) continue 8
If we do an info locals
now, we can see that we’re on the final iteration, as we wanted.
We also notice j = 10
. If you recall arr
has 10 elements, so it can only be indexed from 0 to 9, but arr[j]
will try to index it at 10. Looks like we found our bug! We can confirm this using the print
command to print the value of arr[j]
.
(gdb) print arr[j]
$1 = -1068760301
As we suspected, we’re accessing a garbage value of the array. We can fix this by changing the for
loop condition to be
i < ARR_LEN - 1
Alternate Approach
Although our way of counting continue
s in the last section was successful, it was fairly clunky and requires THINKING. Wouldn’t it be great if we could just tell GDB to stop on the last iteration? Turns out we can! All we have to do is use conditional breakpoints. First, let’s use info break
to see what our current breakpoint setup is:
(gdb) info break
Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000100000edb in main at example2.c:22 breakpoint already hit 10 times
Here we can see information about our breakpoints including their number, where they are, and how many times they’ve been hit. For this next section, we’re going to disable this breakpoint so we can use a conditional one instead. To do that, we’ll use the disable
command:
(gdb) disable 1
Now that we’ve told GDB to disable our first breakpoint, we can replace it with a conditional one.
(gdb) break example2.c:22 if i == 9
Note: breakpoint 1 (disabled) also set at pc 0x100000edb.
Breakpoint 2 at 0x100000edb: file example2.c, line 22.
GDB informs us that our first breakpoint is at the same location, but we disabled it already so that’s not a problem. If we run info break
again, we can confirm our success:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000000100000edb in main at example2.c:22
breakpoint already hit 10 times
2 breakpoint keep y 0x0000000100000edb in main at example2.c:22
stop only if i == 9
As indicated by the last line, our second breakpoint will only be triggered when i
is 9 (i.e. on the last iteration of the loop). Also note that GDB reminds us that breakpoint 1 is disabled by displaying an n
under the Enb
column. If we wish to re-enable breakpoint 1 at any time, we can run
(gdb) enable 1
Right now, we just want breakpoint 2 to be active. Let’s use the run
command to restart the program, and see what happens.
(gdb) run
Starting program: /path/to/executable/example2
3
5
----- snip ----
17
19
Breakpoint 2, main () at example2.c:22
22 result = calculate(arr[i], arr[j]);
As you can see, the loop executed 9 times on its own without us having to use the continue
command at all. If we use info locals
now
(gdb) info locals
<code class="block">i = 9
j = 10
result = 19
arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
We can see that i
is 9, which is just what we wanted! Again we realize that this iteration should never occur and that our for
loop condition is incorrect.
Conditional breakpoints are an extremely powerful tool for debugging, since the condition can basically be an arbitrary C expression.
Other Useful Commands
Although not necessary in a program this small, there two other debugging commands you should know. Both of these commands are run when the program is stopped at a breakpoint.
`command` | Effect |
---|---|
`next` | Execute the current line of code and move on to the next one |
`step` | Like `next`, but will *step* in to function calls that are executed |
Wrapping Up
Congratulations, you've just used GDB to debug two misbehaving C programs! Yes they were pretty simple, but most programs can be debugged using just the commands we discussed today. GDB is truly one of the most powerful tools at a developers disposal and believe me when I say it can prevent hours of hair pulling when used properly.
Further Learning
If you'd like to learn more about GDB, you can visit the official documentation here
If you want help with a specific command in GDB, you can search online, or (from within GDB), type help <command name>
to get help on a specific command or apropos <term>
to view all help topics containing that term.