Programming in RISC-V assembly and Compiling C to RISC-V assembly

Moving things around

register to register

register to/from memory

addi x5,x0,0x11        # set x5 to 0x11
sw x5, 0x100(x0)       # store at address 0x100
lw x6, 0x100(x0)       # get from mem
sw x6, 0x104(x0)       # store to mem 0x104

Assignment

a = b + c

local variables are stored in registers
let x4 - a, x5 -b, x6 - c

global variables are stored in memory
let M[100] - a, M[104] - b, M[108] - c
let x4 - a, x5 - b, x6 - c

must move data from memory to register before perform addition

lw  x5, 0x104(x0)     # get b
lw  x6, 0x108(x0)     # get c
sw  x4, 0x100(x0)     # store to a

using offset,  base address points to the beginning of the array

ax[10]    ax starts at 0x100  base address

to get an element at ax[1]
let x3 - index, x4 - base, x5 - effective address, x5 - value

lw   x6,0(x5)        # get from memory

Loop

while condition
body

is transformed into sequences of instructions

loop:
test condition
jump if false to exit
body
jump loop
exit:

example

i = 0
while i < 10
i = i + 1

let x3 - i

addi x3,x0,0    # i = 0
loop:
bge  x3,x4, exit
j loop
exit:

Put it all together

sum all elements of an array

pseudo code

ax[5]

s = 0
i = 0
while i < 5
s = s + ax[i]
i = i + 1

let x3 - s, x4 - i, x5 - const 5, x6 - base, x7 - effective address,
x8 - offset, x9 - ax[i]

addi x3,x0,0       #  s = 0
addi x4,x0,0       #  i = 0
addi x8,x0,0       #  offset = 0
loop:
bge x4, x5, exit
lw  x9, 0(x7)     # get ax[i]
add x3, x3, x9    # s = s + ax[i]
addi x8, x8, 4    # next element
addi x4, x4, 1    # increment index
j loop
exit:

Compiling from high-level language to assembly

use "godbolt.org" (Compiler Explorer)  with "RISC_V rv32gc clang" compiler

the first simple program (C)

int a,b,c;

int main(){
a = 11;
}

risc-v assembly output, we put a at address 0x100

li     a1, 0x0100
li     a0, 11
sw     a0, 0(a1)

second program: if then else

int a,b,c;

int main(){
if(a > 0){
b = 22;
}
}

risc-v assembly output, a at 0x100, b at 0x104

li     a1, 0x100
lw     a1, 0(a1)
bge    a0, a1, LBB0_2
li     a1, 0x104
li     a0, 22
sw     a0, 0(a1)
LBB0_2:

call a function

to perform a "jump" to subroutine, we need a way to "come back" to where we call.
use a register to store "return address"

int twice(int x){
return( x + x);
}

int main(){
int a;
a = twice(2);
}

risc-v output is

j main
twice:                              # @twice
addi    sp, sp, -16          # create stack frame with 4 slots
sw      ra, 12(sp)           # first slot keeps return address
sw      s0, 8(sp)            # second slot keeps s0
addi    s0, sp, 16           # set s0 to this stack frame
sw      a0, -12(s0)          # store passing value (x) to slot 4
lw      a0, -12(s0)          # get x
add     a0, a0, a0           # x + x   return value in a0
lw      ra, 12(sp)           # restore return address
lw      s0, 8(sp)
addi    sp, sp, 16           # delete stack frame
ret
main:                                 # @main
addi    sp, sp, -16           # stack frame has 4 slots
sw      ra, 12(sp)
sw      s0, 8(sp)
li      a0, 2                 # pass number 2 in a0
call    twice                 # call twice  ***
sw      a0, -12(s0)           # put return value to a
li      a0, 0
lw      ra, 12(sp)
lw      s0, 8(sp)
addi    sp, sp, 16            # restore sp

When a function calls another function, they need some information: return address, return value, space for local variable etc.   These information are stored in "stack frame".  This chunk of memory is pointed to by a register "sp".

When "enter" a function, a stack frame is created, by decrement sp with the size of the stack (in this example, four slots, 16 bytes).

stack frame

sp -> 1) return address   <- s0
2) old s0
3) ...
4) x
sp' ->

Before the function ends, its stack frame is deleted, by increment sp, then the function return (using return address).  Please note that the main function also has its own stack frame.

*** Note: For "call twice" (which translate into two instructions and use "large address") you can use "jal ra,twice" which is the same as call but "twice" is "small address". The "ret" is actually "jalr x0,ra,0" which use "ra" that stored the return address.

How to pass a parameter to a function

This is a simplified explanation that fits this example.

In the above example  main call  twice(2), how the main sends 2 to the function twice?

Answer:  the caller (main) passes number 2 via a register (a0), also when a function return value it returns that in a0.

end

last update 22 Feb 2022