LAB2_DEBUG: Introduction to Debugging¶
Due date: 2026-03-22 23:59.
Debugging is an essential skill for any programmer, and it involves finding and fixing errors or bugs in your code. In this exercise, you will be given a piece of C++ code that compiles and runs, but has some errors in it, and your task will be to identify and correct those errors.
Download The Provided Files¶
Just like you already did it in LAB1_INTRO, start with downloading and unzipping the provided files from here: lab2_debug.zip.
The code for this activity resides in the lab2_debug directory. Get there by typing the following line in the terminal while in your working directory (you can press tab to autocomplete):
cd lab2_debug
You should now have the following files in your lab2_debug directory:
- Makefile
- main.cpp
- png.h
- png.cpp
- rgbapixel.h
- rhbapixel.cpp
- processImage.cpp
- in_01.png
- in_02.png
- in_03.png
- out_01.png
- out_02.png
- out_03.png
Determining What's Going Wrong¶
As you can see, the source code files we provided you with are almost all familiar now, except for processImage.cpp. This is where we planted some bugs for you to catch.
To figure out what is wrong you could try opening processImage.cpp and start looking through the code. This is a good approach for fixing logical bugs (such as when a wrong color gets applied to the output image than intended), however, it's usually not the best option for fixing runtime errors or general code bugs. In such cases, you should follow the workflow outlined below:
Debugging Workflow¶
Quoted 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.
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 << "Reached line " <<__LINE__ << __FILE__ << ": x = " << x << std::endl;
__LINE__ is a special compiler macro containing the current line number of the file, while__FILE__ contains the name 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>
Notice that we intentionally avoid using
using namespace std; to prevent naming conflicts and maintain clarity. By explicitly using std::cout, we ensure it's evident that cout belongs to the standard C++ library. This is considered good programming practice, and helps keep the code clear, readable, and easy to maintain.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) .
Compiling and Running the Code¶
To compile (make) and run your code, type the following into your terminal:
make cp in_01.png in.png ./processImage
Notice that if you do not copy your files with
cp in_01.png in.png, the code will exit with an error message:[EasyPNG]: Failed to open in.png An error occurred: Failed to load the image file in.png.
Make sure you copy each of your sample input files in_01.png, in_02.png, and in_03.png into the file named in.png, which is expected by the executable processImage.
Debugging the Code: Bug 1¶
As you can see, the code crashed with
Segmentation Fault (segfault). This happens when you access memory that doesn't belong to you - such as dereferencing a NULL or an uninitialized pointer.You might also get
munmap_chunk(): invalid pointer error which also indicates a problem with a pointer. Uninitialized pointers can have unpredictable results, so the error might vary in some cases...Try adding print statements to the lines right before and after the call
to
to
original->ReadFromFile() in void processImage() function:std::cout << "Reached line " <<__LINE__ << __FILE__ << std::endl;
Or by explicitly printing the line numbers:
std::cout << "Reached line 100" << std::endl; // // provided code lines 101 - 104 here // std::cout << "Reached line 105" << std::endl;
Now run ./processImage again. You'll see line 100 print out, but not line 105. This means the segfault occurred sometime between executing lines 100 and 105. We'll let you figure out what the bug here is and how to fix it. You'll see
line 105 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 get another segfault. You'll want to narrow down the line it's occurring on and its cause by printing more information. Try putting
cout statements before and after the call to sketchify function.Let Compiler Help You¶
Open up your Makefile and take a look at the compilation flags we provided you with. You will notice the
-w flag that supresses the compiler warnings. Compare these to the flags we used in MP2: INTRO. If you update your Makefile to stop hiding compiler warnings, you’ll start seeing more bugs pop up. Basically, you will turn the compiler into your debugging assistant, letting it help you spot the errors.More debugging!¶
Like most code out there, the code we provided you with isn't perfect. However, fixing it can be as simple as completing the steps outlined above to gain a better understanding of what the program is doing at runtime, thus enabling you to fix the bugs. Good luck!
Testing Correctness of Your Code¶
Once you think processImage is working, you can compare your output (out.png) to the expected output using
diff:diff out.png out_01.png
If they match (
diff won't give any output if they do), you've finished. If not, you've still got a bit more debugging to do - go back and add some print statements to figure out why the outputs differ.If
diff is unhelpful, you can open each image up using a graphical viewer. If you need more advanced tools, for example in Paint.NET, you can open the images in separate layers and change blending to 'difference', but most likely just having the images layered and swapping between them is enough to reveal differences in the image content.Additional Resources¶
Pointers refresher: Binky Pointer.
Acknowledgments¶
We would like to express our gratitude to prof. Cinda Heeren and the student staff of the UIUC CS Data Structures course for creating and sharing the programming exercise materials that have served as the basis for the labs used in this course.
Revised by: Elmeri Uotila and Anna LaValle
Anna palautetta
Kommentteja tehtävästä?