Solving RISC-V Kata locally, the not-so-easy way


Disclaimer: donaldsebleung is a community moderator on Codewars but not an official employee. Any views expressed in this article solely belong to donaldsebleung and should not be considered as official Codewars stance by any means.

Have you heard? RISC-V assembly is now supported on Codewars! In case you haven’t heard of Codewars, it’s like HackerRank or LeetCode, but with:

  • Proper support for test-driven development (TDD) with production-level unit testing frameworks such as JUnit for Java, or Chai for Node.js
  • A diverse range of code challenges (Kata) covering language fundamentals, data structures and algorithms (DSA), mathematics, advanced language features, theorem proving … you name it
  • An equally diverse community
  • Support for 50+ distinct programming languages at the time of writing

If you’re a developer looking to improve your programming skills through practice, you should definitely give Codewars a try 😉

With that out of the way, you might be wondering, “How do I set up a development environment for solving RISC-V Kata locally?” For most programming languages, it’s simple and straightforward – just install the appropriate development tools on your local machine and you’re good to go. For example, to solve Node.js Kata locally, you might install on your computer:

  • Node.js and npm
  • Chai
  • (Optional) VSCode or another IDE of choice, plus Node.js specific plugins/extensions to enhance the developer experience

Then copy over the solution file and unit tests from the relevant Kata and start writing code straight away.

But with RISC-V assembly, it’s not that simple. Assembly programs are tightly coupled with the CPU architecture and operating system they’re targeting, and in 2022, chances are the CPU architecture for the computer (or smartphone) you’re reading this article on isn’t RISC-V – it’s probably x86_64/amd64 (for most consumer PCs) or aarch64 (for smartphones and Apple Silicon Macs). So, if at all possible, how do you run RISC-V assembly programs on your local device?

It turns out there are a few possible ways to do this. You could (in no particular order of preference):

  1. Run a container built for RISC-V with QEMU user mode emulation. This is the easiest way, and also how Codewars does it. On Windows and macOS, you’d do this with Docker Desktop, and on Linux, you could (instead) do this with Docker Engine / Podman directly after installing qemu-user-static on Ubuntu, or the equivalent package(s) for other distributions
  2. Purchase a RISC-V system-on-chip (SoC) such as the SiFive HiFive Unmatched board, load Linux on it, connect to a serial console, then (build and) install GCC and Cgreen on the board plus (optionally) a suitable text editor like Emacs Nano Vim to ease the development process
  3. As some sort of middle-ground between (1) and (2), do the same as (2), but on a fully emulated virt board with QEMU full system emulation

As mentioned, (1) is the easiest and also resembles exactly the Codewars execution environment, but it’s already well-documented in so we won’t cover it here. (2) is the most challenging, exciting way of doing it but requires purchasing a separate board which most solvers would probably consider overkill for solving Codewars Kata. We’ll thus cover (3) in this article, which is nearly as exciting as (2) but a bit simpler and does not require additional hardware investment.


A proper Linux environment. If on Windows or macOS, run a full-blown Linux virtual machine (VM) with a hypervisor such as VirtualBox or VMware, and follow the rest of this article in the VM. On Windows, there’s also the option of WSL2 (WSL1 won’t work at all, I bet) but YMMV.

The reference distribution is Ubuntu 22.04. If you’re using an alternative Linux distribution, modifications to the instructions presented in this article may be necessary (or just run an Ubuntu VM anyway). If you want to try running QEMU directly on Windows / macOS, you’re on your own 😉

Either way, you should be familiar with Linux and able to perform simple troubleshooting. If you encounter an error like gpg2: command not found halfway, we totally expect you to figure out to sudo apt install gnupg2 instead of complaining 😉

Installing dependencies, setting up the virt board to boot Ubuntu for RISC-V

Main article: RISC-V – Ubuntu Wiki

Refresh repository metadata:

$ sudo apt update

Install the required dependencies:

$ sudo apt install qemu-system-misc opensbi u-boot-qemu qemu-utils
  • qemu-system-misc: Provies full system emulation for multiple architectures, including RISC-V
  • opensbi: A supervisor binary interface (SBI) implementation. SBI on RISC-V is analogous to BIOS on consumer desktops / laptops
  • u-boot-qemu: Das U-Boot universal bootloader
  • qemu-utils: Miscellaneous QEMU-related utilities

