Recitation 2

Why GDB?

Pretty much no one argues against the importance of debugging these days. Rather, the issue is with how people choose to debug - which begs the question why have debuggers at all?

Many of you in CS1 or CS10 may have never bothered to touch the Eclipse debugger, and I don’t blame you - I think it is quite cumbersome and difficult to use. You may be thinking to yourself “I don’t need a debugger - I just look at my code and use print statements.” Interesting. Let’s examine that proposition.

Let’s imagine for a moment that you’re writing a program that takes quite a long time to run - on the order of maybe an hour. If you think that’s ridiculous, just wait until you finish writing your crawler - many of yours will take an hour, and when you put all three parts of your search engine together it almost certainly will take longer than that.

So we have this program that is quite expensive to run in terms of run time. And, like many programs, it has a bug in it. Fantastic. Realizing how precious each run is (lest you waste an hour of your life,) you diligently spend the next 10 minutes inserting every print statement you can think of. And then you run your code.

20 minutes in, the inevitable happens. You realize you forgot to put one print statement. For the sake of argument, assume that you could not have known this before running - rather, only by examining the variables you are already printing could you have deduced the importance of this variable. So what now?

Add the print statement and re-run the bloody thing? That sounds awful!

Some of the stubborn members of this class will persist and argue that their ability to make deductions purely based on stepping through the code is unparalleled. These individuals will say fine, I am willing to struggle through commenting and un-commenting and re-commenting all those print statements. After all, it’s certainly easier than learning something new.

Fine.

What if you have multiple processes running at the same time? Later on in this course, you will write a program that uses threads and processes for parallel programming. In this scenario, threads and processes will interact with each other in various ways in often unpredictable outcomes. So your print statements will produce different results every time, and depending on the outcome, you may want to print different things. What now?

The reason I’ve spent a page of text stressing this simple point is that nearly every time I have tried to teach someone a debugging tool, I nearly always face resistance with the same qualm: “why is this necessary when I have printf?”

I hope you understand why now.

Gnu Project Debugger is absolutely essential for you to learn. In advanced projects such as the ones you will code later this term, printf statements simply will not cut it. GDB is the ultimate tool in being able to freeze your code and examine all relavent variables at a given moment in time. Even better, it allows you to travel up and down the stack to examine each frame and everything it contains.

Learning GDB Part 1: GDB Saves Us

We enter GDB by simply typing gdb on our command line. This brings up the GDB interpreter.

Protip: If you’re ever confused about a command, enter gdb prompt and help [command] will show you the way.

Here’s a sample C program with a small bug in it. I understand that for this toy example a printf statement would suffice, but since we are learning a new tool, it’s best to start with a simple example. The purpose of this code is to sort an array.

0   #include <stdio.h>
1   #include <stdlib.h>
2
3   #define LENGTH (7)
4   int array[] = {34, 2, -1, 6, 10, -8, 30};

5   void swap(int a, int b) {
6     int temp = a;
7     a = b;
8     b = temp;
9   }
10
11  void printArray(int list[]) {
12    printf("array: ");
13    for (int i = 0; i < LENGTH; i++)
14      printf("%d ", list[i]);
15    printf("\n");
16  }
17
18 void sortArray(int list[], int length) {
19   for (int j = 0; j < length; j++) {
20      int min = j;
21      for (int i = j; i < length; i++)
22          if (list[i] < list[min])
23              min = i;
24      swap(j, min);
25   }
26 }
27
28 int main() {
29   printArray(array);
30   sortArray(array, LENGTH);
31   printArray(array);
32   return 0;
33 }

So let’s run this code. Remember that for GDB, you have to compile the code with the GDB flag - so that would be gcc -ggdb bug.c. This will produce the executable a.out, and then we enter GDB with gdb a.out.

Great, now we can run the program. Just type run. The program will run as normal - if it crashes, the crash message will usually be the same in GDB as normal; otherwise, you get the standard outputs.

Here is what I get when I run the code:

array: 34 2 -1 6 10 -8 30
array: 34 2 -1 6 10 -8 30

Hmm.. The array values do not change in the slightest! It’s now time to use GDB to debug this code. We already have a print statement, and it’s never being executed. Hmm. This is where breakpoints come in.

