CSCI 2330
Foundations of Computer Systems

Bowdoin College
Fall 2019
Instructor: Sean Barker

Debugging Mini-Lab

Assigned:Friday, September 27.
Suggested Due Date:Wednesday, October 2.
Collaboration Policy:Level 0 (open collaboration)

This exercise is designed to give you practice with methodical debugging, particularly of C programs and various kinds of memory errors. We will focus our attention on two useful debugging tools: valgrind (an automated memory checker), and gdb (a debugger).

This mini-lab is not formally graded, but will familiarize you with tools that will be essential to all future labs. Spending some time up-front to get comfortable with these tools will be very worth your while, both in this class and beyond. The programs you will work with here will also help you get ready to tackle Lab 2.

Tool Overview

The most primitive type of debugging is print-based debugging -- i.e., add some print statements, re-compile and re-run, then repeat as necessary until you track down the bug. While simple, this type of debugging is also cumbersome and inefficient. It is especially unsuited to many types of errors common in C programs (e.g., segmentation faults) that crash a program without providing any useful information on what went wrong. Here, rather than relying on prints, you will use two more advanced tools (valgrind and gdb), described below.

Valgrind

Valgrind is a memory checking tool that is designed to spot various kinds of common memory errors. Typical examples include:

The basic operation of Valgrind is simple: the valgrind program is called and passed the name of the program you wish to execute through the tool. For example, to run myprog through the tool, you could run:

valgrind ./myprog

This command will execute myprog within the Valgrind framework, and will print various Valgrind messages and warnings as the program runs (in addition to any output that the program itself produces). A program that passes Valgrind without warnings will produce a relatively small amount of boilerplate Valgrind output, while a program that has many memory errors is likely to produce a long log of Valgrind warnings.

As a general principle when interpreting Valgrind output, don't try to read and understand every word. Instead, focus on key words and phrases in Valgrind messages (e.g., accessing uninitialized memory, double frees, etc) that may give some hint as to what's going wrong, as well as which lines of code are generating these messages. Line numbers will be indicated as myfile.c:123 if the warning was generated by line 123 of myfile.c. Looking at the first Valgrind warning is also generally a good idea; later warnings are likely to stem from earlier ones and therefore may be less helpful in debugging.

The GNU Debugger (GDB)

In cases where Valgrind alone is not sufficient to fix problems, the next step should be a full-blown debugger such as gdb. The basic idea of a debugger is that some program of interest (such as the one you're trying to debug) is run through the debugger, which allows freezing execution midway through the program and printing various useful information about the program state. GDB is much more powerful than Valgrind alone, but also more complex, as working with the debugger requires learning the basic debugger commands.

As a very simple example of what one could do with a debugger like GDB, consider the following:

  1. Set a mark (called a breakpoint) at a particular line of a program, which tells the debugger to stop execution at that line.
  2. Start running the program through the debugger, which will execute until it hits the breakpoint and then pause execution.
  3. Now you can inspect the program's state - for example, printing variable values, inspecting memory locations, and so forth.
  4. You can also step through the program (executing one line at a time), allowing you to inspect program state at a fine granularity as frequently as desired. For example, you might step through the program while observing the behavior of a particular variable of interest.

You can launch GDB to debug myprog by running gdb myprog. You can then interact with the debugger using GDB commands. Proficiency with GDB requires becoming comfortable with the most common commands. To aid you in this pursuit, we have prepared a GDB Reference Sheet, which lists the most important GDB commands along with illustrative examples. Look over this sheet to get a sense of what is possible with GDB. It's also a good idea to keep a hard copy of this sheet handy while debugging.

Note that not all of these commands will be immediately necessary. The most important commands to start with are those on the left-hand column (especially breakpoints and execution), along with basic print commands. Some more specific pieces of GDB advice are given below:

Debugging Exercise

Your task is to debug a series of buggy C programs using valgrind and gdb, and without inserting any print statements into the programs! While print-based debugging has its place, for the purposes of this exercise, consider printf banned.

The buggy programs have been checked into your SVN directories under debug-practice (run svn update to download them). In that directory are five numbered programs (roughly in order of difficulty) that have various bugs. Some of these bugs result in crashes, while others result in erroneous output but do not crash the program.

Starting with program #1, track down the source of each bug and fix it. Following the guidelines above, you should not modify the source code of the program at all until you have already identified the bug and are fixing it. A suggested plan of attack for each buggy program is below:

  1. Browse the source code to understand what the program is doing. Don't worry about fine details on a first pass; just get a sense of what the program appears to be doing (or rather, what it should be doing).
  2. Run the program. The program may crash, or it may produce erroneous output. In the latter case, make sure you understand what's wrong with the output (which will likely depend on your initial inspection of the source code).
  3. Start debugging. Running valgrind is usually a good first step. Examine the Valgrind output and see if it helps you identify the bug.
  4. If Valgrind alone is not sufficient, fire up gdb and debug the program using breakpoints and inspection of the program state. This process is, of course, more involved than just running Valgrind.
  5. Once you have identified the bug (and only then!), modify the program source code to fix the bug. Recompile and re-run to verify that the bug is fixed.

The first three programs (strings1.c, strings2.c, and strings3.c) can likely be solved using Valgrind alone. The last two (search4.c and matvec5.c) will almost certainly require you to use GDB. Make sure you're running on turing, where Valgrind and GDB are already installed (you may be able to install these tools locally, but such a setup is not supported).

You are welcome to work with classmates on these exercises, but remember that the goal is to practice with the debugging tools, not merely to fix the buggy programs. As such, if you happen to spot one of the bugs (perhaps without even using any of the tools), don't immediately give it away to your classmates!

There is nothing to turn in for this mini-lab.