Now fetch a compressed pre-installed Ubuntu server image for SiFive HiFive Unmatched, which is also applicable to our virt board:

$ wget


$ unxz ubuntu-22.04.1-preinstalled-server-riscv64+unmatched.img.xz

We can now boot our virt board:

$ qemu-system-riscv64 
    -machine virt 
    -m 1024 
    -smp 4 
    -bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf 
    -kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf 
    -device virtio-net-device,netdev=eth0 
    -netdev user,id=eth0 
    -drive file=ubuntu-22.04.1-preinstalled-server-riscv64+unmatched.img,format=raw,if=virtio

Since this is a fully emulated RISC-V board with no hardware acceleration, our Ubuntu RISC-V guest takes a while to boot and fully initialize. While we’re waiting, let’s look at some of the options in the above command:

  • qemu-system-riscv64: RISC-V (64-bit) system emulator
  • -machine virt: Emulate the virt board, which does not correspond to any real-world RISC-V board but is good enough for our purposes
  • -nographic: Disable graphical output
  • -m 1024: Allocate 1024MB of memory to the VM
  • -smp 4: Emulate 4 CPU cores
  • -bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf: Use OpenSBI
  • -kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf: Despite the confusing name (-kernel), use Das U-Boot universal bootloader
  • -device virtio-net-device,netdev=eth0, -netdev user,id=eth0: add virtual network interface card (NIC) eth0; use user-mode networking (-netdev user) so root privileges are not required
  • -drive file=ubuntu-22.04.1-preinstalled-server-riscv64+unmatched.img,format=raw,if=virtio: use the pre-installed server image for our virtual disk

Once our board is fully initialized, login with username ubuntu and password ubuntu. You will be asked to change the password on first login.

Now that we’ve logged in to the system, any further commands in this article should be executed on our virt board unless otherwise specified.

Installing GCC, building and installing Cgreen

Codewars uses the distribution-provided GCC to assemble the RISC-V solution and compile tests, and Cgreen 1.6.0 for unit testing. To replicate this setup, we’ll install GCC with apt along with Cgreen build dependencies, and build Cgreen 1.6.0 from source.

Refresh repository metadata:

$ sudo apt update

Install GCC and build dependencies for Cgreen (g++, make, cmake):

$ sudo apt install -y gcc g++ make cmake

Now let’s git clone Cgreen 1.6.0:

$ git clone --recurse-submodules --branch 1.6.0

Technically we don’t need --recurse-submodules for Cgreen which recursively fetches dependencies on Git repos, but it’s needed for other C unit testing frameworks like Criterion, and it doesn’t hurt to include this option.

Enter cgreen/:

$ pushd cgreen/

Build the codebase (this can take a while):

$ make

It is imperative you do not attempt to parallel build, e.g. with -j$(nproc). I tried it – it appears to build fine to completion, but after installing it, compiling anything involving Cgreen unit tests leads to weird linker errors that disappear once you re-build Cgreen with a single thread and re-install.

Now install:

$ sudo make install

And update dynamic linker bindings:

$ sudo ldconfig

Let’s leave the Cgreen source tree:

$ popd

Finally, a quick test to confirm that Cgreen 1.6.0 is correctly installed:

$ cat > main.c << EOF

