Debugging

In this lab we will focus on debugging in two ways: first we will be doing informal course evaluations to give us some feedback on how you are doing in the course, what is working for you and what isn't. This way we can debug the course itself. After that, your job is to become comfortable with the C++ debugger under Linux.

Course Evaluations

Go to Anonymous Evaluation. Select CS 12. Please answer the following questions, in order (thus question #1 should be in the "Comment #1" field, etc.)

  1. Please comment about the Instructor, Mr. Truppel.
  2. Please comment about your TA, Titus.
  3. What do you think of the in-lab exercises? Are they too easy, too difficult, or just right? Have there been any that are confusing? What needs the most work in the labs?
  4. What do you think of the at-home programming assignments? Too easy, too hard, just right?
  5. Please comment on the in-lecture quizzes.
  6. Comment on the course overall -- please grade the course A-F, and explain. Tell us what we can do better.
  7. What do you think of the "point system?" Is it good, bad, too much freedom, too confusing? If you are behind (you should have 2 or 3 assignments turned in by now), why?
  8. When did you take CS 10 or the equivalent prereq?
  9. If you took CS 10 or an equivalent from another school, tell us what language was used, and whether adjusting to UCR was hard or easy (in either case, tell us why.)
  10. Anything else you would like to say, please say it here.
When you are done and have submitted your comments, please show the "Thank you" screen to Titus for 1 lab point.

gdb

By this point in the course you should be fairly confident in your C++ abilities, and you should be pretty familiar with the Linux environment for development and compilation. One thing remains in the suite of powerful development tools under Linux: the debugger.

The very popular (and free) debugger companion for g++ is gdb. Much like the Microsoft debugger used in CS 10, gdb allows you to execute your code in a number of ways, and inspect the state of your variables throughout the execution of your program. In this lab we will demonstrate some of the more common methods of using gdb, give you some links for more information, walk you through using the debugger, and hopefully let you tackle some real debugging problems.

One of the most common reasons to need a debugger is because your program crashed, usually producing either a "Segmentation Fault" or occasionally a "Bus error" or other mysterious error (like "Floating exception."). What we want you to take from this lab is the lighting-reflex instinct of grabbing the debugger when such an error occurs. (We are serious about this. In the future, if you get a run time error and don't instinctively run the debugger on it, we will be very sad. This is a very powerful tool that we really really want you to become comfortable with.)

Let's try an example:
#include <iostream>

using namespace std;

int main()
{
        // We are setting this pointer to not point at anything
        int* bar = 0;

        // Now try to print out what is in the bad pointer
        cout << *bar << endl;

        // And now try to set the bad pointer
        *bar = 42;
}
This program will certainly crash (hopefully you can see why. If not, please go re-read the chapter of your text regarding pointers.) So we compile it as normal and see what happens:
> g++ -Wall -Werror -W -pedantic test1.cc -o test1
> ./test1
Segmentation Fault
We have a problem (as expected), so lets see what the debugger says:

Using gdb

Start the debugger on the command line and give it the name of your executable as an argument. To continue with our example:
> gdb test1
NU gdb Red Hat Linux (5.2.1-4)
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
(gdb)
Now the debugger waits for instructions. To get it to run our program and see what happened, give it the instruction "run".
(gdb) run
Starting program: /home/titus/cs12/test1

Program received signal SIGSEGV, Segmentation fault.
0x080485fd in main ()
(gdb) bt
#0  0x080485fd in main ()
#1  0x401124ad in __libc_start_main () from /lib/libc.so.6
(gdb)
Which is not terribly helpful. We are now told the memory location of the instruction that killed the program, and the fact that the program died in the function "main ()." This is not news (we only defined main, of course that is where the problem is!).

The problem is that gdb requires some extra information to be in the executable that it is trying to debug. This would be problematic, except for the fact that the same people responsible for g++ are responsible for gdb. So adding in the extra symbol information is quite easy. Go back and recompile your file with "-g"
> g++ -g -Wall -Werror -W -pedantic test1.cc -o test1
and rerun the debugger (hit Ctrl+D to terminate gdb, and let it terminate your program).
(gdb) run
Starting program: /home/titus/cs12/test1

Program received signal SIGSEGV, Segmentation fault.
0x080485fd in main () at t.cc:13
13              cout << *bar << endl;
(gdb)

Ooh, that's much better. Not only do we know what happened, we even know what line it happened on. Very useful. Let's look at another (contrived) example:
#include <iostream>

using namespace std;

int fac(int n)
{
        if (n == 1)
        {
                int* bar = 0;
                cout << *bar << endl;
        }
        return n * fac(n - 1);
}

int main()
{
        cout << fac(5) << endl;
}

Now we want to see what gdb will let us do with regard to functions. Compile as before (with the -g flag), and run the debugger on the program.
> gdb test2
GNU gdb Red Hat Linux (5.2.1-4)
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
(gdb) run
Starting program: /home/titus/cs12/test2

Program received signal SIGSEGV, Segmentation fault.
0x080485f9 in fac(int) (n=1) at test2.cc:10
10                      cout << *bar << endl;
(gdb) bt
#0  0x080485f9 in fac(int) (n=1) at test2.cc:10
#1  0x0804861e in fac(int) (n=2) at test2.cc:12
#2  0x0804861e in fac(int) (n=3) at test2.cc:12
#3  0x0804861e in fac(int) (n=4) at test2.cc:12
#4  0x0804861e in fac(int) (n=5) at test2.cc:12
#5  0x0804864d in main () at test2.cc:17
#6  0x401124ad in __libc_start_main () from /lib/libc.so.6
(gdb)
(Note: bt stands for "back trace".) Ooh, this is nice. It will keep track of all the stack activation frames, their parameters, and where in your code they are listed. With this it is pretty easy to tell how your program got into the state it was in. But now, how do we see what that state really is? On the same run of gdb, let's peek around at the internals of the program.

(gdb) print bar
$1 = (int *) 0x0
(gdb) p n
$2 = 1
The command "print", which can be abbreviated as just "p", shows the current type and value of the variable (or expression) requested. This lets us examine what all the variables were just as the program died. What if we want to see what the value of something in an earlier function (lower on the stack) is?
(gdb) up
#1  0x0804861e in fac(int) (n=2) at test2.cc:12
12              return n * fac(n - 1);
(gdb) p bar
No symbol "bar" in current context
(gdb) p n
$4 = 2
The commands "up" and "down" let you move through the function stack. Slightly annoyingly, "up" moves you lower in the stack and "down" moves you higher (so you must think of the function stack as growing down, rather than growing up.) Here we went up to the next higher recursive call, and tried to print out "bar." Bar was defined locally to the scope of the "if (n == 1)" statement, so it doesn't exist in this part of the function (remember scoping rules!) But the variable "n", which exists in both scopes (as a different local variable, remember), shows the correct value for this activation frame. With just these tools, you can do quite a lot of debugging of broken code.

Other Useful gdb Commands

Another useful set of commands are those related to "breakpoints." Those of you that took CS 10 recently will recall that breakpoints are a point in your code that the debugger will stop execution if it ever reaches. This is a complicated notion, so we will demonstrate with an example. Start gdb on the second example program above.
GNU gdb Red Hat Linux (5.2.1-4)
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
(gdb) break 16
Breakpoint 1 at 0x8048628: file test2.cc, line 16.
(gdb)
The command "break" allows us to set a breakpoint on a specific line of our source code. For programs that have only a single source file, this can be done by simply giving the line number. For programs with multiple source files (like the third homework assignment!), this can be done by specifying the name of the file, a colon, and the line number (like "maze.cc:20"). So we set a breakpoint at the beginning of main (line 16), let us also set one at line 9, where the bad pointer in our factorial function is defined.
(gdb) break 9
Breakpoint 2 at 0x80485e4: file test2.cc, line 9.
(gdb)
Now we can run our program as before.
(gdb) run
Starting program: /home/titus/cs12/test2

Breakpoint 1, main () at test2.cc:16
16      {
(gdb) step
main () at test2.cc:17
17              cout << fac(5) << endl;
(gdb) step
fac(int) (n=5) at test2.cc:7
7               if (n == 1)
(gdb) step
12              return n * fac(n - 1);
(gdb) step
fac(int) (n=4) at test2.cc:7
7               if (n == 1)
(gdb) continue
Continuing.

Breakpoint 2, fac(int) (n=1) at test2.cc:9
9                       int* bar = 0;

The "run" command is stopped when it reaches the first breakpoint. We can continue executing line-by-line using the "step" command (to step through the code.) If we tire of using "step", we can continue the execution using the "continue" command, which runs from the current point in the execution until the program crashes or it hits a breakpoint (much like "run" only without restarting the program.)

With these tools in gdb, we can solve pretty much any problem we encounter. The debugger is a powerful tool, and one that you WILL have to understand at some point in your career. It is very much in your best interest to get proficient with it early, it will help you out a lot. People that are gdb gurus are a valuable commodity.

More Info

gdb has a fairly extensive on-line help system. From within gdb, simply type "help" and it will give you a list of topics, each of which has a long list of commands contained within. Learning everything there is to know about gdb is beyond the scope of the class, but it is a lofty goal.

For more friendly help, simply type gdb tutorial into Google.

The Assignment

7 points:

Write a program in C++ that causes a "Segmentation fault", "Bus error", or "Floating Exception" inside a function other than main. Demonstrate how you would find that error using gdb. Turn in a text file called "debug.txt" containing a copy of your debugging session. Make sure you use breakpoints, "up", "down", and "print."

1 point

Show Titus the results of your evaluation submission so we know you did it.

.2 - .6 points:

Write a 2-3 sentence summary of other useful commands in gdb for .2 points each. Turn this in as "commands.txt".

.2 points:

What is the problem with this program (turn in as "bug.txt")
#include <iostream>

using namespace std;

int main()
{
    int n;
    int temp = 5;
    string foo = "Hello world!";
    int myArray[n];

    cout << foo << temp << endl;
}

.2 points:

What is the problem with this program (turn in as "bug2.txt") (it will compile, it may or may not run)
#include <iostream>

using namespace std;

struct Point
{
	int x;
	int y;
};

int main()
{
	Point* p = new Point();
	Point* q = p;

	p->x = 10;
	q->y = 15;

	delete q;
	delete p;

}