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:

  1. I’m so sorry
  2. 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!

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 nth 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 continues 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
These two commands are extremely useful when you want to walk through a program more slowly and see exactly what's happening at each step.

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.