Breakpoints are used to freeze a program at a specific point so that the variables can be examined more thoroughly. You can also change breakpoints, remove old ones, and add new ones, once a gdb session has already started. This gives you the advantage of basically having dynamic print statements - ones that change every single time you run without having to add all those lines of code to your program.

We use break bug.c:main. This handy little trick sets the breakpoint to the main function - which means as soon as GDB enters the main function, it stops and waits for user input. You can also specify line numbers, so we instead could have said break bug.c:29. These would have been the same. Note that I chose 29 instead of 28 - GDB breaks before executing the line number specified.

The GDB command next executes the next step; type this to see the output of the first call to printArray.

(gdb) n
array: 34 2 -1 6 10 -8 30
40      sortArray(array, LENGTH);

Upon calling next, GDB executes the printArray function, displays the output, and then shows the line of code it will execute after this.

Clearly the printArray function is working as intended - so the bug is not here. Instead, it must be in sortArray or swap. Let’s step into the sortArray function to see what’s happening. Use step for this.

(gdb) s
sortArray (list=0x601050 <array>, length=7) at sort.c:28
28      for (int j = 0; j < length; j++) {

GDB shows first the location in the source code that we have stepped into: the sortArray function, which is what we expected. Even better, it shows the first line that it is about to execute - in this case, the for loop.

Do we really want to step through every iteration of the for loop? Of course not. The first order of business is figuring out which function is wrong - not where in the code it is going wrong. So now we have to decide whether the swap function or the sortArray function is wrong. Let’s skip ahead to right before the swap function!

(gdb) b swap
Breakpoint 2 at 0x40058a: file sort.c, line 8.
(gdb) c
Continuing.

Breakpoint 2, swap (a=0, b=5) at sort.c:8
8       int temp = a;

Okay, so we set a breakpoint at the swap function and then continued the execution of the code. GDB dutifully stopped us when it first reached the swap function. Notice that it also gives us the values of the parameters a=0, b=5. How useful! Let’s step through the swap function and see if the values actually change.

(gdb) n
9       a = b;
(gdb) n
10      b = temp;
(gdb) n
11  }
(gdb) print a
$1 = 5
(gdb) print b
$2 = 0

Cool! It looks like the swap function worked. a is now 5 and b is now 0. Awesome! Let’s continue through now that we know the problem is in sortArray.

WAIT! Let’s just double check if that is true. If swap really worked, then the minimum value in array should be in position 0. So let’s verify:

(gdb) print array[0]
$3 = 34
(gdb) print array[1]
$4 = 2

Interesting! Despite the fact that a and b changed values, the values inside array did not change at all! Looks like swap only changes values locally!

Hooray! We successfully debugged a program. Exit GDB with C-D (control-D).

Let’s make the final change to the source code and re-run it.

