Preface: How Did We Get Here?
Your lab partner is writing an image recognition program. They are 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:33
, etc), the program finally compiles! Overjoyed to have a program, you both decide to test it on a couple images.
Segmentation Fault
Ouch. What do we do now?
Checking Out the Code
All assignments will be distributed via our release repo on github this semester. You will need to have set up your git directory to have our release as a remote repo as described in our git set up.
You can merge the assignments as they are released into your personal repo with
git pull --no-edit --no-rebase release main --allow-unrelated-histories
git push
The first git command will fetch and merge changes from the main
branch on your remote repository named release
into your personal. The --no-edit
flag automatically generates a commit message for you, and the--no-rebase
flag will merge the upstream branch into the current branch. Generally, these two flags shouldn’t be used, but are included for ease of merging assignments into your repo.
The second command will push to origin
(your personal), which will allow it to track the new changes from release
.
You will need to run these commands for every assignment that is released.
All the files for this lab are in the lab_debug
directory.
Preparing Your Code
If you are running a M1/M2 Mac, you may experience issues using gdb with the mold
linker. Edit the CMakeLists.txt
located in the lab_debug
folder, commenting out line 55 so it looks like below:
# find_program(MOLD_FOUND mold)
This semester for MPs we are using CMake rather than just make. This allows for us to use libraries such as Catch2 that can be installed in your system rather than providing them with each assignment. This change does mean that for each assignment you need to use CMake to build your own custom makefiles. To do this you need to run the following in the base directory of the assignment. Which in this assignment is the lab_debug
directory.
mkdir build
cd build
This first makes a new directory in your assignment directory called build
this is where you will actually build the assignment and then moves to that directory. This is not included in the provided code since we are following industry standard practices and you would normally exclude the build directory from any source control system.
Now you need to actually run CMake as follows.
cmake ..
This runs CMake to initialize the current directory which is the the build
directory you just made as the location to build the assignment. The one argument to CMake here is ..
which referes to the parent of the current directory which in this case is top of the assignment. This directory has the files CMake needs to setup your assignment to be build.
At this point you can in the build directory run make as described to build the various programs for the MP.
Make this Yours!
Your partner left one thing out of the code because they weren’t sure… what’s your favorite color?
Use a website like HSLA Color Picker to find your favorite hue (the first slider). Once you found it, edit sketchify.cpp:33 and replace -1 with your favorite hue.
Determining What’s Going Wrong
You could start and 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.
Debugging Workflow
From: DEBUGGING: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems by David J. Agans
- Understand the System.
- Without a solid understanding of the system (the system defined as being
both the actual machine you are running on as well as 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.
- Without a solid understanding of the system (the system defined as being
both the actual machine you are running on as well as 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:
- 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.
- 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)!
- 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.
- 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 or not 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.
- 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.
- Check the Plug.
- Make sure you’re 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.
- 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).
- 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 (std::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, such as:
std::cout << "line " << __LINE__ << ": x = " << x << std::endl;
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
__LINE__
is a special compiler macro containing the current line number of
the file.
If you’re getting compiler errors after trying to use std::cout
statements, then you
need to #include
the iostream
library like this:
#include <iostream>
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).
What’s up with this syntax?
std::cout
is a stream, and <<
is called the insertion operator. If you
want to read more about streams and what this syntax means, you can check out
this article on C++ input/output.
Note: Make sure to remove the print statements before submitting your final version of code .
Debugging Your Code
To make and run the code, first ensure you’re in the build
directory as mentioned above. Then, type the following into your terminal:
make
cp ../tests/in_01.png in.png
./sketch
Your First Bug
As you can see, your 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 40 and 44, before and after the calls
to original->readFromFile()
, width()
, and height()
.
Use the std::cout
statement below on lines 39 and 43.
std::cout << "Reached line " << __LINE__ << std::endl;
Now run main
again. You’ll see line 40 print out, but not line 44. This means the segfault occurred sometime between executing lines 40 and 44.
Work on getting your program to run to Line 44!
Bug 2
Once you’ve fixed the first bug, you’ll get another segfault. You’ll want to
narrow down the line it’s occurring on and its cause by printing more
information. Try putting std::cout
statements at the beginning and end of the
inner for loop.
More debugging!
Like almost most all code written, the code 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. Good luck!
Checking Your Output
Once you think sketchify is working, you can compare your output (out.png
) to the expected output by opening each image up using a graphical viewer.
If the outputs differ, you’ve still got a bit more debugging to do – go back and add some print statements to figure out why the outputs differ!
The command diff
can also be used to compare two different files.
diff out.png ../tests/out_01.png
If the files differ, the output of this command will either say they differ, or provide specific differences between the two files. If there is no output, then the files are identical. This can be used on any file extension (e.g. .txt), and not just images.
You can also use the compare
utility to generate a visual comparison of the images. For this to work, you’ll have to temporarily change your color’s hue to 280, since compare
will expect the images to match exactly. Once you’ve recompiled and rerun with that new hue, run the following command:
compare out.png ../tests/out_01.png comparison.png
Differences between your image and the expected image will be highlighted in red in comparison.png
.
Autograder Testing
You can run a subset of the test cases that will be used in the autograder with the following commands:
make test
./test
Submitting Your Work
The following files are used in grading:
sketchify.cpp
To submit your assignment you upload these file to the lab_debug question on PrairieLearn
Note: Make sure to remove the print statements before submitting your final version of code .