Here is a quick primer on using gdb (a debugger) with the RISC-V assembly language assignments. Note that you must have the full set of RISC-V development tools available under Linux, whether that is through WSL, a virtual machine, or a native installation.
Open two terminal windows in your project directory. In the first, type:
make fordebug
This will compile and run your code, but tell to stop and wait for the debugger. This is the window where you will see your program’s output (and possibly type input).
In the second window, run:
make debug
This will launch the debugger and connect it to the first window. It is helpful to have a fairly large window because the debugger will display a lot of information. You should see the current contents of all of the registers and the source code immediately surrounding the next instruction that is about to run. The program will be paused at this point.
Typically you will be debugging a single function, and you probably want to jump directly to it. To do this, set a breakpoint on the function and then instruct the debugger to continue running the program at full speed until it finishes (or crashes) or reaches the breakpoint. If the function you are debugging is called “process” you would type this into the debugger window:
break process
continue
The program will run at full speed and then stop at the beginning of your function. It will refresh the display with the latest values of the registers and show you the source code of the first lines of your function. You can also look in the other window to see any output (or type any input if necessary).
From here you need to decide what to test and what to look for. Here are the most common commands you will use:
step
: this runs a single instruction and updates the displaynext
: this runs a single line and updates the display.The difference between these two mainly shows up when the next
instruction is a call
instruction. step
will take you into the
function being called so you can debug it. next
will run it at
full speed and stop when it returns.
continue
: resume running at full speed until it hits another
breakpoint or concludes. If your function is called multiple
times with different inputs, you may want to run continue
a
few times to get to the one you are trying to debug.
called multiple times, thisdb
: this refreshes the display. Sometimes useful if you have
been doing other commands and just want to see the registers
again.db memory watch $a0 32
: This command says that you want to
watch some bytes in memory at the location in register a0
and
you want to see 32 bytes. That region of memory will be added to
the display (you can type db
to see the update). To undo this,
use db memory unwatch $a0
.x/12dg $a0
: examine memory. In this example it will show you
12 decimal 64-bit values starting at the location in a0
. The
d
is for decimal and the g
is for giant (gdb’s name for
8-byte values). Type help x
to see all the options. The all
follow the same format but you can have it print different
numbers of values, different kinds (including strings,
characters, hex, etc.) and different sizes. This is extremely
useful when you are working on an array of values and want to
see the array contents.In addition, you will likely want to add additional breakpoints to
focus your debugging. Rather than stepping repeatedly, you can set a
breakpoint on an interesting line and then use continue
to
fast-forward to that line. To add a breakpoint at line 72 in the
current file you would use:
break 72
: add a breakpoint to line 72 of the current souce
file.Note that gdb will show a red !
in front of lines with
breakpoints. You can use db
to refresh the display and see that
the breakpoint has been added.
A useful technique is to put a breakpoint in the middle of a loop,
maybe after an interesting value has been computed or loaded from
memory. Then you can use continue
repeatedly to run another
iteration of the loop and stop at that line. This allows you to ask
high-level questions about the loop, such as “am I loading the right
values from memory”, although it requires you to first figure out
what the right values are.
The code for some assignments requires input from the keyboard. When the automatic tests are run, that input is normally supplied automatically, but when you are debugging it you are not connected to the testing system and may need to supply the inputs yourself.
Start by running the tests normally:
make
Presumably a test fails and execution stops. Scroll back up until you see where that test was launched (note that there may be multiple runs of your program with different inputs, so you want the most recent one that failed). You will see a line similar to this:
python3 lib/inout-stepper.py inputs/piledriver2.input inputs/piledriver2.expected qemu-riscv64 ./a.out
Here’s the breakdown:
python3 lib/inout-stepper.py
launches the test script (called
the stepper)inputs/piledriver2.input
is the file with the input that will
be fed to your program when it runsinputs/piledriver2.expected
is the file with the expected
output of your program (what the output will look like when it
is correct)qemu-riscv64 ./a.out
is the command that is actually used to
run your codeYou want to replicate the input that caused your program to fail, so
pick that part out, in this case it is the file
inputs/piledriver2.input
. Print out the contents of that file:
cat inputs/piledriver2.input
Then you can select the output from the terminal and copy it. Now when you run your code in the debugger, you can paste that input into the window with the program running (not the window with the debugger running) at the appropriate time.