void realSwap(int array[], int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

The more C way to write this would be the following:

void realSwap(int *a, int *b) {
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

But that will come later once we get to pointers…

Let’s recompile this and re-run it:

array: 34 2 -1 6 10 -8 30
array: -8 -1 2 6 10 30 34

Nice! It works. GDB saves the day!

Learning GDB Part 2: GDB Fails Us

Here is the file bug.c.

0  #include <stdio.h>
1  #define TOTAL_ELEMENTS (sizeof(array) / sizeof(array[0]))
2
3  int array[] = {23, 24, 12, 17, 204, 99, 16};
4
5  int main() {
6      int d;
7      for (d = -1; d <= (TOTAL_ELEMENTS-2); d++)
8          printf("%d\n", array[d+1]);
9      return 0;
10  }

So what does the code do? Well it is supposed to print out every element in the array. But what does it actually do? Upon running this code we see that no output is generated at all!

As usual, let’s do break bug.c:main. Now that we’ve set the break point, let’s run. You should see output similar to the following:

Starting program: /net/tahoe3/chander/a.out

Breakpoint 1, main () at bug.c:8
8       for (d = -1; d <= (TOTAL_ELEMENTS-2); d++)

Interesting. Even though we broke at main, it actually skipped right past the declaration of d and instead stopped right before the for loop.

Let’s see if something is already wrong at this state. print d is self explanatory. I get this output:

(gdb) print d
$1 = 0

Okay, so d is alright. Regardless of what the value was, it gets set to -1 in the beginning of the for loop regardless, so thats okay. What about the contents of the array? print array yields:

(gdb) print array
$3 = {23, 24, 12, 17, 204, 99, 16}

And that’s fine, too. Great. So nothing in the setup went wrong - all the data seems correct. So what could be going wrong? Well, because nothing in the for loop is being printed, we can hypothesize that the program is not entering the for loop at all. The most common reason for that is the condition is not being met.

In otherwords, d <= (TOTAL_ELEMENTS-2) is never true. So let’s examine this part next.

(gdb) print sizeof(array)
$4 = 28
(gdb) print sizeof(array)/sizeof(array[0])
$5 = 7

Okay, so that part is working fine. Now here is a cool little trick. Let’s execute that particular line and see if it works out.

(gdb) print d <= 7
$6 = 1

Remember, in C 1 is true and 0 is false. So d is in fact less than or equal to 7. So how could the loop condition not be satisfied?

Some of you may already know: we’re not quite being fair to the program here. We tested whether d <= 7 and not d <= (TOTAL_ELEMENTS - 2). Okay, nitpickers. Let’s try that.

(gdb) print d <= (TOTAL_ELEMENTS - 2)
No symbol "TOTAL_ELEMENTS" in current context.

What now!? As it turns out, the limitation of GDB is that it cannot know about define statements. This is because of the way defines work in C - it is actually a compile time issue; the compiler goes through the code and replaces any instance of TOTAL_ELEMENTS with whatever it is defined as. Well, if that’s true, we can do the same thing and preserve the integrity of the code! So let’s try again:

(gdb) print d <= ((sizeof(array) / sizeof(array[0])) - 2)
$4 = 1

It’s still true! Hmm. So why doesn’t it enter the loop? Well, as it turns out, there is a difference between using an actual define statement and just copy-pasting the code following the define to replace TOTAL_ELEMENTS. defines are almost always unisgned int, whereas when we copy paste the code into GDB, it becomes int. When comparing unsigned int to a signed int, d gets casted to an unsigned int, and -1 becomes 11111111111111111111111111111111 in binary, which is 4,294,967,295. So it evaluates to false!

If you’re not sure as to why that becomes -1, you need to review your computer architecture. This goes back to how negative numbers are stored internally. Read up on 2’s complement.

So, we need to change our for loop code to make sure both sides of the comparison are being treated as signed integers.

for (d = -1; d <= (signed int) (TOTAL_ELEMENTS - 2); d++)

Now run the code again:

[bear:~] 127) gcc bug.c

[bear:~] 128) ./a.out
23
24
12
17
204
99
16

Sure enough, it works. This example illustrates some of the weaknesses of gdb, and why it is absolutely essential to really understand define statements.

Learning GDB Part 3: Commands Galore

break or b - creates a new breakpoint. Can specify line numbers in a file or a function name.

clear - deletes an individual break point. Usually takes an argument as to which breakpoint to clear.

(gdb) b main
Breakpoint 3 at 0x400534
(gdb) clear main
Deleted breakpoint 3

continue or c - continues running a program from where it last left off until it finishes or is interrupted by another breakpoint.

delete or d - deletes all breakpoints.

down - goes down a stack frame.

list or l - shows the source code at a given line number. For example, list 39 would show the first couple lines above line 39 and the first several lines after line 39. Useful viewing source code without ever leaving GDB.

next or n - executes the next statement of a program that is already being run and is currently paused at a breakpoint.

print or p - prints the value of a variable or the given expression.

run or r - runs a program from the beginning. If a session has already begun and you are paused on a breakpoint and you type run, GDB will warn you:

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n)

step or s - steps into the next statement rather than just executing it. For example, if the next statement was foo();, rather than just executing it, GDB would pause at the first line of the foo function.

up - goes up a stack frame.

watch - this extremely useful command pauses the program whenever the value of the variable provided changes.

Note For Mac Users

Mac, especially OS X Mavericks, has deprecated GDB (and GCC). The new Mac equivalents are lldb and clang, respectively. Here is a quick guide to lldb - you’ll find that it is extremely similar but with extended capacity to debug applications and not just C programs.

If you still want to install old school GCC, use brew install homebrew/dupes/gdb. Then, follow this guide. You need to grant GCC root user permissions and certificate access, otherwise Mac will think it’s a virus and kill it (yay security!).

Note than when following the guide I linked, the “Snow Leapord” section applies to everything Snow Leapord and after, not just Snow Leapord (it’s an old guide). If it still doesn’t work after all that, try using sudo when you build as well as when running GDB.

