CSCI 262 Data Structures

Fall 2018

Lab 9: Debug

(Back to Labs)

Goals


Overview


This lab will take you through some simple uses of the CLion debugger, applied to a somewhat silly program based on a previous week's lab. As usual, we ask that you work with a partner or a team. If you end up solo, please raise your hand and we'll pair you with someone else (or make a three-person team if necessary). You are welcome to change partners throughout the semester!


Instructions


Step 0: preliminaries

Download lab09.zip, which contains a CLion project folder for this lab. Open or import the folder in CLion, and it should compile and run as is (although it won't yet work as intended!) The program we are building is called rwg, for Random Word Generator.

Step 1: First bug

The rwg program, when working, should generate some random words from the provided dictionary. Give the program a try now, giving it the dictionary.txt file as input when prompted (don't forget to copy or move dictionary.txt to the cmake-build-debug directory).

Okay, it obviously didn't work. You can probably look at the code and find the problem in a few minutes, but this lab is intended to help you with using the debugger, so let's find the bug that way. To start getting familiar with the debugger, let's set a breakpoint on main(), and start single-stepping through lines of code until the program exits. A breakpoint is simply some location in the code at which the running program will pause and give control to the debugger. To set the breakpoint, open main.cpp in the CLion editor and put your mouse above the bar to the left of the code window (between the actual code and the line numbers). If you click on this bar, you should see a red dot appear on the bar, indicating a breakpoint has been set at this location in the program. (See image below for what you should see in CLion).

Now we are ready to start running the program under the debugger. To do this, click the "bug" icon next to the usual run icon (or using the "Debug 'rwg'" command from the Run menu). You should see some new panes open up at the bottom of CLion in place of the usual console. (The console is still there, in a tab named "Console", next to the "Debugger" tab.) Here's what the new pane looks like on my screen:

Let's explore the debugger interface just a bit. On the left hand side there are buttons to let you restart the program, resume running the paused program, pause a running program (disabled at the moment, because we are paused at the first breakpoint), and do some other tasks like managing all of your breakpoints. Here's what those buttons look like on my screen:

Along the top of the debugging pane is another set of buttons. We're going to use these buttons a lot, as they will let us step through the code in various ways and inspect the values of variables. Left to right, these buttons let you:

Here's what those buttons look like on my screen:

If you hover over any of the buttons (on the left or on the top), a help text should appear to tell you what they do, and a longer explanation will appear at the bottom of the screen. You can also find out the keyboard shortcuts for each button this way.

Using the "Step Over" function (the downward pointing arrow icon), start stepping through main(). Note that each time you step, a blue bar in the code editor shows you where you are. Some additional info is shown in the panes below the stepping buttons. In the left pane, you can see what function and file (with line number) you are in. In the right pane, you can see the values of any local variables.

If you keep stepping, at some point you'll cross over the code that requests a filename from the user. At this point, you'll need to click on the "Console" tab to interact with the program - type in "dictionary.txt" and hit enter in the console to keep going. Click the "Debugger" tab to return to the debugger view of things.

Keep on stepping until you get to line 31 of main.cpp:

     if (generator.is_empty()) {

If you look at the next line of code, you can see that the current line of code is critical. If the generator.is_empty() call returns true, the program will exit with the message you got previously when you ran the code. Let's see why generator.is_empty() is returning true, by stepping into the function, rather than over the next line. Either of the "Step Into" functions should work here.

The editor should now open up in the file rwg.cpp, with the blue bar on this line of code:

     return _max_length == 0;

You should also note that the pane information on the left now shows you that you are in the method rwg::is_empty. Below that is the function main. You might now be able to tell that what you are seeing in the left pane is the function call stack (with the most recent stack frame shown on top)!

Notice also that the right pane is showing you different variables - now it is showing you what is available in the current frame of reference. At this time, all you can see is this, the pointer to the current rwg object. You can click on the triangle button next to this to dive into the variables held in the object.

Dive into this, and take a look at the value of _max_length - this the variable being tested in the current line of code in the editor. (You can alternately use the expression evaluator to find this out, but that is overkill for this situation. Save the expression evaluator for when you want to evaluate something more complex than a simple variable.)

Okay, clearly something is wrong in our code, because _max_length is zero, which means we're about to return true, which means our program is going to quit even though we supplied a valid dictionary file. You might want to spend some time at this point looking at rwg.h to see if you can figure out the broad outlines of how the rwg class is supposed to work. Or, I can just tell you that the _max_length member variable is supposed to be storing the length of the longest word stored in the map _words. If you try diving into the _words collection inside of this, you'll see that, in fact, we have lots of words of varying lengths stored. So somehow, the _max_length variable got set incorrectly. (Ignore the fact that this is probably the wrong way to check to see if we have words - just go with it.)

There are a lot of ways to proceed at this point, including ways to use the debugger, but time is short, so let's cut to the chase. The problem is in the initialize method of rwg. For practice, let's use the debugger to find the offending line of code. You can kill the currently running instance of the program using the red square button on the left of the debugger pane. Now, remove the breakpoint on main() (just click on the red dot to make it go away), and instead set a breakpoint on rwg::initialize in rwg.cpp (line 19).

Run the code again with this new breakpoint, then stepping through the lines in initialize() using the "Step Over" function. Be sure not to use the "Step Into" function, unless you want to step into some gnarly system code! (If you do so accidentally, you can jump back up using the "Step Out" function.) Single step until you hit line 37, and you should see the bug.

Fix the bug by editing line 37 of rwg.cpp only (don't fix any other bugs you happen to notice). You are now ready for the next step!

Step 2: Second bug

Try running the program again (normally, not under the debugger). You should get a little farther this time - it will let you specify the number of words you want to generate. Try asking it for 1000 words. You should (unless you get fantastically lucky) get a message like

    terminate called after throwing an instance of 'std::out_of_range'
      what():  map::at

possibly along with some generated words. It is also possible, but less likely, that you will get a segmentation fault. Try a few times to see.

To the debugger! This time, we'll run the program under the debugger and let it stop the code for us, by stopping when the "out_of_range" exception occurs. (Or, you might get the segmentation fault - if so, restart and try again.) Go ahead and remove the breakpoint on rwg::initialize, and start the debugger.

You should see something like

    terminate called after throwing an instance of 'std::out_of_range'
      what():  map::at
    Signal: SIGABRT (Aborted)

in the console. This isn't really new info for us, so let's switch over to the "Debugger" tab and see what we can find out there. In the function call stack view, you can now see a whole bunch of stuff. Most of that stuff isn't useful to us (maybe if you were a deep system programmer it would be). The interesting stuff is near the bottom, where we see our files: rwg.cpp and main.cpp.

CLion makes it easy to move around the function call stack by just clicking on the stack frame we are interested in - cool! Click on the stack frame that is labeled with the rwg::_get_words method name. Check out the variables in the right pane - what is the value of the variable len? Hm, do we have any words of this length? Looking at the code (you should be now looking at line 89 in rwg.cpp in the code editor), it seems likely that this is our problem. The exception that we got indicates that the problem is we asked for a key that doesn't exist in our map. (The documentation for the map at() method states the reason for the exception: http://www.cplusplus.com/reference/map/map/at/.) How did this happen?

We need to go up further and see where _get_words was called, and maybe further than that. Try moving up (well, down in this case - CLion inverts how we usually think of the function call stack) the stack to see if you can spot where the bug came from.

This part is a bit subtle, and may be hard to debug since you didn't write the code and aren't super familiar with the approach taken. So let me summarize: Essentially, the problem occurs in the _get_lengths method, which is called in generate(). The _get_lengths method uses a too-simple algorithm to determine valid word lengths in the dictionary: it simply chooses values between the minimum and maximum lengths. However, there are some gaps - not all word sizes in this range are represented in the dictionary. The code needs to be fixed to verify that a randomly generated length is actually in the dictionary before adding it to the vector of lengths. Modify the code to do this check (don't forget you can use the count method of your map to test for the existence of a key), and you'll have fixed the second bug.

Step 3: Third bug

If you've fixed the bug properly and retry the program (normally), you should (again, unless lucky) get another error. The error you get may vary depending on what version your compiler is - you may get a "std::logic_error" exception, or you might get a segmentation fault. Either way, the debugger is adept at halting the program when the problem occurs. So fire up the debugger once more! Run your program, and let the debugger tell you where the error occurs. At this point you know enough about the debugger to find the bug yourself - you don't need to do much other than examine the stack trace and examine variables and look at the code around the point where the error occurred in our code. See if you can find and fix the bug yourself - if not, a TA or classmate can help.


Wrapping Up


Obviously there are a lot of things that the debugger can do that we didn't cover here. More advanced/conditional breakpoints, display variables, probes, and much more. You'll have to explore some of this on your own, using whatever CLion documentation you can find, the internet, and your own buggy code :) Also, note there are other debuggers in other IDEs, all of which should have nearly identical features, but different ways of accessing them. If you are a command line user, take a look at the program gdb (if using g++ as your compiler), or lldb (if using clang). Hopefully we've at least given you the tools to get a running start.

To complete this assignment, please complete the quiz for this lab in Canvas. The quiz asks you to explain, in a few sentences, the nature and cause of the third (and hopefully final) bug in the random word generator.

NOTE: Just because we fixed all of them and the program runs, we can't be certain we got all the bugs!!! As Edsger W. Dijkstra once said, "Program testing can be used to show the presence of bugs, but never to show their absence!"