A Freestanding Rust Binary

A Freestanding Rust Binary

Photo by Jay Heike on Unsplash

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 the start 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

main.rs

#![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