I would highly recommend that Mac users actually do this simply because testing locally is much preferrable to doing all the debugging on a remote server - it’s much slower, and remote environments are not as configurable as your own local box.

Misc Benefits

We’ve only done a limited example here, but trust me that when you get to pointers, GDB becomes even more invaluable. In the mean time though, did you know you can use GDB to print structs? This is an awesome feature of GDB.

struct foo {
    int a;
    int b;
    char c;
};

int main() {
    struct foo bar;
    bar.a = 5;
    bar.b = 9;
    bar.c = 'f';
    return 0;
}

Enter GDB and try and print bar:

Breakpoint 1, main () at stuff.c:18
18    printf("%d\n", bar.a);
(gdb) print bar
$1 = {a = 15, b = 9, c = 102 'f'}

That’s really useful - especially when you start your crawler and start dealing with rather complicated structs.

GDB has more or less redefined the standard for debugging. Did you know python also has a GDB equivalent? It’s called - wait for it - PDB. Read all about it and debug your code more effectively.

True Local Testing - Virtual Box

There have been many concerns regarding the differences between Mac-C and standard Linux C. These are fair concerns - you will find that programs that work on Linux may not work on Mac or vice versa. I highly recommend getting a local linux environment set up on your laptop.

Many of you are not comfortable with partitioning your hard drive to do so; I understand. Partitioning is a daunting task for people, especially if you have never done this sort of thing, and it requires careful preparation (taking a back up) because should something go wrong, you are truly in deep doo-doo (that, I believe, is a technical term).

Behold the solution to all your qualms: Virtual Box. Now, for free, you too can partake in the wonders of Linux. Simply select your favorite distro (newbies, you should choose Ubuntu - “The World’s Most Popular Free OS” - it is the easiest and most user friendly bar none,) and develop in an environment that is identical (in all ways that this class is concerned about) to the Linux servers. No more worries of whether your Mac/Windows code will break on the servers! No more strange platform-related bugs!

This is also a fantastic exercise - take a weekend when you have free time, and try this out. Seriously. Making an executable USB, learning how to interact with a VM. This is fantastic systems stuff - you will be surprised in how much you can learn from simple hacky stuff like this.

Some of the more advanced peeps may want to try out:

Of course, there is always this awesome site that decides for you :).

GDB Aliases

You should all use these GDB / GCC aliases in your ~/.bashrc.

alias mygcc='gcc -Wall -pedantic -std=c99'
alias dgcc='mygcc -ggdb -g3 -gdwarf-2'

-Wall is meant to enable all warnings in your code - this is useful because warnings in C are often sources of error.

-pedantic forces you to stick to strict style conventions - this also helps in debugging.

-std=c99 avoids many of the more “useless” warnings. C has adopted many of the better features from C++, and almost nobody codes in standard C79 anymore. Use either this one or std=gnu99.

If you haven’t guessed yet, dgcc is meant for debugging. -ggdb enables GDB hooks into your code. -g3 and -gdwarf-2 are both used to enable macro hooks for GDB. Remember how GDB couldn’t understand the macro TOTAL_ELEMENTS? That problem goes away with these two flags. Unfortunately, the output of d <= (TOTAL_ELEMENTS - 2) doesn’t change, so it still doesn’t help in debugging the other example, but that doesn’t meant it won’t be useful in future problems!

The Lab

This lab is fairly simple - you could do this in Java or Python in a breeze. Rock-paper-scissors is as hard as it gets. It’s not designed to be difficult conceptually. Like the last lab, the purpose is for you to get used to C programming. You don’t know the libraries, you don’t know the functions. This is to get you used to the quirks of the C language before the hard stuff comes - and believe me, winter is coming.

There are some resources you may want to learn about!

Your new C bible - internet edition. Seriously - it has every C function, an example of how it’s used, the parameters it takes, and what it returns. Learn it. Cherish it. Love it.

Anything network related ever - Okay, to be fair you won’t do this until later on in the term, but since this is a comprehensive guide for C, I might as well just put this here. The final project will heavily involve this stuff.

The actual C Bible from the creators of C - read if you have lots of time. A lot of this is very interesting for historical stuff, and there is some phenomenal content in there, but odds are you don’t have time for this or you probably would have learned Vim already, right? RIGHT!?