Credit
I am writing this post as a way of documenting my efforts while following this guide by Philipp Oppermann: os.phil-opp.com. All credit goes to him for creating such a brilliant guide.
I'm just hoping to learn more about Rust and Operating systems through this.
If you're planning to implement this yourself, I strongly suggest going through the real thing since that is way more detailed.
Aim
Here, we will create a Rust binary which will ultimately run on bare metal. This effectively means that we are not going to use any functionality of the Rust standard library.
Moreover, since we are creating an OS from scratch, we won't be using any functionality offered by our host operating system such as threads, files, heap memory, the network, random numbers, standard output, etc.
The no_std Attribute
Since Rust implicitly uses the standard library, let's disable this. The attribute #![no_std]
tells the compiler not to link the standard library.
#![no_std]
fn main() {
}
On building we get the following errors:
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
Let us try to understand these 2 errors:
#[panic_handler]
When Rust runs into an unexpected problem at runtime, it calls the panic handler which in turn dumps the stack trace of the crash. This is present in the standard library.
Since we disabled the standard library, we need to provide an implementation for it.
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
The !
is used to indicate this function should never return. It's called a diverging function. Since we're not doing anything on panicking, the guide just adds an empty loop.
eh_personality
This is a language item. When the compiler parses the code, it looks for these language items
attributes which tell it additional information about the function.
The eh_personality
type, in particular, tells the compiler that this function is used for implementing stack unwinding. Which is essentially the code that gets executed when a function is popped from the function stack. So, probably the memory which the function was using gets freed in this.
Let us disable this by adding the following to our Cargo.toml
file.
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
Now, trying to build it after fixing both the errors we get:
error: requires `start` lang_item
So, as it turns out, the main function is not the first function that gets called in a rust program.
Execution starts with a runtime library. Rust uses
crt0
(“C runtime zero”). That, in turn, calls the Rust runtime, which is a function marked with thestart
language item. And that, in turn, calls the main method.
So, in our case, we are not using the crt0
and start
language item. So, we need to define our own entry point.
Defining our entrypoint
First we need to tell the compiler we don't need a runtime. For that we add a #![no_main]
attribute. Also, we can remove the main
function at this point since we don't have a runtime which will call it.
#![no_std]
#![no_main]
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
Now, we need to define our entry point.
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
}
So, the rust compiler actually changes all the function names to different unique values while compiling. This is known as name mangling. We are disabling that because we want to reference our start function by name at some point.
We also have to mark the function as extern "C" to tell the compiler that it should use the C calling convention for this function.
And the ! will make the function a diverging function which should not return. (later we might change it to a call to shut down the system.)
Building what we have at this point gives the following linking error:
error: linking with `link.exe` failed: exit code: 1561
The linker is a program that takes the objects generated by the compiler and builds them into executable code.
By default, Rust tries to build an executable that is able to run in your current system environment. Here since I'm running on a windows x86_64 machine, it tries to build an .exe
using x86_64
instructions.
By default, the linker assumes we are using the C runtime. So, in order to resolve this, by building for a bare-metal target.
Building for bare-metal
To describe different environments, Rust uses a string called target triple.
The purpose of this string is to describe the architecture of the machine.
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 causes the linker errors. So to avoid the linker errors, we can compile for a different environment with no underlying operating system. An example of such a target is thumbv7em-none-eabihf
(we are going to use this).
cargo build --target thumbv7em-none-eabihf
This will cross-compile our executable for a bare metal target and the build will go through.
Code
#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points
use core::panic::PanicInfo;
#[no_mangle] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
// this function is the entry point, since the linker looks for a function
// named `_start` by default
loop {}
}
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
Cargo.toml
[package]
name = "crate_name"
version = "0.1.0"
authors = ["Author Name <author@example.com>"]
# the profile used for `cargo build`
[profile.dev]
panic = "abort" # disable stack unwinding on panic
# the profile used for `cargo build --release`
[profile.release]
panic = "abort" # disable stack unwinding on panic