This lab is an exercise on writing robust program and using GDB to debug programs. At the end of this lab, you will be familiar with the core dump file and how to fix the Segmentation Fault problem.
Download the following files, you will need them in the lab.
lab6.c - This file contains code to evaluate postfix expression. The code is similar to what you implemented in Project 1.
stack.c - This file contains implementation of a stack. This implementation of stack takes the pointer to an array stack, the stack pointer top, and the size of the array size as arguments. This allow the same code to operate on different stacks instead of implementing the stack operations for different stacks. For example, you have two stacks given by (stack1, top1, size1) and (stack2, top2, size2). To push an item onto the first stack, you call stack_push(stack1, &top1, size1, [item to push]). This way, you do not need to write two push functions for two different stack.
stack.h - This file contains the prototypes of stack operations that can be used in the program. Take a look at how #define is used to prevent including the file multiple times as discussed in lecture.
Makefile - This is the make file. Take a look at this file. The program is compiled with -g -O0 in addition to what you usually have. -g tells the compiler to emit verbose debugging information into the executable, and -O0 tells the compiler to not perform any kind of optimization to the program. Optimized programs are harder to debug.
Now, compile the program with make and run the program. Suprisingly, the program crashes!
lore 14 % ./lab6.out
Enter an expression in postfix order. E.g. 1 234 5 * 6 / + 7 - 89 -
1 234 5 * 6 / + 7 - 89 -
Read a number '1'
Segmentation fault (core dumped)
lore 15 %
There are three parts to this lab.
When you get a segmentation fault, that means your program is accessing some memory location that the program is not supposed to access. This type of error is usually hard to debug. Now do the following:
(gdb) run
Starting program: lab6.out
warning: Temporarily disabling breakpoints for unloaded shared library "/usr/lib/ld.so.1"
Enter an expression in postfix order. E.g. 1 234 5 * 6 / + 7 - 89 -
Program received signal SIGSEGV, Segmentation fault.
0x00010eb4 in stack_push (stack=0x21420, top=0x21418, size=4, item=1)
at stack.c:10
10 *(stack + (int)top++) = item;
Fix this line of code so that it works correctly.
Take a look at the content of stack.c. These are examples of poor implementation. For instance, the stack_push() function does not check whether the stack has full before pushing a new item onto the stack. This potentially causes error in a program.
Validates the input arguments before performing the operations by completing the TODOs. When the input is invalid, print out an error message and return.
Doing this in your future projects and labs will make your programs more robust and reduce the amount of time you spend on debugging.
There is another problem in the program that you may not have discovered earlier. The program crashes when encounter a very long number, e.g. 123.45678987654321 987.65432123456789 +.
Program received signal SIGSEGV, Segmentation fault.
0x00010870 in getch () at lab6.c:32
32 return (bufp > 0) ? *(buf + --bufp) : getchar();
The logic of that line of code is actually correct but the program crashes. So, we will trace this backward to find out the source of the problem. Type backtrace or bt in GDB.
(gdb) backtrace
#0 0x00010870 in getch () at lab6.c:32
#1 0x00010a88 in getToken (s=0x212c8 "123.456789875ÿÿÿ") at lab6.c:57
#2 0x00010b44 in main () at lab6.c:78
(gdb)
The backtrace command shows the program flows. The program starts from main(), calls getToken() on line 78, and calls getch() on line 57. GDB also prints out the function arguments. Take a look at the function argument of getToken(), instead of the actual number you entered, the last few characters have been replaced with some garbage ÿÿÿ. That is usually an indication of buffer overrun.
Fix the buffer overrun problem in getToken() function. The function should check whether the index i will go out of bound in array s before appending the character. If the index is going out of bound, print out an warning message and return without appending the character.
When you are satisfied that your program works correctly (or you run out of time), please do the following.
Goto the parent directory of lab06 and type the following command into the terminal:
turnin -c cs240=XXXX -p lab06 lab06
where XXXX represents your lab section number.
The turnin section is as follows:
| Section | Time | TA |
|---|---|---|
| 0201 | Thursday 15:30-17:20 | Dan Zhang |
| 0301 | Friday 09:30-11:20 | Suli Xi |
| 0401 | Friday 13:30-15:20 | Youhan Fang |
| 0501 | Thursday 09:30-11:20 | J. C. Chin |
Make sure turnin reports that your project was submitted for grading. You can check the files you have submitted by running the following command:
turnin -c cs240=XXXX -v -p lab06
| 10 points | Errors in stack.c are fixed. (About 2.5 points for each error) |
| 6 points | TODOs in stack.c were implemented correctly. |
| 4 points | Buffer overrun problem in getToken() is fixed. |
GDB is a powerful interactive debugger for many compiled languages. We will of course be focusing on C.
GDB can be used in one of two major fashions -- at runtime (gdb is started before there is a problem) or post-mortem (gdb is run to determine why a program that has already died did so). Runtime debugging is desirable whenever possible, but post-mortem debugging is useful when you have a process that fails in some unexpected way and leaves a core dump.
These two major modes of operation are invoked in different fashions. To debug a program at run time, you can either start the program from within gdb or attach to an already running program. To start a program within gdb, you would do the following:
%gdb [progname]
(gdb) run
The second common form of runtime debugging is attaching GDB to a process that is already running. This requires you to know the PID of the running process you are interested in:
%gdb [progname] [PID]
After connecting in this fashion the program can be debugged normally, as if you had started gdb and run the program from there.
To invoke gdb for post-mortem debugging, you must have both the binary you wish to debug and the coredump file from where it failed. (FYI, you can often force a core dump with Ctrl-\ [control backslash]. However, it is often more useful to attach gdb to the running process than force a core.)
%gdb [progname] [coredump file]
Coredump files can also be loaded at runtime in GDB with the core-file command, and executables can be loaded with the exec-file command.
Here is a brief list of GDB commands you will find useful.
| Command syntax | Description |
|---|---|
|
break break linenumber break filename:linenumber break function |
These commands set a "breakpoint", or a place where the debugger should suspend operation of the debugger and ask for further input. Play with their arguments a little to see how flexible break is. |
|
bt where |
The bt and where commands are functionally identical. They cause gdb to print a "backtrace", or listing of the function call stack of the current program. This is very useful for determining where and why a program died. You should play with this command on both programs that have crashed and programs that are running normally to get a feel for how it works. |
| cont | This command continues execution where it was last stopped, either by break, watch, Ctrl-C, a signal, or some other action. |
|
help help topic help command |
GDB includes an extraordinarily comprehensive help system. The bare command help will generate a list of high-level topics which can be researched in more depth by giving arguments to the help command. If you think GDB should probably be able to do some particular thing, look here -- it probably can. |
|
info category |
This prints information about some part of your program or GDB's configuration; you may find the categories locals, variables, functions, and display useful. |
|
next |
Execute the current line of code and proceed to the next, treating subroutine calls as one line. |
|
step |
Like next, except step enters subroutines, treating the first line of a called subroutine as the next line of code. |
| print expression | print is one of the most powerful commands in GDB; it is used to print the value of an arbitrary expression, such as a simple variable, a calculation, or a function call. The power of print stems from the fact that expression can be a quite complex C or C++ expression, and it will be executed just as if your program had done it; for instance, if the variable x is equal to 5 and you know that it should be six, the command print x = 6 will print the number "6", but will more importantly change the actual value of the variable x. This can be used to both determine why a section of code is failing and to prevent it from doing so, as well as to test corner cases and bounds in your code that may not be easily reached through test cases. |
| watch expression | watch is like break, except that it will stop the program's execution whenever the value of the given expression changes, rather than at a specific point in program flow. Watchpoints can make your program run significantly slower if they have to be constantly checked, so set them carefully. |
Remember that the source code line printed to the screen when the program stops is the em>next line to be executed, it has not yet run.