For users new to the RISC-V ISA but familiar with the C or C++ programming languages, Matt Godbolt’s Compiler Explorer is a great tool for interactively seeing how C/C++ constructs gets compiled to the RISC-V ISA. In Compiler Explorer, to compile to the RISC-V ISA, select “riscv32 clang (trunk)” as the compiler.
Since the assembly code generated by Compiler Explorer is stripped of debug symbols, program entry points (there is no main
function) etc., it is fairly straight-forward to adapt the generated assembly code to be able to run it in Ripes.
In the following segment, a C-style factorial function will be adapted to be run in Ripes.
Initially, we define our factorial function as being:
int arg = 7;
int fact(int n) {
int c;
int result = 1;
for(c = 1 ; c <= n ; c++)
result = result*c;
return result;
}
Where we define the argument for the function as a global variable. This will yield a symbol that is stored in the static data segment of the output assembly file.
This initially produces the following RISC-V assembly file (With compiler flag -O3):
fact(int): # @fact(int)
addi sp, sp, -16
sw ra, 12(sp)
sw s1, 8(sp)
sw s2, 4(sp)
mv s2, a0
addi a0, zero, 1
blt s2, a0, .LBB0_3
addi a0, zero, 1
mv s1, zero
.LBB0_2: # =>This Inner Loop Header: Depth=1
addi s1, s1, 1
mv a1, s1
call __mulsi3
bne s2, s1, .LBB0_2
.LBB0_3:
lw s2, 4(sp)
lw s1, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret
arg:
.word 7 # 0x7
This is already pretty good! Compiler Explorer helpfully colors the different segments of both the C source code and the RISC-V assembly code to indicate which code-segments are generated from what.
The biggest issue is call __mulsi3
. This is an attempt to call a built-in runtime function for calculating the product of two registers (a0
and a1
) - For more info on runtime function calls, refer to compiler-rt. Since Ripes supports RV32M - the ISA extension for integer multiplication and division, such a call may be replaced with theinstruction mul a0 a0 a1
. The input registers are a0, a1
given that these are the function argument/return registers, which was already prepared for a call to __mulsi3
as per the RISC-V calling convention, and the output register is a0
, a function return value register.
For specifying variables to be loaded into simulator memory, we must specify that arg
resides in the .data
segment. Thus, we need to move the arg
specification to the top of the file, and discern between the .data
and .text
segments of the file. The top segment of the file should therefore look like this:
.data
arg: .word 7
.text
...
Finally, we need to run our factorial function with arg
as an argument. To do this, we write a small main function as the entry code:
main:
lw a0, arg
jal ra, fact
jal zero, end
as well as write a label at the end of our file - end:
, which we will jump to, to exit the program.
And thus the following code will be runnable in Ripes:
.data
arg:
.word 7
.text
main:
lw a0, arg
jal ra, fact
jal zero, end
fact: # @fact(int)
addi sp, sp, -16
sw ra, 12(sp)
sw s1, 8(sp)
sw s2, 4(sp)
mv s2, a0
addi a0, zero, 1
blt s2, a0, .LBB0_3
addi a0, zero, 1
mv s1, zero
.LBB0_2: # =>This Inner Loop Header: Depth=1
addi s1, s1, 1
mv a1, s1
mul a0, a0, a1
bne s2, s1, .LBB0_2
.LBB0_3:
lw s2, 4(sp)
lw s1, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret
end:
When this is run, register a0
will contain the value 5040
after execution.