RISC-V Setup
Software setup required on Windows and Linux to run RISC-V assembly programs. Code is written in a mix of C and RISC-V assembly and compiled using SiFive's cross compiler toolchains to produce executables for SiFive hardware. There are a number of ways to run the executable in emulators like QEMU, Spike and TinyEMU. The program is debugged using GDB by making a remote (localhost) connection to an instance running in one of the emulators. SiFive's Freedom Studio is a all-encompassing IDE that takes care of such details by providing a GUI for push-button execution of the code-compile-debug pipeline.
Install Software and Setup Local Environment
Download the following software and put them in ~/riscv/ and extract them there.
- Tools from SiFive
- Freedom-E SDK
- Freedom Studio
- Prebuilt RISC‑V GCC Toolchain and Emulator
- GNU Embedded Toolchain
- OpenOCD
- QEMU
- Spike Disassembler
- TinyEMU
- tinyemu-2019-12-21.tar.gz
- diskimage-linux-riscv-2018-09-23.tar.gz
- Spike
- Device Tree Compiler
- GNU RISC-V Toolchain (If you've installed SiFive's, you don't need this).
- RISC-V Proxy Kernel
In ~/riscv, create a directory called bin and symlink all the binaries from the bin directories of the downloaded software in the bin directory you just created. Make sure the first argument to ln -s is the absolute path.
cd riscv; mkdir bin
ln -s ~/riscv/riscv64-unknown-elf-gcc-8.3.0-2020.04.0-x86_64-linux-ubuntu14/bin/* bin/
In ~/.bashrc, add a RISCV variable and the path ~/riscv/bin to the system PATH
export RISCV=~/riscv
export PATH=$PATH:$RISCV/bin
In this way you can access your riscv tools without sprinkling software and links all over the file system.
Test a sample C program from the Freedom-E SDK; run it using the RISC-V QEMU emulator (the one from SiFive)
qemu-system-riscv32 -nographic -machine sifive_e -kernel freedom-e-sdk/software/hello/debug/hello.elf
which should print "Hello world!" to the console/terminal.
Compile and Run RISC-V Assembly Source Code
Freedom Studio IDE
Here we call a function coded in RISC-V assembly from a C program. Open up the IDE,
- Hit the SiFive (logo) button in the horizontal pane at the top.
- Select target: qemu-sifive-e31
- Select example program: hello
- Hit Finish (at the bottom of the window)
Once the project completes loading, in the left pane, right click the src directory to add a new file, say f23.S (the extension S must be in upper case because the bundled Makefile looks for *.S files).
From your C main() in hello.c, call the function f23() which is defined in RISC-V assembly in f23.S
hello.c
#include <stdio.h>
#include <stdint.h>
extern int f23(int x);
int main()
{
printf("f23(1) returned: %d\n", f23(1));
printf("f23(0) returned: %d\n", f23(0));
}
f23.S
.section .text
.global f23
.type f23, @function
f23:
bne a0, zero, RET23
beq a0, zero, RET0
RET23:
addi a0, zero, 23
RET0:
addi a0, a0, 0
ret
Build using the little 'hammer' button in the top pane and hit 'debug' to start the code in a debugger. Then hit 'continue' (in the top pane again) to finish running the program and see the output in the IDE's console window.
TinyEMU RISC-V Emulator
Download TinyEMU and a customized Linux disk image.
- tinyemu-2019-12-21.tar.gz
- diskimage-linux-riscv-2018-09-23.tar.gz
Extract them, and in tinyemu, make and make install to build the executable temu, and at a symlink in $RISCV/bin
cd tinyemu-2019-12-21
make
Write out this RISC-V program in a file named helloworld.s
# Risc-V Assembler program to print "Hello World!"
# to stdout.
#
# a0-a2 - parameters to linux function services
# a7 - linux function number
.global _start # Provide program starting address to linker
# Setup the parameters to print hello world
# and then call Linux to do it.
_start: addi a0, x0, 1 # 1 = StdOut
la a1, helloworld # load address of helloworld
addi a2, x0, 13 # length of our string
addi a7, x0, 64 # linux write system call
ecall # Call linux to output the string
# Setup the parameters to exit the program
# and then call Linux to do it.
addi a0, x0, 0 # Use 0 return code
addi a7, x0, 93 # Service command code 93 terminates
ecall # Call linux to terminate the program
.data
helloworld: .ascii "Hello World!\n"
compile the code with
riscv64-unknown-elf-as -march=rv64imac -o helloworld.o helloworld.s
riscv64-unknown-elf-ld -o helloworld helloworld.o
and, out of curiosity, dump it with
riscv64-unknown-elf-objdump -d helloworld
to see that the executable contains
helloworld: file format elf64-littleriscv
Disassembly of section .text:
00000000000100b0 <_start>:
100b0: 00100513 li a0,1
100b4: 00001597 auipc a1,0x1
100b8: 02058593 addi a1,a1,32 # 110d4 <__DATA_BEGIN__>
100bc: 00d00613 li a2,13
100c0: 04000893 li a7,64
100c4: 00000073 ecall
100c8: 00000513 li a0,0
100cc: 05d00893 li a7,93
100d0: 00000073 ecall
With coding out of the way, copy the executable to /tmp/
cp helloworld /tmp
so that, from TinyEMU's shell, with the host's /tmp mounted in its filesystem, we have access to helloworld.
Then we set up the disk image,
cd diskimage-linux-riscv-2018-09-23/
add the line to root_9p-riscv64.cfg to point to the kernel
kernel: "kernel-riscv64.bin"
and run the emulator
temu root_9p-riscv64.cfg
which drops you into a new prompt; the guest kernel. In there,
mount -t 9p /dev/root /mnt
to mount the host's /tmp at the guest's /mnt. In /mnt you'll find helloworld, so run it there
cd /mnt
./helloworld
which prints out "Hello World!".
To shut down the emulator you might need to kill it with
killall temu
from another host-terminal.
RISC-V QEMU
A RISC-V executable can be run using QEMU;
qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -S -kernel a.out
The -gdb flag is meant to start the GDB server to which we can connect from an instance of GDB running on the host.
Spike RISC-V ISA Simulator
Install the following items,
- Device Tree Compiler
- GNU RISC-V Toolchain (If you've installed SiFive's, you don't need it here)
- Spike
- RISC-V Proxy Kernel
Install the compiler, and GNU toolchain,
apt install device-tree-compiler
git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
cd riscv-gnu-toolchain
mkdir build
cd build
../configure --prefix=$RISCV --with-arch=rv64i
make
then Proxy Kernel,
git clone https://github.com/riscv/riscv-pk
cd riscv-pk
mkdir build
cd build
../configure --prefix=$RISCV --host=riscv32-unknown-elf
make
make install
and finally Spike
git clone https://github.com/riscv/riscv-isa-sim
cd riscv-isa-sim
mkdir build
cd build
../configure --prefix=$RISCV --enable-histogram
make
make install
Write some code; hello.c and f23.S that we used in the IDE previously, and compile
riscv64-unknown-elf-gcc -g -O0 -o hello hello.c f23.s
Note that we have used the debugging flags for GCC; -g -O0 because we will want to demonstrate use of GDB.
Test that Spike is working by running
spike --isa=RV64IMAC riscv-pk/build/pk src/hello
which should print
bbl loader
hello world
f23 returned 23
Debugging With GDB and Spike
Create a file spike.lds with the following content
OUTPUT_ARCH( "riscv" )
SECTIONS
{
. = 0x10010000;
.text : { *(.text) }
.data : { *(.data) }
}
Compile code with GCC, with some extra flags
riscv64-unknown-elf-gcc -g -Og -T spike.lds -nostartfiles -o hello hello.c f23.s
That this works, is, somewhat, proven by this output, where the first three lines are as expected, but a segfault follows, I don't yet know why.
Run the executable hello
spike --isa=RV64IMAC riscv-pk/build/pk src/hello
which prints
bbl loader
hello world
f23 returned 23
z 0000000000000000 ra 0000000000000000 sp 000000007f7e8b50 gp 0000000000000000
tp 0000000000000000 t0 0000000000000000 t1 000000007f7e8a00 t2 0000219000050018
s0 0000000000000000 s1 0000000000000000 a0 0000000000000000 a1 000000001001dd50
a2 0000000000000010 a3 0000000000000000 a4 0000000000000000 a5 0000000000000000
a6 000000000000000a a7 0000000000000040 s2 0000000000000000 s3 0000000000000000
s4 0000000000000000 s5 0000000000000000 s6 0000000000000000 s7 0000000000000000
s8 0000000000000000 s9 0000000000000000 sA 0000000000000000 sB 0000000000000000
t3 0000000000000000 t4 fffffffffffffffd t5 0000000000000064 t6 0000000000000000
pc 0000000000000000 va 0000000000000000 insn ffffffff sr 8000000200046020
User fetch segfault @ 0x0000000000000000
Now we delve into setup for debugging:
Run spike telling it to listen for OpenOCD
spike --rbb-port=9824 -m0x10000000:0x20000 hello
which will immediately tell you
Listening for remote bitbang connection on port 9824.
Create a file spike.cfg with the following content
interface remote_bitbang
remote_bitbang_host localhost
remote_bitbang_port 9824
set _CHIPNAME riscv
jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x10e31913
set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME riscv -chain-position $_TARGETNAME
gdb_report_data_abort enable
init
halt
Then, in another shell, run OpenOCD (Open On-Chip Debugger) with that configuration
openocd -f spike.cfg
(Recollect that OpenOCD was installed at the outset when we picked SiFive's tools).
OpenOCD responds back with several lines where one (near the end) tells you that it's ready:
Info : Listening on port 3333 for gdb connections
In a third shell instance, run GDB (the RISC-V flavored one from SiFive),
riscv64-unknown-elf-gdb hello
and then when you are dropped into the (gdb) prompt, connect to the remote target
(gdb) target remote localhost:3333
which tells me (not you)
Remote debugging using localhost:3333
0x0000000000000000 in ?? ()
(The zero hex address and double question marks shouldn't be there. If you're able to figure out what is wrong with my code, and write yours properly, you should see a valid address and a function name. This probably is also the reason for the segfault.)
Lastly, in the second shell that we had opened in this sequence of operations, where OpenOCD is waiting, we will see new output saying
Info : accepting 'gdb' connection on tcp/3333
In the gdb shell (the third) you can debug the code as usual, setting breakpoints, stepping through, printing variables and so on.