CSCI 2330
Foundations of Computer Systems

Bowdoin College
Spring 2024
Instructor: Sean Barker

Lab D - Debugger Debut

Release Date:Monday, February 19.
Due Date:recommended completion by Monday, February 26.
Collaboration Policy:Level 0 (unrestricted collaboration)

This mini-lab exercise is designed to give you practice methodically debugging C programs and checking for various kinds of memory errors. The most primitive method of debugging (which you have likely used in the past) 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 common C errors that crash the program without providing any useful information on what went wrong (such as a segmentation fault). In this mini-lab, rather than relying on print-based debugging, you will use two more advanced tools: valgrind (an automated memory checker) and gdb (a general-purpose debugger), which are described below.

This lab is ungraded, but completing it will familiarize you with tools that will be extremely useful for the current Lab 2 and will be necessary for future labs. You will save yourself time and hassle in this course by taking the time to become comfortable in the debugger now! Experience with a debugger is also an important skill for programmers in general.

The two tools you will be using are described below, followed by a description of the debugging exercises.

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: Valgrind will run a given program and watch its memory operations as it executes, printing out various kinds of extra output if it observes the program doing unsafe things (such as the examples listed above). To execute Valgrind, just run valgrind and pass the name of the program you wish to execute along with any command-line arguments to be passed. For example, to run myprog through the tool with command line arguments foo 1, you could run:

valgrind ./myprog foo 1

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

While running Valgrind is simple, the primary challenge is interpreting its output to aid in debugging your program. As a general principle, don't try to read and understand every word of Valgrind's output. Instead, look for key words and phrases (such as accessing uninitialized memory, double frees, memory leaks, etc.) that may give some hint as to what's going wrong, and note which lines of code are generating these messages. Line numbers will be indicated as myfile.c:123, which would indicate that the warning was generated by line 123 of myfile.c. Looking at the very first Valgrind warning is also generally a good place to start; later warnings are likely to stem from earlier ones and therefore may be less helpful in debugging.

To some extent, making sense of Valgrind's output simply requires practice. Feel free to ask myself or the LAs if you are not sure what to make of a Valgrind message. These messages may not immediately indicate what's wrong with your code (even if you understand what the message is saying), but they will often point you in the right direction. Plus, an absence of Valgrind warnings is a fairly reliable indicator that your program is free of memory problems.

The GNU Debugger (GDB)

In cases where Valgrind alone is not sufficient to fix problems, your next step should be using a full-blown debugger such as gdb. The essential feature of a debugger is the ability to freeze a program's execution midway and print various pieces of information about the program state at that instant. A full debugger is much more powerful than Valgrind alone but is also more complex, as working with the debugger requires learning the basic debugger commands.

As a very simple example of what you can 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 freeze 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. Since this process is interactive, you do not need to decide in advance what you want to inspect (unlike with print-based debugging).
  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 lines of 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. Here is 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 (though not all of these commands will be useful or even make sense initially). It's a good idea to keep a hard copy of this sheet handy while debugging.

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:

If you'd like another reference on using GDB, here is a quick guide to GDB that includes more detailed examples of most of the typical commands.

Debugging Exercises

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

Each of the five numbered programs (#1 through #5, roughly in order of difficulty) has a bug that you are not likely to spot immediately just by looking at the code. Some of these bugs result in crashes, while others do not crash the program but instead result in incorrect output.

Starting with program #1, track down the source of each bug and fix it. 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 given below:

  1. Read 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 looks like it should be doing).
  2. Run the program. The program may crash, or it may produce incorrect 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). If you don't recognize anything wrong with the program's output when you run it, you won't be in a good position to debug the program.
  3. Start to debug. Running valgrind is usually a good first step. Examine the Valgrind output (the first warning is a good place to look) and see if it helps you identify the bug.
  4. If Valgrind alone is not sufficient, use gdb to debug the program using breakpoints and inspection of the program state as it runs.
  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 you see the expected behavior.
  6. Run a final valgrind pass to make sure there are no memory warnings or leaks reported. If there are, the program still has bugs even if the program is operating as you expect; you may just be getting lucky! Debugging should only be considered finished (as far as memory issues are concerned, at least) when Valgrind is reporting no warnings and no leaks.

The first three programs ex1.c, ex2.c, and ex3.c should be fixable using Valgrind alone. The last two programs ex4.c and ex5.c will almost certainly require you to use GDB to locate the bug.

As a reminder, you will want to consult the GDB Reference Sheet when working with GDB.

Logistics

To save time, you will just download the files for this mini-lab directly rather than using GitHub.

Login to hopper as usual, then copy/paste the following command to download the lab files as a zip archive:

wget https://web.bowdoin.edu/~sbarker/teaching/courses/systems/24spring/files/labd-debug.zip

Then, execute unzip labd-debug.zip, which will extract the labd-debug directory containing the five buggy programs. From within that directory, run make to compile the file programs, then you can execute them individually by running ./ex1, ./ex2, and so forth. Make sure you're doing your debugging on hopper, where Valgrind and GDB are already installed. You may be able to install these tools on your local machine, but such a setup is not supported.

You are welcome to collaborate and openly discuss with classmates on these exercises. However, remember that the purpose of these exercises is to practice with the debugging tools (as opposed to merely fixing the buggy programs). If you are simply told what the bugs are, then you won't have learned anything about how to debug your own programs. As such, if you happen to spot one of the bugs (perhaps without even using any of the tools), don't deprive your classmates of the chance to track it down themselves!

This lab is ungraded and there is nothing to turn in. However, you are encouraged to seek assistance in completing the lab the same as for any other lab.