let’s boot with riscv
Let’s boot some C code1 with riscv64 using qemu: riscy2
After about half a year of os development abstinence. I fell into the riscv-os-dev-rabbit-hole. Those risc, arm, riscv buzzwords are in my news bubble for a while. Now it’s time to get my hands dirty.
Contents
- RISC-V vs. ARM vs. ARM64 vs. AArch64
- Getting started - barebone tutorial
- Interact with the outside world
- Summary
RISC-V vs. ARM vs. ARM64 vs. AArch64
ARM64 and Aarch64 are both the same3: an implementation of the proprietary RISC architecture family ARM. RISC-V on the other side is an open-source architecture.
Those RISC’s are fairly modern4 and faster than CISC due to their nature of being reduced instead of complex. I’m not sure what this “faster” actually means while a CISC operation may be slower than a single RISC operation, but with RISC more operations are needed.
Getting started - barebone tutorial
I initialized my riscy bootloader using the os.dev risc-v bare bones tutorial containing everything needed for a hello world:
entry.S: Entry point and initial bootloader preparing everything before calling the main function in the C kernel code.kernel.c: C kernel printing hello world and reading from the keyboard by UART.linker.ld: Defines the memory layout of the resulting binary image that can be booted by qemu.
Build
In order to compile and link riscv code on a x86 machine, a cross-compiler version of gcc is needed. For Ubuntu and Fedora I could make out the package gcc-riscv64-linux-gnu containing all needed binaries.
Despite the different binaries names and the dropped -lgcc flag5, the commands look the same as in the Tutorial:
$ riscv64-linux-gnu-gcc -Wall -Wextra -c -mcmodel=medany kernel.c -o kernel.o -ffreestanding
$ riscv64-linux-gnu-gcc -c entry.S -o entry.o
$ riscv64-linux-gnu-ld -T linker.ld -nostdlib kernel.o entry.o -o kernel.elf
Let’s boot it via qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf -serial mon:stdio.

\o/
Interact with the outside world
Devices and their drivers are needed to interact with the outside world. Usually devices are defined by the underlying hardware. In our case Qemu and the used machine type virt defines what devices can be used. A driver is defined by the operating system and provides an abstraction of such devices. The Tutorial makes use of the UART device for providing a simple console. Let’s discover devices and drivers separately.
Devices
Qemu provides a list of supported devices for our machine type virt. We also can make out our UART device.
But how can we make use of them?
Drivers
Looking at the Tutorial, the driver consists of a magic address and… well actually nothing more than read and write from it:
unsigned char * uart = (unsigned char *)0x10000000;
void putchar(char c) {
*uart = c;
}
int main(){
putchar(*uart);
}
How can this make any sense?
Magic address
The address is actually well-defined by Qemu’s machine type for the UART device. As far as I understood it, the address-space of the devices are mapped into our address space and directly usable. Qemu can dump all those devices and addresses which can be made readable using the device-tree-compiler, as I found out via this riscv-from-scratch blog post.
And again, we can can make out our UART device with some additional information:
chosen {
bootargs = [00];
stdout-path = "/soc/uart@10000000";
};
...
uart@10000000 {
interrupts = <0x0a>;
interrupt-parent = <0x03>;
clock-frequency = "\08@";
reg = <0x00 0x10000000 0x00 0x100>;
compatible = "ns16550a";
};
- uart device emulates the ns16550A IC
- address spaces starts at magic address
0x10000000 - qemu uses this device for serial stdin/stdout
UART

The Universal Asynchronous Receiver-Transmitter is used for serial console. Its implementation is fairly simple.
RTC
After uart is working good enough, I wondered which other devices Qemu’s virt machinetype offers. Right in the beginning of the device-tree-map, a goldfish got my attention:
rtc@101000 {
interrupts = <0x0b>;
interrupt-parent = <0x03>;
reg = <0x00 0x101000 0x00 0x1000>;
compatible = "google,goldfish-rtc";
};
This is the google goldfish rtc device mapped at 0x101000. Qemu has a documentation about the rtc device and the linux kernel has a pretty small implementation.
I came up with something like this:
uint64_t time() {
volatile uint32_t *rtc = (uint32_t *) 0x101000;
uint64_t time_low = rtc[0];
uint64_t time_high = rtc[1];
return (time_high << 32 | time_low);
}
Which lead to the first ever nanosecond unix timestamp printed to the riscy console: 17074102021870520006.
Inline assembler
After having some convenience with basic drivers and an essential standard lib, it’s time to checkout riscv assembler itself using inline assembler.
Incrementing a number should be a simple start, right? Well, unlike x86’s inc, there is no pedant here. But addi gotcha covered: addi result, a, number where result and a are variables and number is a so-called imediate (literal) value. The inline syntax is a bit odd since it breaks the usual format we know from C/printf. This SO Post helped me to find the right syntax.
int inc(int n) {
volatile int result;
asm volatile(
"addi %0, %1, 1;"
: "=r" (result)
: "r" (n)
);
return result;
}
The keyword volatile is recommended to prevent compiler optimizations.
Summary
Although I learned a bit about RISC and the hardware qemu provides for it, I barely touched the differences between x86 and RISC. I wouldn’t wonder if the C “core” lib (despite the machine type specialities) is architecture-agnostic. But I really enjoyed starting with nothing and introduce concepts like strings or time. The bootloader is still a bit mysterious for me and I couldn’t figure out how to support VGA, but the UART driver was pretty straight forward.
Developing my first driver from scratch with my own findings, going through kernel code and tinkering around was awesome. It’s just a small goldfish but it meant a lot.
- Assembler won’t matter much as I use the bootloader from the bare-bones tutorial and start development with C for convenience.
- Together with Tch1b0
- Based on SO: Apple named it
arm64and all the othersaarch64. - RISC-V is from 2014 and not unlike the famous x86/i386 from 1978.
riscv64-linux-gnu-ld: cannot find -lgcc: no error¯\(ツ)/¯$ date -d @1707410202.187052000 +"%Y-%m-%d %H:%M:%S.%N": 2024-02-08 17:36:42.187052000