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.

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,

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.

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,

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.

References

  1. Stephen Smith's Blog
  2. Pranoy Dutta
  3. twilco: RISC-V From Scratch
  4. SiFive Software
  5. TinyEMU
  6. Spike RISC-V ISA Simulator
  7. RISC-V Proxy Kernel and Boot Loader