# 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/)