Tutorial Goals

This tutorial’s goal is to get you comfortable debugging your own code, since you are both the one who made the mistake and the one who has to find it.

By the end of this tutorial, you should be able to:

  1. Debug with printf, and keep debug output out of your program’s real output with fprintf
  2. Use GDB to pause a running program and inspect its variables

Getting Started

The files for the non-graded exercises are already available in the tutorial folder. Open a terminal and change to that folder:

cd ~/tutorial

Run the following command:

make test

It should print the program’s output, followed by PASS.

Introduction

No one writes correct code the first time. Even experienced programmers spend more time debugging than writing1. The goal is not to get it right immediately, it’s to develop a systematic way of finding and fixing mistakes when they happen.

We will cover two common ways to debug your code. The first is printf debugging: sprinkling print statements everywhere until something makes sense. It is scrappy, but it works, and most programmers reach for it first. The second is using a proper debugger like gdb, which lets you pause your program and look around inside it. We will cover both.

Debugging with printf

Printf debugging is the simplest approach you can take: add a printf statement, run the program, and see what it prints. It requires no setup and works anywhere.

Add printf statements in debug.c to print the values of a and b. Run make test again, and notice how the check fails.

The downside is that print statements tend to accumulate over time, and forgetting to remove them can clutter your output or interfere with test cases. We will look at two ways to manage that.

Using fprintf

fprintf works like printf, but lets you route debug messages somewhere else, keeping them separate from your program’s actual output, so you can leave them in without interfering with test cases.

Modify your print statements in debug.c to use fprintf instead of printf. The syntax is the same, except you add stderr as the first argument:

fprintf(stderr, "a = %d\n", a);

Run make test again, and notice how the check passes.

stderr is a second output channel that the check does not read. We’ll cover it properly in a future tutorial. Debug Macro (Extra)

With fprintf, the print statements are still in your code. If you want them gone entirely without deleting them manually, you can use a debug macro:

#ifdef DEBUG
    fprintf(stderr, "a = %d\n", a);
#endif

Pass -DDEBUG when compiling to include them, or leave it out to remove them all at once.

Wrap your print statements in debug.c with the #ifdef DEBUG macro above, then compile with and without -DDEBUG to see the difference.

GNU Debugger (GDB)

Imagine being able to freeze time in the middle of your program: everything is paused, and you can walk around inspecting every variable, then resume or step forward one line when you are ready. That is what a debugger does.

Debuggers serve two kinds of people. The first group uses them to find bugs in code they wrote. The second group uses them to understand code they did not write, sometimes without access to the source at all.

In CS1010, we are firmly in the first group.

Printf vs GDB

With printf, you decide what to observe before you run the program. With GDB, you decide as you go.

In practice, printf is usually faster when you wrote the code and know it well. There is overhead to using a debugger: you leave your editor, learn a new interface, and set things up before you can start. That overhead pays off when you are staring at unfamiliar code, whether it was generated by AI, written by someone else, or when your TA is stepping through your submission.

Opening an Executable in GDB

Tip

Remember to compile with debug information with the -g flag.

There are two ways to open an executable in gdb.

  1. Specify the filename when starting gdb: gdb <filename>
  2. If you forget to specify the filename: Type file <filename> inside gdb

Compile the file debug.c, with -o debug and -g flags, and try opening the executable in gdb using both ways.

TUI Mode

When debugging source code, you almost always want to enable the Terminal User Interface (TUI). It lets you type commands while showing you exactly where you are in the code.

Similar to opening an executable in gdb, there are two ways to enter TUI mode.

  1. Start gdb with the --tui flag: gdb --tui <filename>
  2. If you did not start gdb with that flag, you can still enter TUI mode from within gdb: Press Ctrl-X, followed by a

Mnemonic

Ctrl-X, a for “eXtra Awesome”.

Try entering the TUI mode using both ways.

Setting Breakpoints

A breakpoint marks a line where the debugger will pause its execution. When a breakpoint is hit, that line has not been executed yet. This is how you take control of program execution. Without them, running code in a debugger is no different from running it normally.

You can set breakpoints using b <filename>:<line number> (e.g. b main.c:5). If your program is compiled from a single file, you can omit the <filename> and the : (e.g. b 5).

Other than specifying filename and line number, you can also set breakpoints using function names (e.g. b main to set breakpoint at the start of main). See: https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_28.html#SEC29

  1. Open the executable debug in gdb
  2. Set the breakpoint at the start of the main function: b main
  3. View the breakpoint you set: info breakpoints or i b

Other type of breakpoints

Breakpoints are triggered when the execution reaches a certain line. You can set conditions on breakpoints, so that they will only trigger when a certain condition is met. See: https://sourceware.org/gdb/current/onlinedocs/gdb.html/Conditions.html

Watchpoints will trigger when a variable’s value changes. See: https://sourceware.org/gdb/current/onlinedocs/gdb.html/Set-Watchpoints.html

Run the Program

You can use start or run to run the executable.

What’s the difference between start and run?

Controlling Program Execution

You can use c, n or s to execute the next lines of code. They stand for “continue”, “next”, and “step” respectively.

Explain the differences between continue, next and step

Tip

The UI might be messed up from the printing of the program, you can use Ctrl-L to refresh the screen.

Viewing Variables

To view the content of a variable, you can use print (or p for short): p var.

If you want to print a variable after every command execution, you can use display: display var. To undisplay, use undisplay instead of display.

Let’s try to print some variables.

  1. Set breakpoint at the start of the main function: b main
  2. Start the program: start
  3. Execute the first line: n
  4. Print the value of variable a: p a
  5. You can treat the syntax a bit like C, where you can print the address of a: p &a. (If the variable is a pointer, you can use * to dereference a pointer)

Let’s explore the usage of display.

  1. Set breakpoint at the start of the find_max function: b find_max
  2. Start the program: start
  3. Execute to breakpoint: c
  4. Display the value of variable max: display max
  5. Step through the function using n and observe the changes of max

Viewing the Return Value of a Function

To view the return value of a function, use the finish command to run until the current function returns.

  1. Set a breakpoint at the find_max1 function: b find_max1
  2. Start the program: start
  3. Continue to breakpoint: c
  4. Type finish to see the return value

Copying text in GDB TUI

You can copy text without leaving TUI mode:

  1. Hold Shift
  2. While holding it, click and drag across the text to select it
  3. Release the mouse, then release Shift
  4. Press Ctrl-Shift-C to copy

Paste with Ctrl-Shift-V. Holding Shift temporarily gives the terminal control of the mouse.

Graded Task(s)

Enter the Graded Task Folder

Run cd ~/tutorial-graded before continuing

Bob has a challenge for you. He has a program called verify, that prompts for a secret. He claims that you won’t be able to find the secret, because he only gives you partial source code. That ensures that you can’t view the source code to see the secret, or recompile the program to add printf statements.

Find the secret and provide it as input to the program. Run the program with:

./verify

Important

You have to run the program outside of gdb to complete the task.

Before you leave: verify your progress

Run this command inside your Tutorial environment:

progress

It shows whether you have successfully completed the tutorial and which individual tasks have been recorded as complete.

The progress command is available only inside a Tutorial environment, not in the general CS1010 WebTop environment.

Footnotes

  1. You will spend more time editing code than writing it from scratch, so knowing your editor well pays off. Time spent in vim is never wasted. ↩