int main(int argc, char **argv) {
  return run_test_suite(create_test_suite(), create_text_reporter());
$ gcc main.c -lcgreen -o main
$ ./main

The expected output:

Running "main" (0 tests)...
Completed "main": No assertions.

Add (inspired by Multiply): an example Kata setup

With GCC and Cgreen installed, we can start solving RISC-V Kata locally. To avoid spoiling existing Kata, we’ll set up a hypothetical Kata known as “Add”, where you have to fix a syntax error to get an add(a, b) function to work. The add() function adds two numbers together and returns the result.

The Codewars setup (roughly) consists of the following files:

  • solution.s: the solver’s solution in RISC-V assembly
  • solution_tests.c: Cgreen unit tests (sample / submission tests) for validating the solver’s solution
  • tests.c: entry point for tests that Codewarriors (solvers, Kata authors and translators alike) need not worry about

RISC-V on Codewars does not support a preloaded section at the time of writing (2022-08-13). There’s also a Codewars reporter for printing Codewars output format in unit tests instead of human-friendly text output, but that’s irrelevant for developing our solution locally.

The command(s) used to compile and execute the code is roughly:

$ gcc solution.s solution_tests.c tests.c -lcgreen -o tests
$ ./tests

With tests.c:


TestSuite *solution_tests();

int main(int argc, char **argv) {
  return run_test_suite(solution_tests(), create_text_reporter());

Let’s start with a broken solution. Can you spot the error? Save this as solution.s:

.globl add
  addw a0, a1

Here are some tests in solution_tests.c:


int add(int, int);

BeforeEach(Add) {}
AfterEach(Add) {}

Ensure(Add, should_work_for_fixed_tests) {
  assert_that(add(3, 5), is_equal_to(8));
  assert_that(add(-7, 2), is_equal_to(-5));
  assert_that(add(11, -4), is_equal_to(7));
  assert_that(add(-2, -2), is_equal_to(-4));

TestSuite *solution_tests() {
  TestSuite *suite = create_test_suite();
  add_test_with_context(suite, Add, should_work_for_fixed_tests);
  return suite;

Try to compile the Kata:

$ gcc solution.s solution_tests.c tests.c -lcgreen -o tests

Whoops – our solution failed to assemble. Here’s what you should see:

solution.s: Assembler messages:
solution.s:3: Error: illegal operands `addw a0,a1'

I’ll give you some time to figure out the error.

Now replace our broken solution with a working one in solution.s:

.globl add
  addw a0, a0, a1

Compile and run the Kata now – the tests should pass:

$ gcc solution.s solution_tests.c tests.c -lcgreen -o tests
$ ./tests

The output:

Running "solution_tests" (1 test)...
  "solution_tests": 4 passes in 22ms.
Completed "solution_tests": 4 passes in 32ms.

Finally, we can do random tests as well, like every other supported language on Codewars. Here’s an updated solution_tests.c:


int add(int, int);

BeforeEach(Add) {}
AfterEach(Add) {}

Ensure(Add, should_work_for_fixed_tests) {
  assert_that(add(3, 5), is_equal_to(8));
  assert_that(add(-7, 2), is_equal_to(-5));
  assert_that(add(11, -4), is_equal_to(7));
  assert_that(add(-2, -2), is_equal_to(-4));

Ensure(Add, should_work_for_random_tests) {
  for (int i = 0; i < 100; ++i) {
    int a = rand() % 100, b = rand() % 100;
    int expected = a + b;
    int actual = add(a, b);
    assert_equal_with_message(actual, expected, "Expected add(%d, %d) to equal: %d, instead got: %d", a, b, expected, actual);

TestSuite *solution_tests() {
  TestSuite *suite = create_test_suite();
  add_test_with_context(suite, Add, should_work_for_fixed_tests);
  add_test_with_context(suite, Add, should_work_for_random_tests);
  return suite;

Compile and run the Kata again:

$ gcc solution.s solution_tests.c tests.c -lcgreen -o tests
$ ./tests

The output:

Running "solution_tests" (2 tests)...
  "solution_tests": 104 passes in 49ms.
Completed "solution_tests": 104 passes in 55ms.

Wrapping up, next steps

Power down the board:

$ sudo systemctl poweroff

That’s it – I hope you enjoyed the article, and happy sparring!

Here are some possible next steps, not necessarily related to Codewars:

  • Purchase a real, physical RISC-V board and follow along this article with it, adapting the instructions as necessary
  • We loaded an out-of-the-box (OOTB) Linux distribution in this article, which is fun in its own right, but it would be more exciting to compile our own kernel and (minimal) userspace and load that onto our virtual (or physical) board. E.g. download the latest stable kernel from and cross-compile it for RISC-V, do the same for BusyBox, then boot our board with them. The RISC-V docs provides a brief outline of the process, but you have to fill in the gaps which aren’t trivial from my limited experience
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Hosting your self hosted runners on GitHub Codespaces

Next Post

How to check if a number is a power of two for O(1)

Related Posts