Eroxl's Notes
02 - Lab Debugging

Learn how to debug your code!

Introduction to Debugging

Alice is writing an image recognition program. She's working on the tracing algorithm, which turns the image into a trace of the outlines in the image. After going through all the compiler errors (sketchify.cpp:13, etc), the program finally compiles! Overjoyed to have a program, she decides to test it on a couple images.

Segmentation Fault

Ouch. What does she do now? She has to debug her code.

Follow instructions from Lab Intro on setting up the directory and downloading the code.

You can get the files for this lab by downloading lab_debug.zip

Determining What's Going Wrong

Alice could open sketchify.cpp and try to figure out what's happening. This is good for logical bugs—when you only rotate half of your image, for example, or the image doesn't rotate at all. Walking through what your code does to yourself or your partner is a good exercise in debugging bugs in your algorithm. However, this is often a poor choice for debugging runtime errors or general code bugs. In this case, you should attempt to use the following workflow to debug your code (taken mostly from "DEBUGGING: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems" by David J. Agans):

Debugging Workflow

  1. Understand the System.
    • Without a solid understanding of the system (the system defined as being both the actual machine you are running on and the general structure behind the problem you are trying to solve), you can't begin to narrow down where a bug may be occurring. Start off by assembling knowledge of:
      • What the task is
      • What the code's structure is
      • What the control flow looks like
      • How the program is accomplishing things (library usage, etc)
    • When in doubt, look it up—this can be anything from using Google to find out what that system call does to simply reading through your lab's code to see how it's constructed.
  2. Make it Fail.
    • The best way to understand why the bug is occurring is to make it happen again—in order to study the bug you need to be able to recreate it. And in order to be sure it's fixed, you'll have to verify that your fix works. In order to do that, you'll need to have a reproducible test case for your bug.
    • A great analogy here is to turn on the water on a leaky hose—only by doing that are you able to see where the tiny holes might be (and they may be obvious with the water squirting out of them!).
    • You also need to fully understand the sequence of actions that happen up until the bug occurs. It could be specific to a certain type of input, for example, or only a certain branch of an if/else chain.
  3. Quit Thinking and Look.
    • After you've seen it fail, and seen it fail multiple times, you can generally have an idea of at least what function the program may be failing in. Use this to narrow your search window initially.
    • Start instrumenting your code. In other words, add things that print out intermediate values to check assumptions that should be true of variables in your code. For instance, check that that pointer you have really is set to NULL.
    • Guessing initially is fine, but only if you've seen the bug before you attempt to fix it. Changing random lines of code won't get you to a solution very fast, but will result in a lot of frustration (and maybe even more bugs)!
  4. Divide and Conquer.
    • Just like you'd use binary search on an array to find a number, do this on your code to find the offending line! Figure out whether you're upstream of downstream of your bug: if your values look fine where you've instrumented, you're upstream of the bug (it's happening later on in the code). If the values look buggy, you're probably downstream (the bug is above you).
    • Fix the bugs as you find them—often times bugs will hide under one another. Fix the obvious ones as you see them on your way to the root cause.
  5. Change One Thing at a Time.
    • Use the scientific method here! Make sure that you only have one variable you're changing at each step—otherwise, you won't necessarily know what change was the one that fixed the bug (or whether your one addition/modification introduces more).
    • What was the last thing that changed before it worked? If something was fine at an earlier version, chances are whatever you changed in the interim is what's buggy.
  6. Keep an Audit Trail.
    • Keep track of what you've tried! This will prevent you from trying the same thing again, as well as give you an idea of the range of things you've tried changing.
  7. Check the Plug.
    • Make sure your assumptions are right! Things like "is my Makefile correct?" or "am I initializing everything?" are good things to make sure of before blindly assuming they're right.
  8. Get a Fresh View.
    • Getting a different angle on the bug will often times result in a fix: different people think differently, and they may have a different perspective on your issue.
    • Also, articulating your problem to someone often causes you to think about everything that's going on in your control flow. You might even realize what is wrong as you are trying to describe it to someone! (This happens a lot during office hours and lab sections!)
    • When talking to someone, though, make sure you're sticking to the facts: report what is happening, but not what you think might be wrong (unless we're asking you what you think's going on).
  9. If you didn't fix it, it ain't fixed.
    • When you've got something you think works, test it! If you have a reproducible test case (you should if you've been following along), test it again. And test the other cases too, just to be sure that your fix of the bug didn't break the rest of the program.

Basic Instrumentation: Print (cout) Statements

The easiest way to debug your code is to add print statements. To do this, you can add comments at various points in your code. We added these statements at the top of the file:

#include <iostream>
using namespace std;

so you can print messages to standard output. Here's an example:

cout << "line " << __LINE__ << ": x = " << x << endl;

__LINE__ is a special compiler macro containing the current line number of the file.

The above line prints out the current line number as well as the value of the variable x when that line number executes, for example:

line 32: x = 3

Print statements work for debugging in (almost) any language and make repeated debug testing easy—to repeat debug testing with a new change, all you need to do is compile and run the program again. They also require nothing new to learn (smile).

Command line Debugger: Gdb

But what if you want to know exactly which line caused the segfault, or what function calls led up to the error? Print statements will not be very helpful here. Thankfully, we have GDB (GNU Debugger), a debugger you can run from the terminal.

To start a GDB session on an executable, simply run gdb ./execname from the terminal. In our case, we'd want to examine the sketchify executable.

