8 minutes
My RISC-V development environment for programming and reverse engineering purposes
π§ First, what is RISC-V?
RISC-V is an open standard instruction set architecture (ISA) that began as a project at UC-Berkeley in 2010. and is based on established reduced instruction set computer (RISC) principles. Unlike most other ISA designs, RISC-V is provided under open source licenses that do not require fees to use.
The applications of this new architecture are multiple, here is a non-exhaustive list of examples:
- The European Union has launched a supercomputer project based on the RISC-V architecture, known as the European Processor Initiative (EPI)
- The Institute of Software Chinese Academy of Sciences announced in June 2021, that it is planning to build 2,000 RV64GC-based laptops by the end of 2022
- A Low Power RISC-V IoT Processor Optimized for Artificial Intelligence Applications
- Ali-Baba annouces open-source RISC-V based processors
The increasing presence of this architecture on highly critical installations led me to question the security aspect.
Although I will soon invest in a development board to facilitate my research, when I wanted to start tinkering with this architecture I did not have one at my disposal. So I had to set up a development environment on my non-RISC-V machine, and this is the topic of this first publication about RISC-V systems.
I’d like to show you the setup I’ve built to ease the development of programs in assembly on a RISC-V architecture when you don’t have a RISC-V machine at home.
- How I will structure my setup.
- Download a Debian RISC-V 64 bits image
- Using QEMU to emulate a RISC-V machine
- Creating a shared directory between host and guest machine
- Installing debugging and compilation tools on my host machine
- Demonstration
- Conclusion
- To go further
βοΈ How I will structure my setup
I started by mapping out what I was going to need:
- A Debian 64-bit RISC-V image since it is a linux distribution I know quite well. I could also have decided to work with Fedora since there is a RISC-V version of it.
- A folder where I will store all my useful scripts to perform redundant actions (assembly code compilation, setup launch…)
- A shared folder between the host machine, and the guest machine.
- A folder where I store the assembly programs I write.
Here is how I built the directory tree of my project:
./RISC-V_Setup
βββ image
β βββ Debian image
βββ scripts
β βββ All the useful scripts (run.sh, compile.sh...)
βββ share
β βββ Share directory between HOST (my computer) and GUEST machine (RISC-V emulator)
βββ src
β βββ Directory where I write RISC-V assembly code
π΅οΈ Download a Debian RISC-V 64 bits image
I will create an image
directory and download a pre-made RISC-V 64 Debian Image.
mkdir image
wget https://gitlab.com/api/v4/projects/giomasce%2Fdqib/jobs/artifacts/master/download?job=convert_riscv64-virt" -O ./image/debian-rv64.zip
We now only have to unzip it in the ./image
folder.
The default credentials are debian:debian and root:root
π Using QEMU to emulate a RISC-V machine
A small point of vocabulary to avoid confusion: hereafter we will call my main machine the host machine and the RISC-V machine emulated with QEMU the guest machine.
-> We emulate a 64 bits version of the RISC-V processor using QEMU. In order to do this we will need the package qemu-system-riscv64
which you can install with sudo apt-get install qemu-system-riscv64
For the purpose of this demonstration, we will use a 64-bit version of the RISC-V architecture, but it is however possible to emulate a 32-bit RISC-V processor with QEMU using the
qemu-system-riscv32
package
qemu-system-riscv64 \
-machine virt \
-cpu rv64 \
-m 1G \
-device virtio-net-device,netdev=net \
-netdev user,id=net,hostfwd=tcp::2222-:22 \
-device virtio-blk-device,drive=hd \
-drive file=./Image/artifacts/overlay.qcow2,if=none,id=hd \
-bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \
-kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
-append "root=LABEL=rootfs console=ttyS0" \
-nographic \
-fsdev local,security_model=passthrough,id=fsdev0,path=./share \
-device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare
I think you may need some further explanation of this script:
-cpu rv64
-> We select a RISC-v 64 bits CPU
-m 1G
-> We allocate 1GB of RAM to the guest machine.
This value depends on your needs and the amount of RAM you are able to allocate to the guest machine.
-netdev user,id=net,hostfwd=tcp::2222-:22:
-> This line makes port 22 accessible as localhost:2222. This lets us forward SSH connections.
-bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \
-> If needed, replace with the location of your OpenBSI. But make sure itβs the same configuration.
-append "root=LABEL=rootfs console=ttyS0" \
-> The append line adds extra options to the kernel command line in UNIX derivatives.
-kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
-> the path of my U-Boot image.
-nographic
-> With this option, you can totally disable graphical output so that QEMU is a simple command line application.
-fsdev local,security_model=passthrough,id=fsdev0,path=./share \
-device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare
-> These two lines allow us to create a common folder between the host machine and the guest machine.
π Creating a shared directory between Host and Guest machines
I have seen some people choosing to use scp
to communicate between their host machine and their guest machine. I propose a different method by using a shared folder between the two machines.
-> We run this script on the guest machine
SHARED_FOLDER="/mnt/share"
# Create shared folder
mkdir ${SHARED_FOLDER}
mount -t 9p -o trans=virtio,version=9p2000.L hostshare ${SHARED_FOLDER}
Now the contents of ./share
on the host machine and /mnt/share
on the guest machine will be the same
βοΈ Installing debugging and compilation tools
for reverse engineering purposes, I sometimes need a debugger to analyze the behavior of a binary. I am familiar with GDB and Radare2, and I will show you how to use them in this case.
We will start by installing RISC-V GNU toolchain as it contains a compiler (GCC) and our favorite debugger (GDB), as well as other very useful tools, such as an assembler and a linker. Installation instructions can be found here
GDB
We can now debug a RISC-V binary with the command :
$ ~ riscv64-unknown-elf-gdb binary
GCC
If you want to compile a binary with gcc for the RISC-V architecture, here is the command to use
$ ~ riscv64-unknown-elf-gcc -ggdb -static -o binary binary.c
Radare2
Radare2 is pretty cool when it comes to working with RISC-V binaries since it has built-in RISC-V support.
You can analyze a binary simply by running it as usual:
$ ~ r2 ./riscv-binary
β¨ Demonstration
-> Here is a quick reminder of how my setup is organized. :
./RISC-V_Setup
βββ share
β βββ Shared directory between HOST (my computer) and GUEST machine (RISC-V emulator)
βββ src
β βββ Directory where I write RISC-V assembly code
βββ ...
β βββ ...
So we will write our assembly code in the ./src
folder, and compile it into the ./share
folder to be able to access it from the guest machine. To do this we will first open a file program.s
which will be a basic RISC-V assembler program that displays a “Hello world” message on the standard output (We must start somewhere :D ) :
#
# 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"
Next we will have to compile this code for a 64-bit RISC-V architecture from our host machine. For this we will use several tools contained in the RISC-V GNU Compiler Toolchain.
- We use
riscv64-linux-gnu-as
to assemble the program. riscv64-linux-gnu-ld
to link the object file into an executable file.
I wrote a bash script (stored in ./script
) to automatically do the job:
#!/bin/bash
ASSEMBLY_DIR="$(dirname $0)/../src"
SHARE_DIR="$(dirname $0)/../share"
riscv64-linux-gnu-as -march=rv64imac -o ${SHARE_DIR}/program.o ${ASSEMBLY_DIR}/program.s
riscv64-linux-gnu-ld -o ${SHARE_DIR}/program ${SHARE_DIR}/program.o
rm ${SHARE_DIR}/program.o
chmod +x ${SHARE_DIR}/program
We launch it :
$ ~ ./scripts/compile.sh
We now have an executable binary named program
in /mnt/share
on the guest machine.
-> We can test if it works :
debian@debian:/mnt/share$ ./program
Hello World!
and it’s working!
We can now debug the binary on the host machine with riscv64-unknown-elf-gdb
or with Radare2
.
π Conclusion
So here is a setup I made to be able to work more easily under a RISC-V architecture in a linux environment. My goal was to facilitate the task of those who would like to develop in RISC-V assembler without being able to get a RISC-V machine, or to do reverse engineering. To do so, I published my complete setup with installation instructions here.
If you have any question or corrections to suggest for this article, I can be reached by mail at the following address: r0g3r5@protonmail.Com
You can also follow me on Twitter (even if I am very (very) little active) : @Rog3rSm1th
I hope I helped you, or taught you something :D