Saturday, January 14, 2017

Debugging in MARS

This is a topic that is kind of difficult to effectively teach on paper, but I'm going to go ahead and try my best.  The ability to debug your code in MARS is perhaps its most important feature, and it would be a shame if I were not to include a post on this on my blog.  Further, it is pretty inescapable - you will have to use it at some point whether you like it or not, otherwise you'll get stuck.  If you are/were anything like me in my freshman to sophomore years, you'd groan any time you'd have to reach for the debugger - fortunately, this debugger is pretty well designed and its functionality maps intuitively to how MIPS is executed.

Let me set the scene first:



I have this short program with a bug in it.  You might see it offhand, but the goal here is not to simulate a realistic scenario - instead it is to walk you through the process of fixing it.  Assume I have no idea what the problem is, and I'm completely baffled as to why I have an infinite loop here.  I've read over my code about ten times, and I can't find anything wrong.  Time to bring out the debugger.

When you build your program, you are automatically taken to the execution tab.  You will see something like this in the text segment of the screen:


This is your final program as the assembler built it.  All pseudo-instructions have been translated to real instructions, and we're looking at an interpretation of the machine code that was generated.  This is what MARS will run when we hit the play button.

The leftmost column is the breakpoint column.  A breakpoint is sort of an intervention into the running of your program that causes the machine to pause so that you can look at its state.  You can tell MARS to place a breakpoint at any line - just click the box to enable a breakpoint at that location.

The second column is the address column.  Most of the time you can ignore this column completely, but there may be times that you want to compare the contents of the $ra register to where you expected the program to jump to.  It just states where the binary instruction is located in memory when the program is running.

The third column shows you how your instructions are represented in binary.  I can't think of a situation where this would be all that useful unless you were writing the MIPS virtual machine yourself, which you're probably not planning on doing if you're using MARS.

The basic column shows you the instruction in assembly form, rather than binary form.  This is how you tell what part of your code you're looking at, in conjunction with the unnamed rightmost column, which tells you the line number in your assembly file that the instruction comes from and any comments you may have added.

You may notice that the labels factorial and exit are entirely absent.  This is somewhat unfortunate because you will usually want to set a breakpoint immediately following those labels.  The solution is just to put a comment that you can recognize on the first line underneath the label next to an instruction.  Lines that only contain comments won't show up at all.  I can tell that line 8 is where the factorial loop begins, because that's the instruction that exits the loop and I always put that instruction at the top.  You may prefer some other style whereby the first instruction in your loop does not look like this, but you'll recognize where a section starts with practice.

Since I have an infinite loop, I'm going to go ahead and put a breakpoint right at the top of that loop:


I can now hit the play button to run the program.  Lo and behold, my program pauses at the top of the loop:


I can tell where the program has stopped because that line is highlighted in yellow.

Now that the program has paused itself, I have several options.  I can run the program line by line on my command, I can run it really slowly, or I can keep pressing play and running into my breakpoint and just observe the machine's state once every time the loop cycles.  For the first option, I can use two of the buttons on the toolbar:


The third button from the left will move the machine's state forward one instruction, and the fourth will move the machines state backward one instruction (it will return to the previous state).  I can keep hitting this button until something looks off.

I could also play with the slider next to the toolbar:


By default this is set to the max setting, which just tells MARS to run the program as fast as possible (as fast as your computer can run it).  If I move the slider to the left, I can choose a specific number of instructions per second in the range of 1 - 30:


There's a bit of a quirk that can show up sometimes with this feature.  If you ever move the slider back to the right after changing its setting, MARS may end up running your program in slow motion anyway.  This is hardware dependent and happens only to some people.  If you run into this problem, just save your file and restart MARS.

For the third technique, all I have to do is keep pressing play and MARS will hit my breakpoint again, having gone through the loop one more time.  In this specific case, I prefer to use this tactic.

No matter which technique you decide is most user-friendly, you will have to say hello to the registers panel on the right side of your screen:


There are three tabs here but Coproc 1 and Coproc 0 are for advanced use only, so we'll stick to the Registers tab.  You have the whole family of registers at your disposal here.  In the case of this program, I was using $t0, $t1, and $t2, so I'll want to look at those.  Right now they contain the initial values that I gave them before the start of the loop, because MARS hasn't actually simulated one loop cycle yet.  If I hit the play button, the MARS will hit my breakpoint again and I'll see this:


The register that was last written to is highlighted in green.  Notice that the values stored in these registers still hasn't changed, despite having gone through one loop cycle.  That's not the expected behavior - I should see that the counter has been incremented, but it hasn't.  Hmm.

Oh!  I forgot to increment the counter!  Doy.


I added my increment at line 11, and the program is fixed!  Yay!

There is one more important feature that I didn't need to use, though.  My little program doesn't go to memory.  Sometimes I might want to look at memory, since the state of my program is dependent on it.  There is a screen for this underneath the breakpoint setting screen:


This shows you the entirety of main memory.  Every single byte.  The left column tells you the address in memory that the row begins at.  From left to right, the columns next to this one show the contents of several words in increasing address.  The column labels of these columns tell you the offset of each word from the address all the way to the left.  You will have to be familiar with endianness if you are looking for resolution at the byte level.  For integers in MIPS, the bigger bytes are placed first, so it looks exactly as you would write the integer on paper, but in hex.  You can toggle the hex off if you'd like with the convenient checkbox at the bottom, though.

Also useful is the drop-down menu that will allow you to travel between different areas in memory.  Because main memory is so big, this allows you to skip the process of scrolling down thousands of times.  Most of the time we're interested in the contents in the .data section, which is the default location anyway.

Like the registers tab, the word that was last written to will be highlighted while debugging:



In this case I've just written the word 15 to address 0x10010000.

And that's it.  If you have any questions or are running into trouble in ways that this tutorial hasn't addressed, feel free to leave a comment.  I will be checking my blog often, and will update this post with attribution if you run into an interesting problem.

No comments:

Post a Comment