Programming
In this section, we begin by discussing program flow and instructions that support high-level languages like C. Then, we explore how to translate the high-level constructs into RISC-V assembly code.
Program Flow
Like data, instructions are stored in memory. Each instruction is 32 bits (4 bytes) long, as we will discuss later, so consecutive instruction addresses increase by four. For example, in the code snippet below, the addi instruction is located in memory at address 0x538 and the next instruction, lw, is at address 0x53C.

Program Counter
The program counter — also called the PC — keeps track of the current instruction. The PC holds the memory address of the current instruction and increments by four after each instruction completes so that the processor can read or fetch the next instruction from memory. For example, when addi instruction is executing, PC is 0x538. After addi instruction completes, PC increments by four to 0x53C and the processor fetches the lw instruction at that address.
Logical, Shift, and Multiply Instructions
The RISC-V architecture defines a variety of logical and arithmetic instructions. We introduce these instructions briefly here because they are necessary to implement higher-level constructs.
Logical Instructions
RISC-V logical operations include and, or and xor. These each operate bitwise on two source registers and write the result to a destination register, as shown in Figure 6.2. Immediate versions of these logical operations, andi, ori, and xori, use one source register and a 12-bit sign-extended immediate.

The
andinstruction is useful for clearing or masking bits. (e.g., forcing bits to 0). See more from NUS CG2111A Notes.The
orinstruction is useful for set bits in a register (e.g., for bit to be 1). See more from NUS CG2111A Notes.A logical NOT operation can be performed with
xori s8, s1, -1. Remember that -1 (0xFFF) is sign-extended to 0xFFFFFFFF (all 1's). XOR with all 1's inverts all the bits, sos8will get the one's complement ofs1.
Shift Instructions
Shift instructions shift the value in a register left or right, dropping bits off the end. RISC-V shift operations are
sll: shift left logicalslr: shift right logicalsra: shift right arithmetic
These shifts specify the shift amount in the second source register. Immediate versions of each instruction are also available (slli, srli, and srai), where the amount to shift is specified by a 5-bit unsigned immediate.
Figure 6.3 shows the assembly code and resulting register values for slli, srli, and srai when shifting by an immediate value. s5 is shifted by the immediate amount, and the result is placed in the destination register.

As discussed in the previous notes,
shifting a value by N is equivalent to multiplying it by
shifting a value right by N is equivalent to dividing it by
arithmetic right shifts divide two's complement numbers, while
logical right shifts divide unsigned numbers
Multiply Instructions
As we have seen in previous notes, multiplying two N-bit numbers produces a 2N-bit product. The RISC-V architecture provides various multiply instructions that result in 32- or 64-bit products. These instructions are not part of RVI321 but are included in teh RVM (RISC-V multiply/divide) extension.
The multiply instruction (
mul) multiplies two 32-bit numbers and produces a 32-bit product.mul s1, s2, s3multiplies the values ins2ands3and places the least significant 32 bits of the product ins1; the most significant 32 bits of the product are discarded.Three versions of the "multiply high" operation exist:
mulh,mulhsu, andmulhu. These instructions put the high 32 bits of the multiplication result in the destination register.mulh(multiply high signed signed) treats both operands as signed.mulhsu(multiply high signed unsigned) treats the first operand as signed and the second as unsigned.mulhu(multiply high unsigned unsigned) treats both operands as unsigned.
Using a series of two instructions — one of the "multiply high" instructions followed by the mul instruction — will place the entire 64-bit result of the 32-bit multiplication in the two registers designated by the user. For example, the following code multiplies 32-bit signed numbers in s3 and s5 and places the 64-bit product in t1 and t2. That is {t1, t2} = s3 x s5.
Branching
Branch instructions modify the flow of the program so that the processor can fetch instructions that are not in sequential order in memory. They modify the PC to skip over sections of code or to repeat previous code.
Conditional branch instructions perform a test and branch only if the test is TRUE.
Unconditional branch instructions, called jumps, always branch.
Conditional Branches
The RISC-V instruction set has six conditional branch instructions, eahc of which take two source registers and a label indicating where to go.
beq(branch if equal) branches when the values in the two source registers are equal.bne(branch if not equal) branches when they are unequal.blt(branch if less than) branches when the value in the first source register is less than the value in the second.bge(branch if greater than or equal to) branches when the first is greater than or equal to the second.bltandbgetreat the operands as signed numbers, whilebltuandbgeutreat the operands as unsigned (the fifth and sixth conditional branch instructions in RISC-V)
Code Example 6.12 illustrates the use of beq. When the program reaches the branch if equal instruction (beq), the value in s0 is equal to the value in s1, so the branch is taken. Thus, the next instruction executed is the add instruction just after the label called target. The addi and sub instructions between the branch and the label are not executed.
Code Explanation
Assembly code uses labels to indicate instruction locations in the program. A label refers to the instruction just after the label. When the assembly code is translated into machine code, these labels correspond to instruction addresses (as will be discussed later).
RISC-V assembly labels are followed by a colon (
:).Most programmers indent their instructions but not the labels to help make labels stand out.
In Code Example 6.13, the branch is not taken because s0 is equal to s1, and the code continues to execute directly after the bne (branch if not equal) instruction. All instructions in this code snippet are executed. (Including the instruction under target)
Unconditional Branches
A program can jump — that is, unconditionally branch — using one of three instructions:
jump (
j): jumps directly to the instruction at specified label. See Code Example 6.14jump and link (
jal): will be discussed later in function calls.jump register (
jr): will be discussed later in function calls.
Code Explanation
After the
jinstruction executes, this program unconditionally continues executing theaddinstruction at the labeltarget. All of the instructions between the jump and the label are skipped.
Conditional Statements
This section shows how to translate the high-level constructs (if/else, and switch/case) into RISC-V assembly language.
If Statements
Code Example 6.15 shows how to translate an if statement into RISC-V assembly code.
Code Explanation
The assembly code for the if statement tests the opposite condition of the one in the high-level code.
If/else Statements
Code Example 6.16 shows an example if/else statement.
Code Explanation
The assembly code tests for (
apples ≠ oranges). If that opposite condition is TRUE,bneskips the if block and executes the else block. Otherwise, the if block executes and finishes with a jump (j) past the else block.
Switch/case Statements
A case statement is equivalent to a series of nested if/else statements. Code Example 6.17 shows two high-level code snippets with the same functionality: they calculate whether to dispense $20, $50, or $100 from an ATM depending on the button pressed.
Code Explanation
The RISC-V assembly implementation is nearly the same as the high-level code snippet.
Getting Loopy
While Loops
The while loop in Code Example 6.18 determines the value of x such that . It executes seven times, until pow=128.
Code Explanation
Like if/else statements, the assembly code for while loops tests the opposite condition of the one in the high-level code.
Do-while Loops
Code Example 6.19 illustrates such a loop.
Code Explanation
Unlike the previous example, the branch checks the same condition as in the high-level code.
For Loops
Code Example 6.20 adds the numbers from 0 to 9.
Code Explanation
The for loop in the high-level code checks the
<condition to continue, so the assembly code checks the opposite condition, ≥, to exit the loop.
Arrays
Figure 6.4 shows a 200-element array of integer scores stored in memory. Each consecutive element address increases by 4, the number of bytes in an integer.

Code Example 6.21 is a grade inflation algorithm that adds 10 points to each of the scores. The code for initializing the scores array is not shown.
Code Explanation
Assume that
s0is initially0x174300A0, the base address of the array.The index into the array is a variable (
i) that increments by 1 for each array element, so we multiply it by 4 before adding it to the base address.
Bytes and Characters
In RISC-V assembly, the load byte (lb), load byte unsigned (lbu), and store byte (sb) instructions access individual bytes in memory.
lbsign-extends the byte to fill the entire 32-bit registerlbuzero-extends the byte to fill the entire 32-bit registersbstores the least significant byte of the 32-bit register into the specified byte address in memory.
All these three instructions are illustrated in Figure 6.5, with the base address, s4, being 0xD0.

Code Explanation
lbu s1, 2(s4)loads the byte at memory address0xD2into the least significant byte ofs1and fills the remaining register bits with 0.lb s2, 3(s4)loads the byte at memory address0xD3into the least significant byte ofs2and sign-extends the byte into the upper 24 bits of the register.sb s3, 1(s4)stores the least significant byte ofs3(0x9B) into memory byte address at 0xD1; it replaces0x42with0x9B. The more significant bytes ofs3are ignored.
In C, the null character (0x00) signifies the end of a string. For example, Figure 6.6 shows the string "Hello!" (0x48 65 6C 6C 6F 21 00) stored in memory.

Function Calls
When a function calls another, the calling function, the caller, and the called function, the callee, must agree on where to put the arguments and the return value. In RISC-V programs,
the caller conventionally places up to eight arguments in registers
a0toa7before making the function call,the callee places the return value in register
a0before finishing.
By following this convention, both functions know where to find the arguments and return value, even if the caller and callee were written by different people.
Function Calls and Returns
RISC-V uses the jump and link instruction (jal) to call a function and jump register instruction (jr) to return from a function. Code Example 6.22 shows the main function calling the simple function. main is the caller and simple is the callee.
Code Explanation
The instruction
jal simpleperforms two tasks:it jumps to the target instruction located at
simple(0x0000051C)it stores the return address, the address of the instruction after
jal(in this case, 0x00000304) in the return address register (ra).
Input Arguments and Return Values
As we have seen earlier, by RISC-V convention, functions use a0 to a7 for input arguments and a0 for the return value. In Code Example 6.23, the function diffofsums is called with four arguments and returns one result. result is a local variable, which we choose to keep in s3. (Saving and restoring registers will be discussed soon.)
Code Explanation
According to RISC-V convention, the calling function,
main, places the function arguments from left to right into the input registers,a0toa7, before calling the function. The called function,diffofsums, stores the return value in the return register,a0.When a function with more than eight arguments is called, the additional input arguments are placed on the stack, which we discuss next.
The Stack
The stack (We have learned this idea of stack in NUS CS1010!) is memory that is used as scratch space — that is, to save temporary information within a function. Each function may allocate stack space to store local variables and to use as scratch space, but the function must deallocate it before returning.
The RISC-V stack grows down in memory. That is, the stack expands to lower memory addresses when a program needs more scratch space.
Stack Pointer
The stack pointer, sp (register x2), is an ordinary RISC-V register that, by convention, points to the top of the stack. sp starts at a high memory address and decrements to expand as needed. Figure 6.7(b) shows the stack expanding to allow two more data words of temporary storage. To do so, sp decrements by 8 to become 0xBEFFFAE0.

One of the important uses of the stack is to save and restore registers that are used by a function. Recall that a function should calculate a return value but have no other unintended side effects. In particular, a function should not modify any registers besides a0, the one containing the return value.
thisThe diffofsums function in Code Example 6.23 violates thsi rule because it modifies t0, t1, and s3. If main had been using these registers before the call to diffofsums, their contents would have been corrupted by the function call.
To solve this problem, a function saves registers on the stack before it modifies them and then restores them from the stack before it returns. Specifically, it performs the following steps:
Makes space on the stack to store the values of one or more registers
Stores the values of the registers on the stack
Executes the function using the registers
Restores the original values of the registers from the stack
Deallocates space on the stack
Code Example 6.24 shows an improved version of diffofsums that saves and restores t0, t1, and s3.
Figure 6.8 shows the stack before, during, and after a call to the diffofsums function from Code Example 6.24.

Code Explanation
The stack starts at
0xBEF0F0FC.diffofsumsmakes room for three words on the stack by decrementing the stack pointerspby 12.It then stores the current values held in
t0,t1, ands3in the newly allocated space. It executes the rest of the function, changing the values in these three registers.At the end of the function,
diffofsumsrestores the values of these registers from the stack, deallocates its stack space, and returns.When the function returns,
a0holds the result, but there are no other side effects:t0,t1,s3, andsphave the same values as they did before the function call.
The stack space that a function allocates for itself is called its stack frame. diffofsums's stack frame is three words deep. The principle of modularity tells us that each function should access only its own stack frame, not the frames belonging to other functions.
Saving a register value on the stack is called pushing a register onto the stack. Restoring the register value from the stack is called popping a register off of the stack.
Preserved Registers
Code Example 6.24 assumes that all of the used registers (t0, t1, and s3) must be saved and restored. If the calling function does not use those registers, the effort to save and restore them is wasted. To avoid this waste, RISC-V divides registers into preserved and nonpreserved categories.
As we have seen from the RISC-V registers set before,
The preserved registers are
s0tos11(hence their name, saved),spandra.The nonpreserved registers, also called scratch registers, are
t0tot6(hence their name, temporary) anda0toa7, the argument registers.
A function can change the nonpreserved registers freely (no need to save and restore) but must save and restore any of the preserved registers that is uses.
Code Example 6.25 shwos a further improved version of diffofsums that saves only s3 on the stack, t0 and t1 are nonpreserved registers, so they need not be saved.
Because a callee function may freely change any nonpreserved registers, the caller must save any nonpreserved registers containing essential information before making a function call and then restore these registers afterward. For these reasons, preserved registers are also called callee-saved and nonpreserved registers are called caller-saved.
Table 6.3 summarizes which registers are preserved.

The stack above the stack pointer is automatically preserved, as long as the callee does not write to memory addresses above sp.
Last updated