Lab 3: RISC-V Assembly

Deadline: Friday, September 16, 11:59:59 PM PT

Goals

  • Get familiar with using the Venus simulator
  • Get an idea of how to translate C code to RISC-V.
  • Write your first RISC-V Program

Setup

In your labs directory, pull the files for this lab with:

git pull starter main

If you run into a merge conflict, run the following commands:

git rm -f tools/venus.jar tools/logisim-evolution.jar
git commit

If you get an error like the following:

fatal: 'starter' does not appear to be a git repository
fatal: Could not read from remote repository.

make sure to set the starter remote as follows:

git remote add starter https://github.com/61c-teach/fa22-lab-starter.git

and run the original command again.

Still in your labs directory, run the following command to download the newest version of some tools we may need:

bash tools/download_tools.sh

Introduction to Assembly

In this course so far, we have dealt mostly with C programs (with the .c file extension), used the gcc program to compile them to machine code, and then executed them directly on your computer or hive machine. Now, we're shifting our focus to the RISC-V assembly language, which is a lower-level language much closer to machine code. We can't execute RISC-V code directly because your computer and the hives are built to run machine code from other assembly languages --- most likely x86 or ARM.

For the next few labs, we will work with several RISC-V assembly files, each of which has a .s file extension. To run these, we will be using Venus, an educational RISC-V assembler and simulator. You can run Venus locally from your own terminal or on the Venus website, and the following instructions will guide you through the steps to set it up. Though you may find using the web editor easier to use for this lab, please go through these instructions for local setup regardless: these steps will also set up other infrastructure needed for future projects and labs.

Assembly/Venus Basics

To get started with Venus, please take a look at "The Editor Tab" and "The Simulator Tab" in the Venus reference. We recommend that you read this whole page at some point, but these sections should be enough to get started.

Warning: For the following exercises, please make sure your completed code is saved on a file on your local machine. Otherwise, we will have no proof that you completed the lab exercises.


Exercise 1: Connecting your files to Venus

You can "mount" a folder from your local device onto Venus's web frontend, so that edits you make within the browser Venus editor are reflected in your local file system, and vice versa. If you don't do this step, files created and edited in Venus will be lost each time you close the tab, unless you copy/paste them to a local file.

This exercise will walk you through the process of connecting your file system to Venus, which should save you a lot of trouble copy/pasting files between your local drive and the Venus editor.

If for some reason this feature ends up not working for you (it's relatively new, and there's a chance there might still be bugs), then for the rest of this assignment, wherever it says to open a file in Venus, you should copy/paste the contents into the Venus web editor, and manually copy/paste those changes back to your local machine.

Here's what you need to do:

  1. If you don't already have your labs repo cloned on your local machine, open a terminal on your local machine and clone it.
    • Windows users should clone outside WSL (Git Bash is recommended). Note that Windows paths are also accessible from WSL (e.g. C:/Users/oski/cs61c/labs/ in Windows is /mnt/c/Users/oski/cs61c/labs/ in WSL).
  2. cd into your labs repo folder, and run java -jar tools/venus.jar . -dm. This will expose your lab directory to Venus on a network port.
    • You should see a big "Javalin" logo.
    • If you see a message along the lines of "port unable to be bound", then you can specify another port number explicitly by appending --port PORT_NUM to the command (for example, java -jar tools/venus.jar . -dm --port 6162 will expose the file system on port 6162).
  3. Open https://venus.cs61c.org in your web browser (Chrome or Firefox are recommended). In the Venus web terminal, run mount local labs (if you chose a different port, replace "local" with the full URL, such as http://localhost:6162). This connects Venus to your file system.
    • In your browser, you may see a prompt saying Key has been shown in the Venus mount server! Please copy and paste it into here.. You should be able to see a key in the most recent line of your local terminal output; just copy and paste it into the dialog.
  4. Go to the "Files" tab. You should now be able to see your labs directory under the labs folder.
  5. Navigate to lab03, and make sure it works by hitting the Edit button next to fib.s. This should open in the Editor tab.
    • If you make any changes to the file in the Editor tab, hitting command-s on a Mac and ctrl-s on Windows/Linux will update your local copy of the file. To check if the save was successful, open the file on your local machine to see if it matches what you have in the web editor (unfortunately no feedback message has been implemented yet).
    • Note: If you make any changes to a file in your local machine, if you had the same file open in the Venus editor, you'll need to reopen it from the "Files" menu to get the new changes.
  6. To make it so that the file system will attempt to remount automatically whenever you close and reopen Venus, enable "Save on Close" in the Settings pane (again in the Venus tab). This will make the Venus web client attempt to locate the file system exposed by running Venus locally, and will pop up an error saying that it couldn't connect to the server if it doesn't see it running. If this happens, just follow the above steps to manually remount the file system.

Once you've got fib.s open, you're ready to move on to Exercise 2!


Exercise 2: Translating from C to RISC-V

In this exercise, we are going to walk through translating a C program to a RISC-V program. The following program will print out the nth Fibonacci number.

