RISC-V Assembly
Selection
// Doing static (storage class) memory allocation here. The variables are
// global to the compilation unit / block / function, and has a lifetime
// that is the entire program.
static int x = x_init_val;
static int y = y_init_val;
static int z = z_init_val;
// inside some function
x++;
if (x > 0)
{
y++;
}
else
{
y--;
}
z = x + y;
.text
# inside some function
# Load the values of 'x' and 'y' via lw pseudoinstrn
lw t0, x # Load x into register t0
lw t1, y # Load y into register t1
# x++
addi t0, t0, 1 # Increment x (t0 = t0 + 1)
sw t0, x, t3 # Store the updated value of x
# if (x > 0)
bgtz t0, greater_than_zero # Branch if x > 0
# Else branch: y--
addi t1, t1, -1 # Decrement y (t1 = t1 - 1)
sw t1, y, t3 # Store the updated value of y.
# sw pseudoinstrn requires a temp register
# to be specified. Here, t3 is used throughout
j update_z # Jump to update_z to skip the 'then' branch
greater_than_zero:
# y++
addi t1, t1, 1 # Increment y (t1 = t1 + 1)
sw t1, y, t3 # Store the updated value of y
update_z:
# z = x + y
add t2, t0, t1 # t2 = x + y
sw t2, z, t3 # Store the value of z
.data
# Declare vars x,y,z
x: .word x_init_val
y: .word y_init_val
z: .word z_init_val
Iteration
// Doing static memory allocation here.
// C variable / array declarations not shown.
// inside some fn
for (i = 0; i < n; i++)
{
x[i] = y[i] + 1;
}
.text
# inside some fn
lw t0, n # Load n into register t0 (t0 = n)
li t1, 0 # Initialize i to 0 (t1 = i)
la t4, y # Get the starting address of y
la t5, x # Get the starting address of x
loop:
# Check if i < n
bge t1, t0, exit_loop # If i >= n, exit loop
# Load y[i] into t2
slli t3, t1, 2 # i * 4 (for byte offset)
add t6, t4, t3 # base + offset
lw t2, 0(t6) # Load y[i] into t2
# x[i] = y[i] + 1
addi t2, t2, 1 # t2 = y[i] + 1
add t6, t5, t3 # base + offset
sw t2, 0(t6) # Store the result in x[i]
# Increment i
addi t1, t1, 1 # i++
# Repeat the loop
j loop
exit_loop:
# Exit loop, to the rest of the function
.data
# Example array 'x' and 'y' with 5 elements each
x: .word 0, 0, 0, 0, 0
y: .word 1, 2, 3, 4, 5
# Number of elements, n = 5
n: .word 5
Function call
The C code used for both the caller and the callee below:
// Caller
void fn1() {
// z1 and z2 are globals
// x and y are local variables that use registers or stack
int x = 20;
int y = 30;
z1 = fn2(x, y);
x++;
y++;
z2 = x + y;
}
// Callee
int fn2(int p, int q) {
int temp = p + q;
return temp;
}
Caller
# Caller function (which could be a callee of some other fn)
fn1:
# Prologue: Save callee saved registers: s$ modified by fn1.
addi sp, sp, -4 # Make space on the stack (full descending)
sw s0, 0(sp) # Save whatever was in s0 as we modify s0 in fn1
# Initialize variables in registers
li s0, 20 # Load 20 into s0 (x = 20)
li t0, 30 # Load 30 into t0 (y = 30)
# Save caller saved registers: a$, ra, t$
addi sp, sp, -8 # Make space on the stack
sw t0, 0(sp) # Store t0 (y) to stack
sw ra, 4(sp) # Store ra
# Call fn2(20, 30), result in a0
mv a0, s0 # First argument (p = 20)
mv a1, t0 # Second argument (q = 30)
call fn2 # jal ra, fn2
sw a0, z1, t0 # Store the result (z1 = 50)
# Restore caller saved registers: a$, ra, t$
lw ra, 4(sp) # Restore ra from stack
lw t1, 0(sp) # Restore y (to t1 this time)
addi sp, sp, 8 # Restore sp
# x++ and y++
addi s0, s0, 1 # x++. x = 21
addi t1, t1, 1 # y++. y = 31
add t2, s0, t1 # x+y = 52
sw t2, z2, t3 # Store the result (z2 = 52)
# Epilogue: Restore callee saved regs: s$ modified by fn1
lw s0, 0(sp) # Restore s0 from stack
addi sp, sp, 4 # Restore the stack pointer
ret # Return to caller: jalr x0, ra, 0
Note
Could reuse t0 instead of using t1, t2, t3 above, but no benefit as
there is no more function call before ret.
Alignment
The RISC-V ABI requires sp to stay 16-byte aligned. The example here
doesn't follow that perfectly, for simplicity.
Callee
# Callee function.
# Arguments: p in a0, q in a1.
# Returns: temp (p + q) in a0
fn2:
# Prologue: Save callee saved registers: s$ to stack
# No need to save caller saved registers as fn2 does
# not call any function (i.e., is not a caller)
addi sp, sp, -4 # Make space on the stack
sw s0, 0(sp) # Save s0 to stack
add s0, a0, a1 # temp = p + q
mv a0, s0 # Return temp in a0
# Epilogue: Restore callee saved registers: s$ from stack
lw s0, 0(sp) # Restore s0 from stack
addi sp, sp, 4 # Restore sp
jr ra # Return to caller: jalr x0, ra, 0 (ret)
Function considerations
- For local variables, memory is allocated and deallocated automatically in the stack, or they are kept in registers.
- Modifying globals (
z1,z2) from a function should be avoided as it goes against modularity of functions.- Done here just for illustration.
- There is no need to save and restore
ra(caller saved) each time before calling a function.- Sufficient to save it once at the beginning of a function and restore it at the end.
Self-study
- Read up more about the RV calling convention — how data bigger than 32 bits are passed and returned, how a large number of parameters are dealt with, etc.!
- Read up / experiment with how local variables are allocated in a function, stack frame, frame pointer, etc.
- godbolt.org is a great tool. Choose the correct language (C) and the compiler/ISA (RISC-V rv32gc clang).
Think about it
What if the callee function is farther than the range of jal?