+++ title = "Minimal Kernel" weight = 1 path = "minimal-kernel" date = 0000-01-01 draft = true [extra] chapter = "Bare Bones" icon = ''' ''' +++ The first step in creating our own operating system kernel is to create a [bare metal] Rust executable that does not depend on an underlying operating system. For that we need to disable most of Rust's standard library and adjust various compilation settings. The result is a minimal operating system kernel that forms the base for the following posts of this series. [bare metal]: https://en.wikipedia.org/wiki/Bare_machine This blog is openly developed on [GitHub]. If you have any problems or questions, please open an issue there. You can also leave comments [at the bottom]. The complete source code for this post can be found in the [`post-3.1`][post branch] branch. [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments [post branch]: https://github.com/phil-opp/blog_os/tree/post-3.1 ## Introduction Kernels are the heart of an operating system. They provide all the fundamental building blocks that are required for building higher-level programs. Typical building blocks are threads, files, heap memory, timers, or sockets. Other important tasks of a kernel are the isolation of different programs and the multiplexing of resources. When writing an operating system kernel, we need to provide all of these building blocks ourselves. This means that we can't use most of the [Rust standard library]. However, there are still a lot of Rust features that we _can_ use. For example, we can use [iterators], [closures], [pattern matching], [`Option`] and [`Result`], [string formatting], and of course the [ownership system]. These features make it possible to write a kernel in a very expressive, high level way and worry less about [undefined behavior] or [memory safety]. [`Option`]: https://doc.rust-lang.org/core/option/ [`Result`]: https://doc.rust-lang.org/core/result/ [Rust standard library]: https://doc.rust-lang.org/std/ [iterators]: https://doc.rust-lang.org/book/ch13-02-iterators.html [closures]: https://doc.rust-lang.org/book/ch13-01-closures.html [pattern matching]: https://doc.rust-lang.org/book/ch06-00-enums.html [string formatting]: https://doc.rust-lang.org/core/macro.write.html [ownership system]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html [undefined behavior]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs [memory safety]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention In this post, we create a minimal OS kernel that can be run without an underlying operating system. Such an executable is often called a “freestanding” or “bare-metal” executable. We then make this executable compatible with the early-boot environment of the `x86_64` architecture so that we can boot it as an operating system kernel. ## Disabling the Standard Library By default, all Rust crates link the [standard library], which depends on the operating system for features such as threads, files, or networking. It also depends on the C standard library `libc`, which closely interacts with OS services. Since our plan is to write an operating system, we cannot use any OS-dependent libraries. So we have to disable the automatic inclusion of the standard library, which we can do through the [`no_std` attribute]. [standard library]: https://doc.rust-lang.org/std/ [`no_std` attribute]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html We start by creating a new cargo application project. The easiest way to do this is through the command line: ``` cargo new kernel --bin --edition 2021 ``` We name the project `kernel` here, but of course you can choose your own name. The `--bin` flag specifies that we want to create an executable binary (in contrast to a library) and the `--edition 2021` flag specifies that we want to use the [2021 edition] of Rust for our crate. When we run the command, cargo creates the following directory structure for us: [2021 edition]: https://doc.rust-lang.org/edition-guide/rust-2021/index.html ``` kernel ├── Cargo.toml └── src └── main.rs ``` The `Cargo.toml` contains the crate configuration, for example the crate name, the [semantic version] number, and dependencies. The `src/main.rs` file contains the root module of our crate and our `main` function. You can compile your crate through `cargo build` and then run the compiled `kernel` binary in the `target/debug` subfolder. [semantic version]: https://semver.org/ ### The `no_std` Attribute Right now our crate implicitly links the standard library. Let's try to disable this by adding the [`no_std` attribute]: ```rust,hl_lines=3 // main.rs #![no_std] fn main() { println!("Hello, world!"); } ``` When we try to build it now (by running `cargo build`), the following errors occur: ``` error: cannot find macro `println!` in this scope --> src/main.rs:4:5 | 4 | println!("Hello, world!"); | ^^^^^^^ error: `#[panic_handler]` function required, but not found error: language item required, but not found: `eh_personality` [...] ``` The reason for the first error is that the [`println` macro] is part of the standard library, which we no longer include. So we can no longer print things. This makes sense, since `println` writes to [standard output], which is a special file descriptor provided by the operating system. [`println` macro]: https://doc.rust-lang.org/std/macro.println.html [standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29 So let's remove the printing and try again with an empty main function: ```rust,hl_lines=5 // main.rs #![no_std] fn main() {} ``` ``` ❯ cargo build error: `#[panic_handler]` function required, but not found error: language item required, but not found: `eh_personality` [...] ``` The `println` error is gone, but the compiler is still missing a `#[panic_handler]` function and a _language item_. ### Panic Implementation The `panic_handler` attribute defines the function that the compiler should invoke when a [panic] occurs. The standard library provides its own panic handler function, but in a `no_std` environment we need to define one ourselves: [panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html ```rust,hl_lines=3 9-13 // in main.rs use core::panic::PanicInfo; #![no_std] fn main() {} /// This function is called on panic. #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } ``` The [`PanicInfo` parameter][PanicInfo] contains the file and line where the panic happened and the optional panic message. The handler function should never return, so it is marked as a [diverging function] by returning the [“never” type] `!`. There is not much we can do in this function for now, so we just loop indefinitely. [PanicInfo]: https://doc.rust-lang.org/core/panic/struct.PanicInfo.html [diverging function]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions [“never” type]: https://doc.rust-lang.org/std/primitive.never.html After defining a panic handler, only the `eh_personality` language item error remains: ``` ❯ cargo build error: language item required, but not found: `eh_personality` | = note: this can occur when a binary crate with `#![no_std]` is compiled for a target where `eh_personality` is defined in the standard library = help: you may be able to compile for a target that doesn't need `eh_personality`, specify a target with `--target` or in `.cargo/config` ``` ### Disabling Unwinding Language items are special functions and types that are required internally by the compiler. They are normally provided by the standard library, which we disabled using the `#![no_std]` attribute. The [`eh_personality` language item] marks a function that is used for implementing [stack unwinding]. By default, Rust uses unwinding to run the destructors of all live stack variables in case of a [panic]. This ensures that all used memory is freed and allows the parent thread to catch the panic and continue execution. Unwinding, however, is a complex process and requires some OS-specific libraries, such as [libunwind] on Linux or [structured exception handling] on Windows. [`eh_personality` language item]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45 [stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php [libunwind]: https://www.nongnu.org/libunwind/ [structured exception handling]: https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling While unwinding is very useful, it also has some drawbacks. For example, it increases the size of the compiled executable because it requires additional context at runtime. Because of these drawbacks, Rust provides an option to [abort on panic] instead. [abort on panic]: https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html#unwinding-the-stack-or-aborting-in-response-to-a-panic We already use a custom panic handler that never returns, so we don't need unwinding for our kernel. By disabling it, the `eh_personality` language item won't be required anymore. There are multiple ways to set the panic strategy, the easiest is to use [cargo profiles]: [cargo profiles]: https://doc.rust-lang.org/cargo/reference/profiles.html ```toml,hl_lines=3-7 # in Cargo.toml [profile.dev] panic = "abort" [profile.release] panic = "abort" ``` This sets the panic strategy to `abort` for both the `dev` profile (used for `cargo build`) and the `release` profile (used for `cargo build --release`). Now the `eh_personality` language item should no longer be required. When we try to compile our kernel now, a new error occurs: ``` ❯ cargo build error: requires `start` lang_item ``` Our kernel is missing the `start` language item, which defines the _entry point_ of the executable. ## Setting the Entry Point The [entry point] of a program is the function that is called when the executable is started. One might think that the `main` function is the first function called, however, most languages have a [runtime system], which is responsible for things such as garbage collection (e.g. in Java) or software threads (e.g. goroutines in Go). This runtime needs to be called before `main`, since it needs to initialize itself. [entry point]: https://en.wikipedia.org/wiki/Entry_point [runtime system]: https://en.wikipedia.org/wiki/Runtime_system In a typical Rust binary that links the standard library, execution starts in a C runtime library called [`crt0`] (“C runtime zero”), which sets up the environment for a C application. This includes creating a [call stack] and placing the command line arguments in the right CPU registers. The C runtime then invokes the [entry point of the Rust runtime][rt::lang_start], which is marked by the `start` language item. Rust only has a very minimal runtime, which takes care of some small things such as setting up stack overflow guards or printing a backtrace on panic. The runtime then finally calls the `main` function. [`crt0`]: https://en.wikipedia.org/wiki/Crt0 [call stack]: https://en.wikipedia.org/wiki/Call_stack [rt::lang_start]: hhttps://github.com/rust-lang/rust/blob/0d97f7a96877a96015d70ece41ad08bb7af12377/library/std/src/rt.rs#L59-L70 Since we're building an operating system kernel that should run without any underlying operating system, we don't want our kernel to depend on any Rust or C runtime. To remove these dependencies, we need to do two things: 1. Instruct the compiler that we want to build for a bare-metal target environment. This removes the dependency on the C library. 2. Disable the Rust main function to remove the Rust runtime. ### Bare-Metal Target By default Rust tries to build an executable that is able to run in your current system environment. For example, if you're using Windows and an `x86_64` CPU, Rust tries to build a `.exe` Windows executable that uses `x86_64` instructions. This environment is called your "host" system. To describe different environments, Rust uses a string called [_target triple_]. You can see the target triple for your host system by running `rustc --version --verbose`: [_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple ``` rustc 1.68.1 (8460ca823 2023-03-20) binary: rustc commit-hash: 8460ca823e8367a30dda430efda790588b8c84d3 commit-date: 2023-03-20 host: x86_64-unknown-linux-gnu release: 1.68.1 LLVM version: 15.0.6 ``` The above output is from a `x86_64` Linux system. We see that the `host` triple is `x86_64-unknown-linux-gnu`, which includes the CPU architecture (`x86_64`), the vendor (`unknown`), the operating system (`linux`), and the [ABI] (`gnu`). [ABI]: https://en.wikipedia.org/wiki/Application_binary_interface By compiling for our host triple, the Rust compiler and the linker assume that there is an underlying operating system such as Linux or Windows that uses the C runtime by default, which requires the `start` language item. To avoid the runtimes, we can compile for a different environment with no underlying operating system. #### The `x86_64-unknown-none` Target Rust supports a [variety of target systems][platform-support], including some bare-metal targets. For example, the `thumbv7em-none-eabihf` target triple can be used to compile for an [embedded] [ARM] system with a `Cortex M4F` CPU, as used in the [Rust Embedded Book]. [platform-support]: https://doc.rust-lang.org/rustc/platform-support.html [embedded]: https://en.wikipedia.org/wiki/Embedded_system [ARM]: https://en.wikipedia.org/wiki/ARM_architecture [Rust Embedded Book]: https://docs.rust-embedded.org/book/intro/index.html Our kernel should run on a bare-metal `x86_64` system, so the suitable target triple is [`x86_64-unknown-none`]. The `-none` suffix indicates that there is no underlying operating system. To be able to compile for this target, we need to add it using [`rustup`]. [`x86_64-unknown-none`]: https://doc.rust-lang.org/rustc/platform-support/x86_64-unknown-none.html
rustup?