CSC 161 Schedule Readings Labs & Projects Homework Deadlines Resources

Debugging with gdb

Starting gdb

Recall from today's reading that you may run GDB from the terminal or within Emacs. While we hope you might settle into using GDB from within Emacs (one of its great strengths as a development environment), here you will make sure you can do both.

  1. The program array-bug.c contains two simple functions for doing array manipulations.
    1. Copy the program to your account.
    2. Read over the program (particularly the function documentation and the main procedure) to be sure you understand what it is supposed to do, but do not make any changes to it.
    3. Compile and run the program to confirm its (mis)behavior. For this exercise, we'll use the system default compiler:
      cc -g -o array-bug array-bug.c
  2. Use the terminal to start GDB on your program and then run your program within GDB. (You will give one terminal command, and one GDB command within terminal.) You may then exit GDB with the quit command.
  3. Start GDB within Emacs and then run your program within the GDB buffer window. Leave GDB running after the program completes.

Setting break points

You should have noticed that your program does not yield the "expected" results. To isolate the bug (in case you haven't spotted it already), we will want to probe some values while the program is running. (That way we don't have to decide before we compile which values may be interesting to look at.) Thus we will practice setting break points in the code to get a better handle on the action.

  1. Set a breakpoint at the line just after the definition of the variable expected, (i.e., the line with the call to compute_average, then run your program.
    1. Use GDB to inspect the value of expected before the compute_average procedure runs. Does it have the value intended? (You may also want to verify the value of length.)
    2. Use GDB to "step over" the call to compute_average (that is, use the next command).
    3. Inspect the value of expected (i.e., just before the call to the halve procedure). Did it change?
    4. Inspect the value of the mean variable after calling compute_average. Is it correct?
    5. Step over the call to halve and inspect the value of expected once again. Did it change?
    6. Corruptions are often caused by mismanaging references. Determine the memory addresses of the four variables declared in main.
    7. Use kill to release the current execution of the program.

Setting watchpoints

By this point you should have identified two problematic behaviors in the program: (1) mean is computed incorrectly within compute_average, and (2) expected is somehow corrupted within halve. We will now use additional GDB tools to help isolate the cause(s).

  1. Run the program again. While stopped at the first breakpoint (within main) set a write watchpoint for the expected variable.
    1. continue executing the program. That is, don't use the step or next commands; instead, GDB should pause the program wherever expected changes value.
    2. Where does the corruption occur? What are the old and new values?

      While GDB shows you the code context, issue a backtrace request to see the execution context as well. (This particular stackframe is very short, but when a function has many possible entry points, understanding the execution context is a critical step in the debugging process.)

    3. Use the info command to inspect the values of the current stack frame's arguments and local variables.
    4. Use the print command to additionally determine addresses of the several local variables available where the program is currently paused: i, length, and values, and even values[i]. Compare these addresses to those you identified earlier.
    5. Construct a hypothesis about how (not why) the variable gets corrupted.

Stepping the source

Chances are very good now you've not only determined the nature of the bug, but also its source. Bear with us as we walk through one more important aspect of using the debugger, stepping through code line by line to monitor behavior.

  1. Set a breakpoint within the halve procedure. (Recall you may simply give a function name as an argument to break—a rather pleasing command in this instance.) Temporarily disable the first breakpoint you had set earlier and then re-run the program.
    1. Where does the program actually break? Is it where you expected? Why do you suppose it stops there?
    2. Inspect the value of the relevant variables (all those listed in Exercise 4c) in a single line using a printf statement. Here is a starting example:
      printf "i=\t%p\t=\t%d\n",&i,i
      Are any of these values surprising? Construct a hypothesis to explain them.
    3. Execute the step command once, note the output produced, and then re-issue (via command history) the printf command to help identify the side-effect and execution context.
    4. Repeat the previous instruction (step, printf, inspect) several times as you traverse the loop.

If you've been paying attention to the values and the addresses, by now you should be able to hypothesize the precise cause for the bug(s), which you may now attempt to fix. Be sure to confirm your fixes. If they do not fix the problem, restore your program to its original state and continue to use the debugger to explore the behavior while also studying the source code carefully.

Optional: Über-advanced GDB

For those with extra time, explore one or more of the following sections of the GDB Manual, identify some more advanced command(s) we have not yet discussed or used in the lab, and try them out on this or another program.