You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: content/rust-os/1-hello-riscv/index.md
+34-35
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
title = "Operating Systems in Rust #1: Hello RISC-V"
3
3
description = "In this first post, we'll set up our environment and write a simple program that prints 'Hello World' to the screen."
4
4
date = 2023-05-07
5
-
updated = 2023-05-12
5
+
updated = 2024-03-01
6
6
aliases = ["osdev-1"]
7
7
transparent = true
8
8
@@ -22,15 +22,15 @@ This is a series of posts about my journey creating a kernel in rust. You can fi
22
22
I've been interested in operating systems for a while now and, with many of the recent advancements in rust's role in the OS ecosystem, I thought it would be fun to try and write a kernel in rust.
23
23
I've found that many blogs and guides on writing kernels and operating systems are either pretty outdated or not very accessible, so this will be a different (and hopefully more fun) approach, fully utilizing the Rust ecosystem to get up and running quickly and minimizing the use of unsafe and assembly code.
24
24
25
-
This series requires pre-requisite knowledge of Rust or a similar programming language. All commands throughout the series will also be expecting a Linux terminal and might need to be adjusted slightly for macOS or Windows. I'll be using Arch Linux, but any distro should work fine. I'm trying to keep everything approachable, so if you have any questions or suggestions, feel free to reach out to me at [[email protected]](mailto:[email protected])
25
+
This series requires pre-requisite knowledge of Rust or a similar programming language. All commands throughout the series will also be expecting a Linux terminal and might need to be adjusted slightly for macOS or Windows. I'll be using Arch Linux, but any distro should work fine.
26
26
27
-
To follow along, I've also created a [GitHub Repo](https://github.com/explodingcamera/pogos) with a branch for each part of the series. You can find the code for this part [here](https://github.com/explodingcamera/pogos/tree/part-1).
27
+
<!--To follow along, I've also created a [GitHub Repo](https://github.com/explodingcamera/pogos) with a branch for each part of the series. You can find the code for this part [here](https://github.com/explodingcamera/pogos/tree/part-1).-->
28
28
29
29
<!-- {{toc}} -->
30
30
31
31
# CPU Architectures and RISC-V
32
32
33
-
X86 is currently the dominant CPU architecture and has recently lost a bit of market share to emerging ARM CPUs like the [Apple M Series](https://en.wikipedia.org/wiki/Apple_M2) or [AWS Graviton](https://en.wikipedia.org/wiki/AWS_Graviton). For this series, however, we'll be targeting RISC-V. RISC-V is a CPU Architecture released in 2015 under royalty-free open-source licenses. By focusing on small and modular instruction extensions and being standardized so recently, it avoids the sizeable historical baggage and weird design decisions plaguing x86 (check out [this](https://mjg59.dreamwidth.org/66109.html) article to see the horrendous boot process in action). RISC-V was also designed to be extensible, allowing for custom instruction extensions to be added to the base ISA. This enables us to use the same kernel on a wide range of CPUs, from small embedded devices to high-performance servers.
33
+
X86 is currently the dominant CPU architecture and has recently lost a bit of market share to emerging ARM CPUs like the [Apple M Series](https://en.wikipedia.org/wiki/Apple_M2) or [AWS Graviton](https://en.wikipedia.org/wiki/AWS_Graviton). For this series, however, we'll be targeting RISC-V. RISC-V is a CPU Architecture released in 2015 under royalty-free open-source licenses. By focusing on small and modular instruction extensions and being so new, it avoids the sizeable historical baggage and weird design decisions plaguing x86 (check out [this](https://mjg59.dreamwidth.org/66109.html) article to see the horrendous boot process in action). RISC-V was also designed to be extensible, allowing for custom instruction extensions to be added to the base ISA. This enables us to use the same kernel on a wide range of CPUs, from small embedded devices to high-performance servers.
34
34
35
35
I'll be using [QEMU](https://www.qemu.org/) to run our kernel. QEMU is a virtual machine that can emulate a wide range of CPUs and devices, including RISC-V. At the end of this series, we'll also run it on an actual board (my [MangoPi MQ-Pro](https://mangopi.org/mangopi_mqpro) recently arrived, and I'm excited to try it).
36
36
@@ -44,7 +44,7 @@ Because the standard library depends on an operating system to provide memory al
44
44
45
45
{% end %}
46
46
47
-
To do this, we'll use the `core` library, a subset of the standard library that doesn't depend on an operating system. With it, we won't have access to things like `println!` or `Vec`, but we can still use types like `Option` and `Result` and many other useful APIs.
47
+
To do this, we'll use the `core` library, a subset of the standard library that doesn't depend on an underlying operating system. With it, we won't have access to things like `println!` or `Vec`, but we can still use types like `Option` and `Result` and many other useful APIs.
48
48
In the next post, we'll also use the `alloc` create to enable us to use language features that require heap allocations, such as `Vec` and `Box`.
49
49
50
50
Before we start, we'll need to install a couple of things:
# create a new cargo project (this will be our kernel)
@@ -108,26 +104,26 @@ rustflags = [
108
104
build-std = ["core", "alloc"]
109
105
```
110
106
107
+
Even though you can use `no_std` and `alloc` without building the standard library yourself just fine for building libraries, it's required for building executables (at least for now).
108
+
111
109
# Booting on RISC-V
112
110
113
111
## RISC-V Privilege Levels
114
112
115
113
To better understand how we'll be booting our kernel, we'll first have to understand how RISC-V's privilege levels work. RISC-V has three privilege levels, sometimes called _rings_ or _modes_.
116
114
117
-
Firmware runs in Machine mode, the highest [privilege level](http://docs.keystone-enclave.org/en/latest/Getting-Started/How-Keystone-Works/RISC-V-Background.html#RISC-V-privilieged-isa) on RISC-V. The kernel will run in Supervisor-mode, and user programs will run in User-mode, the lowest privilege level.
115
+
Firmware runs in Machine mode, the highest [privilege level](http://docs.keystone-enclave.org/en/latest/Getting-Started/How-Keystone-Works/RISC-V-Background.html#RISC-V-privilieged-isa).
116
+
This is where the bootloader and the Supervisor Execution Environment (SEE) run. This SEE is a piece of software that provides a small abstraction layer between the kernel and the hardware, and loads the kernel into memory and jumps to it. Our kernel will run in Supervisor-mode, which is the second highest privilege level. Finally, applications will run in User-mode, the lowest privilege level.
118
117
119
118
{{ figure(caption = "The three privilege levels of RISC-V", position="center", src="./assets/rings.svg") }}
120
119
121
120
## SBI and OpenSBI
122
121
123
-
Compared to other CPU Architectures, RISC-V's boot process is straightforward.
122
+
Compared to other CPU Architectures, RISC-V's boot process is relatively straightforward (If you don't try to force UEFI onto it).
124
123
We're using OpenSBI as our Supervisor Execution Environment (SEE), our _M-mode RUNTIME firmware_.
125
124
126
-
{% quote (class="info")%}
127
-
SBI (Supervisor Binary Interface) is a standard interface for interacting with the SEE, and OpenSBI is an implementation of this standard.
128
-
{% end %}
129
-
130
-
The version shipping with QEMU uses a Jump Address ([_FW_JUMP_](https://github.com/riscv-software-src/opensbi/blob/master/docs/firmware/fw_jump.md)), in this case, `0x80200000`, which is where we'll be putting our kernel. QEMU will load our kernel into memory and jump to `0x80000000`, from where OpenSBI will then jump to `0x80200000`, where our kernel is located.
125
+
The version shipping with QEMU uses a Jump Address ([_FW_JUMP_](https://github.com/riscv-software-src/opensbi/blob/master/docs/firmware/fw_jump.md)), in this case, `0x80200000`.
126
+
This is a location in memory where we'll be putting our kernel, using the `-kernel` flag we set in our `.cargo/config.toml` file earlier. From there, OpenSBI will run some initialization code and jump to our kernel.
@@ -143,7 +139,9 @@ To interact with SBI, we will use the `ecall` instruction, a trap instruction th
143
139
144
140
To make a binary that can be loaded as a kernel, we'll use the [riscv-rt](https://crates.io/crates/riscv-rt) crate, which provides a runtime for RISC-V. It also provides us with a trap handler which will be very useful for handling interrupts and exceptions and a linker script which we'll be using to set up the memory layout of our kernel.
145
141
146
-
Later on, we'll be writing our own runtime, but for now, we'll use this to get up and running quickly. We can add it to our project by adding the following to our `Cargo.toml` file:
142
+
`riscv-rt` is primarily designed to be used with the microcontrollers, so in a later post we'll be replacing it with our own linker script and runtime.
143
+
144
+
To add it to our project, we have to update our `Cargo.toml` file:
147
145
148
146
{{ file(name = "cargo.toml")}}
149
147
@@ -155,18 +153,19 @@ name = "kernel"
155
153
version = "0.1.0"
156
154
157
155
[dependencies]
158
-
# enable the s-mode feature to use the supervisor mode runtime (opensbi will run in machine mode and load our kernel into supervisor mode)
156
+
# enable the s-mode feature to use the supervisor mode runtime
157
+
# (opensbi will run in machine mode and load our kernel into supervisor mode)
159
158
riscv-rt = {version = "0.11", features = ["s-mode"]}
160
159
sbi = "0.2"# provides a wrapper around the SBI functions to make them easier to use
161
160
```
162
161
163
-
Now we need to configure our linker. A linker is a program that takes a bunch of object files and combines them into a single binary, and - in our case - we need to tell it where to put the different sections of our binary so SBI can find them. `riscv-rt` already ships with a linker script. We will only need to tell it about the memory layout of our kernel.
162
+
Now that we have `riscv-rt` and `sbi` in our project, we can start writing our kernel. We'll start by configuring our linker to tell it where to put the different sections of our binary, such as the text, data, and bss sections. `riscv-rt` already ships with a linker script, so we will only need to tell it some basic information about the memory layout the device we want to run it on.
164
163
165
-
Since we're using the Executable and Linking Format (ELF) for our kernel, we'll be distinguishing different regions of memory TEXT, DATA, and BSS.
164
+
The executable format we'll be using is ELF, which is the format used by Linux and most other UNIX-like operating systems. Other common formats are PE (windows) and Mach-O (macOS). On x86, we'd have to endure the pain of dealing with PE binaries.
To make sure that our kernel is loaded at the correct address, we need to add the following to our `memory.x`file:
168
+
To start, we'll put the entire kernel in RAM, and give it size of 16MB. We'll do this by creating a new file called `memory.x`in the root of our project:
170
169
171
170
{{ file(name = "memory.x")}}
172
171
@@ -175,7 +174,6 @@ To make sure that our kernel is loaded at the correct address, we need to add th
Now that we have the linker configured, we can start writing our kernel. We'll start by writing a simple hello world program that prints "Hello world!" to the console and then shuts down the machine.
210
+
Now that we have the linker configured, we can start writing some actual code. We'll start by writing a simple hello world program that prints "Hello world!" to the console and then shuts down the machine.
214
211
215
212
## Printing
216
213
217
-
Since we're in a `no_std` environment, we can't use the standard library and must implement a print function ourselves. We'll be using the `sbi` crate to interact with our Supervisor Execution Environment (SEE) OpenSBI, which provides a `console_putchar` function that we can use to print a single character to the console. We can use this to implement a simple print function that prints a string to the console. This print function iterates over the characters in the string and prints them to the QEMU debug console.
214
+
Since we're in a `no_std` environment, we can't use the standard library and must implement a print function ourselves. We'll be using the `sbi` crate to interact with our Supervisor Execution Environment (SEE) OpenSBI, which provides a `console_putchar` function that we can use to print a single character to the console. Using this crate, we can create a simple print function that prints a string to the console. This print function iterates over the characters in the string and prints them to the QEMU debug console.
215
+
216
+
{% quote (class="info")%}
217
+
`console_putchar` is now part of the SBI Debug Extension, which is not yet available in all SBI implementations. For now, we'll use the legacy console putchar function, which is available in all SBI implementations.
218
+
{% end %}
218
219
219
220
{{ file(name = "src/utils.rs")}}
220
221
221
222
```rust
222
223
223
224
pubfnprint(t:&str) {
224
225
t.chars().for_each(
225
-
// FUTURE: replace with the new SBI debug extension once it's available in all SBI implementations
To get super fancy with our print function, we can also implement a macro that allows us to print to the console using the same syntax as the standard library's `println!` macro. All we need to do for this is implement the `core::fmt::Write` for our print function.
231
+
To get super fancy with our print function, we can also implement a macro that allows us to print to the console using the same syntax as the standard library's `println!` macro. All we need to do for this is implement the `core::fmt::Write`trait for our print function.
232
232
233
233
{{ file(name = "src/utils.rs")}}
234
234
235
235
```rust
236
236
237
237
structWriter {}
238
+
238
239
pubfnprint_args(t:core::fmt::Arguments) {
239
240
usecore::fmt::Write;
240
241
letmutwriter=Writer {};
@@ -284,7 +285,7 @@ pub fn shutdown() -> ! {
284
285
285
286
## Panic handler
286
287
287
-
Notice the `.unwrap()` call in the `print_args` function? Since we're using `no_std`, we can't use the standard library's panic handler. Instead, we'll write our panic handler to print the panic message to the console and then halt the CPU. We can do this by implementing the `panic_handler` lang item. This is a special function that is called when a panic occurs. We can implement it like this:
288
+
Notice the `.unwrap()` call in the `print_args` function? Since we're using `no_std`, we can't use the standard library's panic handler. Instead, we'll write our panic handler to print the panic message to the console and then halt the CPU. To do this, wee need to mark a function with the special `panic_handler` lang item:
288
289
289
290
{{ file(name = "src/panic_handler.rs")}}
290
291
@@ -296,7 +297,7 @@ use sbi::system_reset::{ResetReason, ResetType};
Now we can finally write our hello world program. We'll be using the `entry` macro from `riscv-rt` to mark our main function as the entry point of our program. `riscv-rt` will load some assembly to set up a basic c-runtime environment and then call our main function. The main function will be passed the hart id of the hart that is executing it, which is passed to us by OpenSBI through the `a0` register.
319
+
Now we can finally write our hello world program. Using the `entry` macro from `riscv-rt`, we can mark our main function as the entry point of our program. `riscv-rt` will load some assembly to set up a basic c-runtime environment and then call our main function with the hart id of the hart that is executing it (passed to us by OpenSBI through the `a0` register).
319
320
320
321
{% quote (class="info")%}
321
-
322
322
Hart is the RISC-V term for a CPU core. A RISC-V system can have multiple harts, each with its own register state and program counter.
323
-
324
323
{% end %}
325
324
326
-
Something you might not have seen before is the `!` return type. This is a special type that means that the function never returns. Since we're writing a kernel, there's nowhere for our program to return to, so we'll need to either loop forever or shut down the machine.
327
-
328
325
{{ file(name = "src/main.rs")}}
329
326
330
327
```rust
@@ -349,6 +346,8 @@ fn main(a0: usize) -> ! {
349
346
}
350
347
```
351
348
349
+
Something you might not have seen before is the `!` return type. This is a special type that means that the function never returns. Since we're writing a kernel, there's nowhere for our program to return to, so we'll need to either loop forever or shut down the machine.
350
+
352
351
# Review
353
352
354
353
Once we run `cargo run`, we should see "Hello world!" printed on the console:
0 commit comments