In this lab we will explore the use of both mechanisms. In a later class we will even consider more difficult problems of memory leaks.
#include <stdlib.h>
#include <stdio.h>
int sum(int x, int y, int z) {
char c = 4;
int *a;
return (c + x + y + z + *a) / 3;
}
int main(int argc, char *argv[]) {
int i, j, k;
int result;
if (argc == 1) {
printf("Please specify three numbers as parameters.\n");
exit(1);
}
i = atoi(argv[1]);
j = atoi(argv[2]);
k = atoi(argv[3]);
result = sum(i,j,44) + sum(j,k,55) + sum(i,k,66);
printf("%d\n", result);
return 0;
}
This program has some problems, and it would not take you long to figure them out. However, we're going to use debugging statements to attempt to help determine where the problems are.
So, copy and put this into a file called test1.c. Also create a Makefile and put something like the following into it:
CC = gcc
CPARAMS = -Wall
test1: test1.c
$(CC) $(CPARAMS) -o test1 test1.c
If you make and run this program on an old Sun, you'll likely get the following result:
tritan 2% ./test1 1 2 3 Bus error tritan 2%
Exercise 1. Modify test1.c to include debugging statements in main() such as
Now add an additional statement immediately after the last one that successfully printed:
Think about it: why might it be useful to use fprintf() as we did here, rather than the normal printf()?
and in sum() such as
fprintf(stderr, "Number of parameters = %d\n", argc);
Now re-make and run the revised program. Assuming you put the
debugging statements in the right places, you'll most likely see output from
the statement from main() and some, but not all of the ones from
sum().
fprintf(stderr, "x=%d\n", x);
fprintf(stderr, "y=%d\n", y);
fprintf(stderr, "z=%d\n", z);
fprintf(stderr, "*a=%d\n", *a);
This should help to explain why the last fprintf() failed to print.
Now try running the program with only one parameter, and see how it fails.
Add some more debugging statements to your main() to find exactly
where the line is that is causing the problem. TIP: When using
printf/fprintf, always end the format string with a \n. This will
ensure that your debugging message is printed to the screen, and not just
buffered, waiting for additional characters to be sent.
fprintf(stderr, "a=%ld\n", (long)a);
OK, so that should help make sure that you can use printf() to generate debugging output to identify the problem line when your program crashes. But what about when it is not crashing? Lots of debugging output can be annoying. In a recent lecture, we looked at preprocessor directives that allow you to turn on and off debugging code. Review that material if needed.
Exercise 2.
Revise test1.c to use preprocessor directives to easily turn on or
off the debugging output on the basis of whether the DEBUG symbol
is defined.
Add an additional entry to the bottom of your Makefile to generate a new
executable, called test1d that uses the same source file, but
includes the -D DEBUG compiler option. Note that if you just type
make at this point, the new program will not get built. You can
either type make test1d, or add a new entry at the beginning of
your Makefile such as:
You should now have two binaries---one that was compiled with debugging
enabled, and one that was compiled with it disabled.
This creates a new dependency, called all, which requires that test1
and test1d be up-to-date. Note that make will automatically
make just the first target in the Makefile, which is why make did
not build test1d earlier.
all: test1 test1d
While we will not cover it here, it is possible to use the compiler option -D to also set a value for a symbol, which is useful for different levels of debugging. It might also be prudent to note that you could also control debugging code interactively with a regular variable (which might be controlled by an option to your program), rather than using preprocessor directives during compilation.
Debugging statements are simple, and work in all environments. But debugging that way is often quite tedious, as you must edit and re-compile many times until the right statements are in place to capture the effect that you need to understand. A debugger provides an interactive way to watch the execution of your program---you can stop the program at any point, examine the contents of variables, look at the calling stack, check function parameters, and more.
There are a number of common debuggers (adb, dbx, gdb, for example), but generally you want to use the one that matches the compiler you used. Since we used gcc, we want to use the GNU Debugger (gdb). In general, to use any debugger, we need to generate an executable program that contains debugging symbols (essentially information needed to map from the code that is running to the source code with which you started, so that the debugger can tell you variable names, function names, and line numbers from the original source). This requires re-compilation with the parameter -g in both compilation and linking. Once you've modified your Makefile and re-compiled test1 with debugging symbols, we can now run test1 within the debugger.
Exercise 3.
Run your program within the GNU debugger with the command
To actually start your program, type run along with any needed parameters, as in:
This might be enough information for you to know where the problem is,
fix it, and run it again. In that case, type quit to exit
the debugger (and yes, the debugger will need to kill your running
program to do so).
If not, you can ask the debugger for more information at this point. One useful question is to ask the contents of a particular variable (a). To do that, type
We might also want to know how we got here---that is, what function
called this one, and so on. The command for that is backtrace.
Try that command now.
Notice that in this simple example, there is only one calling context to see
on the stack---just the call to main(). In a real program there may be
many such contexts, and backtrace, or more simply
bt, will show the sequence of calls (and the values of their
parameters).
At this point you haven't actually started your program yet, but can set variables to watch, breakpoints in your code at which to stop running, etc.
tritan 2% gdb test1
GNU gdb 6.2.1
Copyright 2004 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 "sparc-sun-solaris2.10"...
(gdb)
Note that it shows us the usual output, plus the error and
interestingly, the function in which the error occurred---sum() with
particular parameter values, and the specific line at which it failed.
(Note that the specific information for your program may vary slightly.)
(gdb) run 1 2 3
Starting program: /home/brian/test1 1 2 3
Number of parameters = 4
x=1
y=2
z=44
Program received signal SIGSEGV, Segmentation fault.
0x106ec in sum (x=1, y=2, z=44) at test1.c:10
10 fprintf(stderr, "*a=%d\n", *a);
(gdb)
This tells us nicely that in this case
the int * named a contains the value 0 (i.e., it
is a null pointer). Note that the value of *a will depend on the OS, the compiler, the architecture, etc., which is why this program does not necessarily fail on the new (X86) Suns. We could easily ask about other variables that are
accessible in this context (where the error occurred), such as x, y, or z.
(gdb) print a
$1 = (int *) 0x0
(gdb)
Given the limited amount of time available in this class, we haven't really even scratched the surface of what is possible to do with gdb. It will be up to you to learn more about gdb on your own. I highly recommend Norman Matloff's Guide to Faster, Less Frustrating Debugging as it has an intro to using gdb. Learn how to set breakpoints, change the values of variables of a running program, etc. Much of this is also covered Chapter 17 of the text by Robbins.
As mentioned earlier, gdb is just one debugger that may be available. In practice, it is a good one, and works even when you only have an ssh or telnet connection to the system. However, the interface is a little cryptic. [It helps if you know and remember that you can type help at the gdb prompt and get information on how to use it.] Fortunately, others have extended gdb by incorporating a visual interface. One well-regarded interface is called ddd (data display debugger), which can be the front-end for a variety of back-end debuggers (for perl, bash, make, python, java, etc). ddd is becoming fairly common, and is often found on Linux installations, and is also installed here on the Suns. Again, I highly recommend Norm Matloff's Debugging Tutorial---in particular, the PDF slide show on using ddd.
Regardless of what debugger you use, there is one more aspect that you need to know.
To see how large a core file you are permitted to generate, type ulimit -a . A limit of 0 will prevent the generation of core files at all. I suggest that you set it (typically in a file called .profile) to a large value, such as:
Now, any programs that you run from this shell that exit abnormally will also likely generate a core file. Note that this change will only affect the current shell window session---you'll need to do this again in the future whenever you wish to generate core files (or else modify your .login or .bashrc file to include this command).io:~% ulimit -c 500000 io:~% ulimit -a core file size (blocks) 500000 data seg size (kbytes) unlimited file size (blocks) unlimited open files 256 pipe size (512 bytes) 10 stack size (kbytes) 8192 cpu time (seconds) unlimited max user processes 7893 virtual memory (kbytes) unlimited io:~%
Why is this important? Well, gdb is capable of reading in a core file (that was generated by a program with debugging symbols) and telling you where the problem is! In exactly the same way that we saw the line and the calling stack, etc., gdb can do the same for a core file. Typically, you'll want to run it as:
This is quite helpful, as it means you can capture the state of the program in which the error occurs, even when you are not running under a debugger (which is most of the time). This is the mode in which I use a debugger most---after I've used a program, it crashes and dumps core. I start a debugger and examine the core to find out where, and hopefully why, it crashed.tritan 2% ./test1 1 Number of parameters = 2 Segmentation fault (core dumped) tritan 2% ls -l core -rw------- 1 brian faculty 80364 Feb 23 00:39 core tritan 2% gdb -c core test1 GNU gdb 6.2.1 Copyright 2004 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 "sparc-sun-solaris2.10"... Core was generated by `./test1 1'. Program terminated with signal 11, Segmentation fault. Reading symbols from /lib/libc.so.1...done. Loaded symbols for /lib/libc.so.1 #0 0xff2c0310 in atoi () from /lib/libc.so.1 (gdb) bt #0 0xff2c0310 in atoi () from /lib/libc.so.1 #1 0x000108c8 in main (argc=2, argv=0xffbff7f4) at test1.c:29 (gdb)
I hereby repeat my strong recommendation to learn to debug faster by working through the tutorials mentioned above for gdb and ddd.