Once you're at the above point, you can start debugging the program. If there's a segfault, the debugger will stop when it happens, and you can inspect from there.

Here's a list of useful GDB commands, and there's also a very handy cheatsheet here. Shortcuts to the commands are in parentheses, arguments to commands are in angle brackets.

run (r) # Run the program from start
next (n) # Execute the next line of code
step (s) # Step into function in current line
continue (c) # Continue until next break point
break (b)  # Set a breakpoint at the location
backtrace (bt) # Print function call stack, most recent first
print (p)  # Evaluates and prints expression
info locals # Prints info about all local variables
info args # Prints info about all function arguments
quit (q) # Quit GDB

Debugging Alice's Code

A single test case is provided in the testsketchify.cpp. To compile and run the test, type the following into your terminal:

make test
./test

The test will sketchify the UBC CS logo and compare it against the sample output in the given_imgs folder. The test will pass if the output matches with the sample output.

Alice's First Bug

As you can see, Alice's code caused a Segmentation Fault, or segfault. This happens when you access memory that doesn't belong to you—such as dereferencing a NULL or uninitialized pointer.

Try adding print statements to lines 54 and 58, before and after the calls to original->readFromFile(), width(), and height().

cout << "Reached line 54" << endl;
cout << "Reached line 58" << endl;

Now run sketchify again. You'll see line 54 print out, but not line 58. This means the segfault occurred sometime between executing lines 54 and 58. We'll let you figure out what the bug here is and how to fix it. You'll see "line 58" print to the terminal once you've finished—then move on to Bug 2, below.

Bug 2

Once you've fixed the first bug, You'll see a bunch of error outputs.

Why is it getting pixels out of bounds? You should decide first which function call is causing this issue, and then figure out why it's causing the issue. Maybe there's a requires statement (think back to 210) that isn't being honoured?

Bug 3

Once you've fixed the second bug, you'll get another segfault. You'll want to narrow down the line it's occurring on and its cause by using GDB. Sometimes a bug will cause your program to run in an infinite loop: you can press CTRL-C to terminate execution.

Note that you will see the following warning when you run "make"—your code will still run with this warning, but you should try to fix this in order to avoid unexpected behavior.

warning: address of stack memory associated with local variable 'pixel'
returned [-Wreturn-stack-address]
    return &pixel;

More Debugging

No more segfault! However, the test is still failing. Try to open out.png and compare it with the sample output (see the section below). Once you think sketchify is working, you could also add more tests to testsketchify.cpp with different images.

Alice's code, like most of ours, isn't perfect. But fixing it is as simple as repeating the above to learn more about what the program is actually doing at runtime so that you can solve the issues. Read through the specifications carefully and pay attention to the details. Good luck!

Checking Your Output

If the test is unhelpful, you can open each image up. Or you could compare them using compare:

compare out2.png given_imgs/out_2.png comparison.png

Differences will be highlighted in red.

Submission

#include "cs221util/PNG.h"
#include "cs221util/RGBAPixel.h"
#include "cs221util/RGB_HSL.h"
#include <cmath>
#include <cstdlib>
#include <iostream>

using namespace cs221util;
using namespace std;

// sets up the output image
PNG *setupOutput(unsigned w, unsigned h) {
  PNG *image = new PNG(w, h);
  return image;
}

// Returns my favorite color
RGBAPixel *myFavoriteColor(int blueval) {
  RGBAPixel *pixel = new RGBAPixel(64, 191, blueval);
  return pixel;
}

// Returns a score determining if the pixel is an edge
// requires 1 <= x < width - 1; 1 <= y < height - 1
double edgeScore(PNG *im, unsigned x, unsigned y) {
  double score = 0;

  // calculate score based on the (HSL) luminance of the current pixel
  // and the 8 neighboring pixels around it
  // Don't worry about the RGB->HSL conversion here
  for (int i = -1; i < 2; i++) {
    for (int j = -1; j < 2; j++) {
      RGBAPixel *px = im->getPixel(x + i, y + j);
      rgbaColor rgba = {px->r, px->g, px->b, (unsigned char)(px->a * 255.0)};
      hslaColor hsla = rgb2hsl(rgba);

      if (i == 0 && j == 0) {
        score += 8 * (hsla.l);
        continue;
      }

      score -= hsla.l;
    }
  }

  return score;
}

void sketchify(std::string inputFile, std::string outputFile) {
  // Load in.png
  PNG *original = new PNG();

  cout << "Reached line 54" << endl;
  original->readFromFile(inputFile);
  unsigned width = original->width();
  unsigned height = original->height();
  cout << "Reached line 58" << endl;

  // Create out.png
  PNG *output = setupOutput(width, height);

  // Load our favorite color to color the outline
  RGBAPixel *myPixel = myFavoriteColor(169);

  // Go over the whole image, and if a pixel is likely to be an edge,
  // color it my favorite color in the output
  for (unsigned y = 1; y < height - 1; y++) {
    for (unsigned x = 1; x < width - 1; x++) {

      // calculate how "edge-like" a pixel is
      double score = edgeScore(original, x, y);

      RGBAPixel *currOutPixel = (*output).getPixel(x, y);
      // If the pixel is an edge pixel,
      // color the output pixel with my favorite color
      if (score > 0.3) {
        *currOutPixel = *myPixel;
      }
    }
  }

  // Save the output file
  output->writeToFile(outputFile);

  // Clean up memory
  delete myPixel;
  delete output;
  delete original;
}