Writing an Embedded Operating System in Rust
Alistair Francis, Western Digital [Open Source Summit EU 2022]

Rust is a systems language like C, that can be used to build reliable and efficient software. It provides compile-time memory and thread safety. It’s slowly being introduced into open source projects, including the Linux kernel. There is even talk about rewriting the NVMe driver in rust. This allows to evaluate performance, it turns out to be about the same as C, and it might even get better as some of the current C bindings are converted to Rust as well.

Rust is similar to C in a lot of aspects. It’s fully ahead of time compiled. It focuses on programmer control and zero runtime overhead. It works well on bare metal in addition to hosted. It links with C/C++ programs and libraries.

Differences: really storngly typed (no casts), modules instead of include files, statements evaluate to values (so everything is an expression). References are completely different: there can be only one mutable reference or many immutable reference - that is the core of the memory and thread safety. It has powerful generics and macros. It also has an unsafe subset that allows to bypass some of the safety constraints.

unsafe doesn’t say that it’s wrong, it says that the programmer knows something that the compiler doesn’t. This makes it easy to focus reviewers and comments on the unsafe parts. They are usually also hidden behind wrappers.

Rust has 3 bundled library: core is always there. alloc gives you memory - you can skip it completely (i.e. only static allocation) or implement you own. std contains a bunch of utilities and uses alloc (either your own or the standard one).

Tock is an OS written in rust targetting microcontrollers without MMU.

The core kernel has scheduler etc. Microcontroller-specific drivers use unsafe for IO etc and are trusted (they actually usually don’t use it, instead they use lower-level crates that wrap the unsafe things like MMIO, but they are allowed to). Protocol implementations (e.g. bluetooth) are done in capsules. These are not allowed to use unsafe at all (config option that disallows unsafe completely). All this runs in supervisor mode. Applications run in user mode (untrusted mode on ARM).

Hardware abstraction is done with traits. These define what functions an implementation must implement. This allows a capsule to use a driver for any microcontroller, the capsule specifies which traits it uses. Rust statically enforces that the low-level driver implements everything that the driver needs.

An OS needs inline assembly, e.g. for startup code. Rust supports that. It’s of course always unsafe.

On RISC-V, Tock uses ePMP (memory protection) to isolate applications from the kernel and other applications. Also no executable code is writeable.

Pain points:

  • Lifetimes can get complicated. You basically have to specify which objects stay alive together.
  • Rust can still panic - which you don’t want in an OS. Explicit ones (i.e. asserts) are fine. But hidden ones are bad, e.g. out-of-bound array access causes a panic. In addition to make it difficult to handle this, it also causes code size inflation because it adds strings to the executable. There is no easy way to catch panics. Linux has the same issue. The hope is that the language is going to evolve to make this more controllable.
  • There is a lot of overhead due to dynamic dispatch. You can easily write dynamic dispatch without realizing. There can be functions that are due to dynamic dispatch that never can be called but LTO doesn’t find them so they still end up in the binary.

For tock, compilation times aren’t bad because it builds something small. Only really big applications really suffer from it.

There is a google project that uses tock, otherwise there are no known users.