The smallest #![no_std] program

In this section we'll write the smallest #![no_std] program that compiles.

What does #![no_std] mean?

#![no_std] is a crate level attribute that indicates that the crate will link to the core crate instead of the std crate, but what does this mean for applications?

The std crate is Rust's standard library. It contains functionality that assumes that the program will run on an operating system rather than directly on the metal. std also assumes that the operating system is a general purpose operating system, like the ones one would find in servers and desktops. For this reason, std provides a standard API over functionality one usually finds in such operating systems: Threads, files, sockets, a filesystem, processes, etc.

On the other hand, the core crate is a subset of the std crate that makes zero assumptions about the system the program will run on. As such, it provides APIs for language primitives like floats, strings and slices, as well as APIs that expose processor features like atomic operations and SIMD instructions. However it lacks APIs for anything that involves heap memory allocations and I/O.

For an application, std does more than just providing a way to access OS abstractions. std also takes care of, among other things, setting up stack overflow protection, processing command line arguments and spawning the main thread before a program's main function is invoked. A #![no_std] application lacks all that standard runtime, so it must initialize its own runtime, if any is required.

Because of these properties, a #![no_std] application can be the first and / or the only code that runs on a system. It can be many things that a standard Rust application can never be, for example:

  • The kernel of an OS.
  • Firmware.
  • A bootloader.

The code

With that out of the way, we can move on to the smallest #![no_std] program that compiles:

$ cargo new --edition 2018 --bin app

$ cd app
$ # modify main.rs so it has these contents
$ cat src/main.rs
#![allow(unused)]
#![no_main]
#![no_std]

fn main() {
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}
}

This program contains some things that you won't see in standard Rust programs:

The #![no_std] attribute which we have already extensively covered.

The #![no_main] attribute which means that the program won't use the standard main function as its entry point. At the time of writing, Rust's main interface makes some assumptions about the environment the program executes in: For example, it assumes the existence of command line arguments, so in general, it's not appropriate for #![no_std] programs.

The #[panic_handler] attribute. The function marked with this attribute defines the behavior of panics, both library level panics (core::panic!) and language level panics (out of bounds indexing).

This program doesn't produce anything useful. In fact, it will produce an empty binary.

$ # equivalent to `size target/thumbv7m-none-eabi/debug/app`
$ cargo size --target thumbv7m-none-eabi --bin app
   text	   data	    bss	    dec	    hex	filename
      0	      0	      0	      0	      0	app

Before linking, the crate contains the panicking symbol.

$ cargo rustc --target thumbv7m-none-eabi -- --emit=obj

$ cargo nm -- target/thumbv7m-none-eabi/debug/deps/app-*.o | grep '[0-9]* [^N] '
00000000 T rust_begin_unwind

However, it's our starting point. In the next section, we'll build something useful. But before continuing, let's set a default build target to avoid having to pass the --target flag to every Cargo invocation.

$ mkdir .cargo

$ # modify .cargo/config so it has these contents
$ cat .cargo/config
[build]
target = "thumbv7m-none-eabi"

eh_personality

If your configuration does not unconditionally abort on panic, which most targets for full operating systems don't (or if your custom target does not contain "panic-strategy": "abort"), then you must tell Cargo to do so or add an eh_personality function, which requires a nightly compiler. Here is Rust's documentation about it, and here is some discussion about it.

In your Cargo.toml, add:

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

Alternatively, declare the eh_personality function. A simple implementation that does not do anything special when unwinding is as follows:

#![allow(unused)]
#![feature(lang_items)]

fn main() {
#[lang = "eh_personality"]
extern "C" fn eh_personality() {}
}

You will receive the error language item required, but not found: 'eh_personality' if not included.