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.)
- Please comment about the Instructor, Mr. Truppel.
- Please comment about your TA, Titus.
- 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?
- What do you think of the at-home programming assignments? Too
easy, too hard, just right?
- Please comment on the in-lecture quizzes.
- Comment on the course overall -- please grade the course A-F, and
explain. Tell us what we can do better.
- 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?
- When did you take CS 10 or the equivalent prereq?
- 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.)
- 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;
}