RVOS

contributed by <leoyehx>

RVOS is a 32-bit real-time multitasking operating system built from scratch. It is designed to run on QEMU with RISC-V.

Development Environment

$ system_profiler SPHardwareDataType

Hardware:

    Hardware Overview:

      Model Name: MacBook Air
      Model Identifier: Mac16,13
      Model Number: MW1M3LL/A
      Chip: Apple M4
      Total Number of Cores: 10 (4 performance and 6 efficiency)
      Memory: 16 G
      ...
      
$ system_profiler SPSoftwareDataType

Software:

    System Software Overview:

      System Version: macOS 15.5 (24F74)
      Kernel Version: Darwin 24.5.0
      Boot Volume: Macintosh HD
      Boot Mode: Normal
      Computer Name: macBook air
      User Name: Leo Yeh (leoyeh)
      Secure Virtual Memory: Enabled
      System Integrity Protection: Enabled
      ...

Prerequisite

Since we plan to build an operating system based on QEMU and RISC-V, we need to download the necessary development tools.

$ brew install llvm lld qemu

Then add llvm binutils to the PATH

$ export PATH="$PATH:$(brew --prefix)/opt/llvm/bin"
$ which llvm-objcopy
/opt/homebrew/opt/llvm/bin/llvm-objcopy

Boot Process

When we press the power button, the CPU executes the first instruction at a default address. It then begins loading the BIOS ROM (or UEFI firmware).

BIOS and UEFI

BIOS or UEFI then checks and initializes all hardware components, including memory, keyboard, display card, etc. After the hardware check, it loads the bootloader.

Supervisor Binary Interface

The Supervisor Binary Interface (SBI) is an API for OS kernels, but defines what the firmware provides to an OS. A famous SBI implementation is OpenSBI. In QEMU, OpenSBI starts by default, performs hardware-specific initialization, and boots the kernel.


OpenSBI v1.5.1
   ____                    _____ ____ _____
  / __ \                  / ____|  _ \_   _|
 | |  | |_ __   ___ _ __ | (___ | |_) || |
 | |  | | '_ \ / _ \ '_ \ \___ \|  _ < | |
 | |__| | |_) |  __/ | | |____) | |_) || |_
  \____/| .__/ \___|_| |_|_____/|____/_____|
        | |
        |_|

Platform Name             : riscv-virtio,qemu
Platform Features         : medeleg
Platform HART Count       : 1
Platform IPI Device       : aclint-mswi
Platform Timer Device     : aclint-mtimer @ 10000000Hz
...

Boot Loader

In this phase, the bootloader reads the OS kernel and sets the kernel parameters. It then jumps to the kernel's entry point.







Boot



node1

Power On



node2

BIOS/UEFI



node1->node2





node3

Boot Loader



node2->node3





node4

OS Kernel



node3->node4





Linker Script

A linker script is a file which defines the memory layout of executable files. Based on the layout, the linker assigns memory addresses to functions and variables.

ENTRY(boot)

SECTIONS {
    . = 0x80200000;

    .text :{
        KEEP(*(.text.boot));
        *(.text .text.*);
    }

    .rodata : ALIGN(4) {
        *(.rodata .rodata.*);
    }

    .data : ALIGN(4) {
        *(.data .data.*);
    }

    .bss : ALIGN(4) {
        __bss = .;
        *(.bss .bss.* .sbss .sbss.*);
        __bss_end = .;
    }

    . = ALIGN(4);
    . += 128 * 1024; /* 128KB */
    __stack_top = .;
}

If we refer to the memory layout of C programs, it will be very clear.







Layout



Layout


Low Address

text

read-only data

initialized data

uninitialized data (bss)

heap


stack


High Address




First, the entry point of the kernel is the boot function at the base address 0x80200000. ENTRY(boot) declares that the boot function is the entry point of the program. Then, the placement of each section is defined within the SECTIONS block.

SECTION Description
.text The code of the program
.rodata Constant data that is read-only
.data Read/Write data
.bss Read/Write data with an initial value of zero at execution time

The *(.text .text.*) directive places the .text section and any sections starting with .text. from all files (*) at that location. The . symbol represents the current address. It automatically increments as data is placed, such as with *(.text).
The statement . += 128 * 1024 means advance the current address by 128KB. The ALIGN(4) directive ensures that the current address is adjusted to a 4-byte boundary.

Kernel

The minimum kernel is as below.

Boot function

The execution of the kernel starts from the boot function, which is specified as the entry point in the linker script. In this function, the stack pointer (sp) is set to the end address of the stack area defined in the linker script. Then, it jumps to the kernel_main function. The boot function has two special attributes.

  • The __attribute__((naked)) attribute instructs the compiler not to generate unnecessary code before and after the function body. This ensures that the inline assembly code is the exact function body.
  • The __attribute__((section(".text.boot"))) attribute, which controls the placement of the function in the linker script.
__attribute__((section(".text.boot")))
__attribute__((naked))
void boot(void) {
    __asm__ __volatile__(
        "mv sp, %[stack_top]\n" // Set the stack pointer
        "j kernel_main\n"       // Jump to the kernel main function
        :
        : [stack_top] "r" (__stack_top) // Pass the stack top address as %[stack_top]
    );
}

Get linker script symbols

At the beginning of the file, each symbol defined in the linker script is declared using extern char.

extern char __bss[], __bss_end[], __stack_top[];

Since __bss alone means the value at the 0th byte of the .bss section, we add [] to ensure that __bss returns an address and prevent any careless mistakes.

Initializing bss

The .bss section is first initialized to zero using the memset function. Finally, the function enters an infinite loop and the kernel terminates.

void *memset(void *buf, char c, size_t n) {
    uint8_t *p = (uint8_t *) buf;
    while (n--)
        *p++ = c;
    return buf;
}

void kernel_main(void) {
    memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);

    for (;;);
}

Reference