# 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 ```bash $ 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. ```bash $ brew install llvm lld qemu ``` Then add `llvm binutils` to the `PATH` ```bash $ 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. ```bash 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. ```graphviz digraph "Boot" { rankdir = "LR" node1[shape = rectangle, label = "Power On"]; node2[shape = rectangle, label = "BIOS/UEFI"]; node3[shape = rectangle, label = "Boot Loader"]; node4[shape = rectangle, label = "OS Kernel"]; node1 -> node2 -> 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. ```linker 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. ```graphviz digraph "Layout" { node [shape=plaintext]; Layout [ label=< <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="lightgray" HEIGHT="20"><B>Low Address</B></TD></TR> <TR><TD HEIGHT="30" WIDTH="200">text</TD></TR> <TR><TD HEIGHT="40">read-only data</TD></TR> <TR><TD HEIGHT="50">initialized data</TD></TR> <TR><TD HEIGHT="50">uninitialized data (bss)</TD></TR> <TR><TD HEIGHT="80">heap</TD></TR> <TR><TD HEIGHT="60"></TD></TR> <TR><TD HEIGHT="80">stack</TD></TR> <TR><TD BGCOLOR="lightgray" HEIGHT="20"><B>High Address</B></TD></TR> </TABLE> > ]; } ``` 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. ```clike __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`. ```clike extern char __bss[], __bss_end[], __stack_top[]; ``` :::warning 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. ```clike 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 * [OS in 1000 Lines](https://operating-system-in-1000-lines.vercel.app/en/)