#include <stdio.h>

int n = 9;

// Function to find the nth Fibonacci number
int main(void) {
    int curr_fib = 0, next_fib = 1;
    int new_fib;
    for (int i = n; i > 0; i--) {
        new_fib = curr_fib + next_fib;
        curr_fib = next_fib;
        next_fib = new_fib;
    }
    printf("%d\n", curr_fib);
    return 0;
}

Let's break down how we'll translate this step-by-step. First, we need to define the global variable n. In RISC-V global variables are declared under the .data directive. This represents the data segment. It will look like this:

.data
n: .word 9
  • n is the name of the variable
  • .word means that the size of the data is one word
  • 9 is the value that is assigned to n

Let's move on to initalizing curr_fib and next_fib

.text
main:
    add t0, x0, x0 # curr_fib = 0
    addi t1, x0, 1 # next_fib = 1

Here we have added the .text directive. Everything under this directive is our code.

Remember that x0 always holds the value 0.

We don't need to do anything to declare new_fib (we don't declare variables in RISC-V).

Next, let's get to the loop. We'll start with setting up the loop variables. The following code will set i to n

la t3, n # load the address of the label n
lw t3, 0(t3) # get the value that is stored at the adddress denoted by the label n

You can think of the code above as doing something along the lines of

t3 = &n;
t3 = *n;

We have a new instruction here la. This instruction loads the address of a label. The first line essentially sets t3 to be a pointer to n. Next, we use lw to dereference t3 which will set t3 to the value stored at n.

Now, you're probably thinking, "Why can't we directly set t3 to n?" In the .text section, there is no way that we can directly access n. (Think about it. We can't say add t3, n, x0. The arguments to and must be registers and n is not a register.) The only way that we can access it is by obtaining the address of n. Once we obtain the address of n, we need to dereference it which can be done with lw. lw will reach into memory at the address that you specify and load in the value stored at that address. In this case, we specified the address of n and added an offset of 0.

Let's get down to the loop now. First, we'll create the outer structure below:

fib:
    beq t3, x0, finish # exit loop once we have completed n iterations
    ...
    ...
    addi t3, t3, -1 # decrement counter
    j fib # loop
finish:

The first line (fib:) is a label that we will use to jump back to the beginning of the loop.

The next line (beq t3, x0, finish) specifies our terminating condition. Here, we will jump to another label, finish, once t3 (which is representing i) reaches 0.

The next line (addi t3, t3, -1) decrements i at the end of the loop body. It's important to do this at the end because i is used in the loop body. If we updated it right after beq, then it would not have the correct value in the loop body.

The next instruction jumps back to the start of the loop.

Now, let's add in the loop body.

fib:
    beq t3, x0, finish # exit loop once we have completed n iterations
    add t2, t1, t0 # new_fib = curr_fib + next_fib;
    mv t0, t1 # curr_fib = next_fib;
    mv t1, t2 # next_fib = new_fib;
    addi t3, t3, -1 # decrement counter
    j fib # loop
finish:

Nothing special here. The corresponding C lines are written in the comments.

Let's print out the nth Fibonacci number!

finish:
    addi a0, x0, 1 # argument to ecall to execute print integer
    addi a1, t0, 0 # argument to ecall, the value to be printed
    ecall # print integer ecall

Printing is a system call. You'll learn more about these later in the semester, but a system call is essentially a way for your program to interact with the Operating System. To make a system call in RISC-V, we use a special instruction called ecall. To print out an integer, we need to pass two arguments to ecall. The first argument specifies what we want ecall to do (in this case, print an integer). To specify that we want to print an integer, we pass a 1. The second argument is the integer that we want to print out.

