It was while watching Bryan Cantrill’s presentation “The Soul of a New Machine”1 that my interest for RISC-V was piqued. I vaguely remember looking at RISC-V a while ago but at the time hardware wasn’t readily available unless you had an FPGA to run it on. Nowadays there’s ample choice of both 32-bit and 64-bit hardware to buy.

No RISC, no fun

First off, a very brief introduction to RISC-V and the different extensions which are available. This’ll help navigate the swaths of cores out there.

RISC-V is a 32/64/128-bit instruction set architecture (ISA) which is completely open and free to use and implement by anyone, originally developed in Berkeley. As the name implies it’s a reduced instruction set computer (RISC) as opposed to the complex instruction set computer (CISC) architectures. There are two “frozen” standards (i.e. ratified and final): the unprivileged/userland ISA and the privileged ISA. On top of this anyone is free to develop their own extensions provided these work without conflict with the base ISA. It’s this feature of RISC-V that makes it an attractive ISA to get familiar with because of this modularity you can build small 32-bit embedded systems (RV32E) or a general purpose server computer (RV64IMAFD, or RV64G in short). Knowledge of this ISA is going to become very helpful I reckon. This could also allow for developing extensions which provide mitigations against certain types of attacks (such as ROP) right in the ISA itself. I do have concerns however whether these extensions will create a fragmented ecosystem.

Currently the most common extensions are:

Name Description
M Integer multiplication and division
A Atomic instructions
F Single-Precision floating-point
D Double-precision floating-point
C Compressed instructions

Here’s the complete list of extensions along with the version and status. Also I’d recommend reading this general description of RISC-V: Instruction Sets Want to be Free. As well as this presentation from Berkeley Architecture Research, with a focus on QEMU support for RISC-V (and some mentions of Spike, see below).

Nitty gritty basics

I merely wanted to get some hands on experience and learn the RISC-V assembly and I didn’t want to spend any time/money (yet) on buying a hardware board or on setting up a complete SDK or RTOS build environment (although Zephyr does seem like an interesting project to look into at some point).

Turns out, there’s actually a really neat simulator developed by the RISC-V foundation called Spike. This simulator defaults to RV64IMAFDC (or RV64GC) and it works really well with the RISC-V proxy kernel (pk) to run your code (provided you’re not doing any fancy IO operations).

pk is really a “lightweight execution environment for statically linked ELF binaries”, which is just perfect for my use case.

If you’re on macOS you can install the required toolchain using homebrew:

brew tap riscv/riscv
brew install riscv-tools

On OpenBSD you can install the toolchain from packages:

pkg_add riscv-elf-newlib

spike and pk should be build manually, though the upstream repositories provide the build instructions for OpenBSD too!

With a simple hello.c invoke the cross-compiler (I use -g to keep all interesting bits for inspection and debugging):

riscv64-unknown-elf-gcc -g hello.c -o hello

and run the binary on the proxy kernel in the Spike simulator:

nazca ~ % file hello
hello: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped
nazca ~ % spike /usr/local/Cellar/riscv-pk/master/bin/pk hello
bbl loader
Hello RISC-V!
nazca ~ %

Spike can do so much more than simply running the proxy kernel, please checkout the upstream documentation for additional features such as testing new instructions and interactive debugging.

While there are plenty of examples of using QEMU’s UART, I’m sticking to a trivial sample using stdio for the sake of simplicity:

.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 and run it with spike:

nazca ~ % riscv64-unknown-elf-as -march=rv64imac -o hello.o hello.s
nazca ~ % riscv64-unknown-elf-ld -o hello hello.o
nazca ~ % spike /usr/local/Cellar/riscv-pk/master/bin/pk hello
bbl loader
Hello World!

With a test environment set up and the reference card in hand it’s time to dig in!


  1. if you haven’t read the book with the same title, do yourself a big favour and do read it. ↩︎