Bare-metal binaries in Rust
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;
#[panic_handler]
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
:
[profile.dev]
panic = "abort"
[profile.release]
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:
- Execution starts in C runtime library called
crt0
— C Runtime Zero. - 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. - 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:
- Add
#![no_main]
attribute - Define
_start
entry point:#[no_mangle] pub extern "C" fn _start() -> ! { loop {} }
#[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