In C, we are used to functions looking like ecall(1, t0). In RISC-V, we cannot pass arguments in this way. To pass an argument, we need to place it in an argument register (a0-a7). When the function executes, it will look in these registers for the arguments. (If you haven't seen this in lecture yet, you will soon). The first argument should be placed in a0, the second in a1, etc.

To set up the arguments, we placed a 1 in a0 and we placed the integer that we wanted to print in a1.

Next, let's terminate our program! This also requires ecall

addi a0, x0, 10 # argument to ecall to terminate
ecall # terminate ecall

In this case, ecall only needs one argument. Setting a0 to 10 specifies that we want to terminate the program.

And there you have it! Here's our full program!

.data
n: .word 9

.text
main:
    add t0, x0, x0 # curr_fib = 0
    addi t1, x0, 1 # next_fib = 1
    la t3, n # load the address of the label n
    lw t3, 0(t3) # get the value that is stored at the adddress denoted by the label n
fib:
    beq t3, x0, finish # exit loop once we have completed n iterations
    add t2, t1, t0 # new_fib = curr_fib + next_fib;
    mv t0, t1 # curr_fib = next_fib;
    mv t1, t2 # next_fib = new_fib;
    addi t3, t3, -1 # decrement counter
    j fib # loop
finish:
    addi a0, x0, 1 # argument to ecall to execute print integer
    addi a1, t0, 0 # argument to ecall, the value to be printed
    ecall # print integer ecall
    addi a0, x0, 10 # argument to ecall to terminate
    ecall # terminate ecall

Exercise 3: Familiarizing yourself with Venus

Here is how you can run fib.s in Venus:

  1. Open fib.s into the Venus editor. If you were unable to mount the filesystem in Exercise 1, then you can copy/paste fib.s from your local machine into the Venus editor directly.
  2. Click the "Simulator" tab and click the "Assemble & Simulate from Editor" button. If you've previously assembled the code, use the yellow "Re-assemble from Editor" button instead. This will prepare the code you wrote for execution. If you click back to the "Editor" tab, your simulation will be reset.
  3. In the simulator, to execute the next instruction, click the "step" button.
  4. To undo an instruction, click the "prev" button. Note that undo may or may not undo operations performed by ecall, such as exiting the program or printing to console.
  5. To run the program to completion, click the "run" button.
  6. To reset the program from the start, click the "reset" button.
  7. The contents of all 32 registers are on the right-hand side, and the console output is at the bottom.
  8. To view the contents of memory, click the "Memory" tab on the right. You can navigate to different portions of your memory using the dropdown menu at the bottom.

Action Item

Open fib.s in Venus and answer the following questions.

  1. At what address is n stored in memory? Hint: Step through the code and look at the contents of the registers
    Answer `0x10000000`
  2. How would you compute the 20th fibonacci number?
    Answer Change the value stored at n to 20

Exercise 4: Array Practice

Consider the discrete-valued function f defined on integers in the set {-3, -2, -1, 0, 1, 2, 3}. Here's the function definition:

f(-3) = 6
f(-2) = 61
f(-1) = 17
f(0) = -38
f(1) = 19
f(2) = 42
f(3) = 5

Implement the function in discrete_fn.s in RISC-V, with the condition that your code may NOT use any branch and/or jump instructions! Make sure that your code is saved locally. We have provided some hints in case you get stuck.

Make sure that you only write to the t and a registers. If you use other registers, strange things may happen (you'll learn about why soon).

Hint 1

All of the output values are stored in the output array which is passed to f through register a1. You can index into that array to get the output corresponding to the input.

Hint 2

You can access the values of the array using lw.

Hint 3

lw requires that the offset is an immediate value. When we compute the offset for this problem, it will be stored in a register. Since we cannot use a register as the offset, we can add the value stored in the register to the base address to compute the address of the index that we are interested in. Then we can perform a lw with an offset of 0.

In the following example, the index is stored in t0 and the pointer to the array is stored in t1. The size of each element is 4 bytes. In RISC-V, we have to do our own pointer arithmetic, so (1) we need to multiply the index by the size of the elements of the array. (2) Then we add this offset to the address of the array to get the address of the element that we wish to read and then (3) read the element.

slli t2, t0, 2 # step 1 (see above)
add t2, t2, t1 # step 2 (see above)
lw t3, 0(t2) # step 3 (see above)

Testing

To test your function, open discrete_fn_tester.s and run it through the simulator.

You can also test your code locally. We'll be using this local version in the autograder, so make sure that the test passes locally before you submit to the autograder.

java -jar tools/venus.jar lab03/discrete_fn_tester.s

Exercise 5: Factorial

In this exercise, you will be implementing the factorial function in RISC-V. This function takes in a single integer parameter n and returns n!. A stub of this function can be found in the file factorial.s.

The argument that is passed into the function is located at the label n. You can modify n to test different factorials. To implement, you will need to add instructions under the factorial label. There is an recursive solution, but we recommend that you implement the iterative solution. You can assume that the factorial function will only be called on positive values with results that won't overflow a 32-bit two's complement integer.

At the start of the factorial call, the register a0 contains the number which we want to compute the factorial of. Then, place your return value in register a0 before returning from the function.

Make sure that you only write to the t and a registers. If you use other registers, strange things may happen (you'll learn about why soon).

Also, make sure you initialize the registers you are using! Venus might show that the registers are initially 0, but in real life they can contain garbage data. Make sure you set the register values that you will be using to some defined number before using them.

Testing

To test your code, you can make sure your function properly returns that 3! = 6, 7! = 5040 and 8! = 40320.

To test your function, open factorial.s and run it through the simulator.

You can also test your code locally. We'll be using this local version in the autograder, so make sure that the test passes locally before you submit to the autograder.

java -jar tools/venus.jar lab03/factorial.s

Exercise 6

Please fill out this short survey about your experience with the lab. Your responses will be used to improve the lab in the future. The survey will be collecting your email to verify that you have submitted it, but your responses will be anonymized when the data is analyzed. Thank you!


Transitioning to More Complex RISC-V Programs

In the future, we'll be working with more complex RISC-V programs that require multiple files of assembly code. To prepare for this, we recommend looking over the Venus reference.


Submission

Save, commit, and push your work, then submit to the Lab 3 assignment on Gradescope.