I wanted to go through Writing OS in Rust tutorial for a long time. Today I started the journey. First post was about creating freestanding (bare-metal) binary in rust.

Bare-metal binary means that it can be run without presence of any OS abstractions — no threads, no stdout (which is special OS file descriptor), no heap, no random numbers, no network etc.

The challenge is that by default, Rust compiler links standard library which delivers many OS abstractions. Internally standard library uses libc which interacts closely with the OS. We need to get rid of that.

Rust allows adding #![no_std] attribute to a main file, to indicate that we don't want to include stdlib. But it's not all we need to do. When we try to compile the program, we'll get two errors.

First error is about not having panic handler. Panic handler defines the behavior of the program in case of a panic. By default, stdlib delivers that functionality. We just get rid of stdlib, so we need to deliver our own panic handler. To do that, all we need to do is:

use core::panic::PanicInfo;

fn panic(_info: &PanicInfo) -> ! {
  loop {}

This allows to get rid of the first error. The second error is a bit more cryptic. It says:

error: language item required but not found: `eh_personality`

First: language items. Language items are additional functions and types which are used by the compiler. They are unstable and shouldn't be used directly. The item eh_personality is used to mark the function which does stack unwinding (calling destructors in case of a panic and returning the execution to the parent thread). This is pretty complicated process and uses specific libraries on different OSes. For example on Linux it's using libunwind and on Windows, Structured Exception Handling.

We need to get rid of that as well. To do that all we need to do is to add the following to Cargo.toml:

panic = "abort"

panic = "abort"

Now we fixed two issues. But when we try to build the binary, we'll get another error:

error: requires `start` lang_item

The way Rust binaries are started is as following:

  1. Execution starts in C runtime library called crt0 — C Runtime Zero.
  2. The control is passed to Rust function marked with start language item. The Rust runtime is minimal and takes care of special things like setting up stack overflow guards or printing backtrace on panic.
  3. Then the execution is passed to main function.

Our binary doesn't have access to Rust runtime and crt0, so we need to deliver entry point on our own. This is pretty easy in Rust. All we need to do is the following:

  1. Add #![no_main] attribute
  2. Define _start entry point:
    pub extern "C" fn _start() -> ! {
      loop {}
    In the second step, we need to use #[no_mangle] attribute to tell Rust compiler to not change the function name. By default, it will generate some random name. With #[no_mangle] the function name will stay the same.

Now, when we'll try to compile our binary we will get linker error. This is because by default Rust tries to link the binary with C runtime which we don't want.

The quickest solution here is to compile for bare-metal target like this: cargo build --target thumbv7-none-eabihf