Operating System For Raspberry PI 3 Using Rust
Operating System For Raspberry PI 3 Using Rust
ABSTRACT
The main focus of this project is to develop a fully fledged operating system for a single board computer
Raspberry Pi 3. The OS is mainly implemented in a fairly new programming language called Rust. Rust
is a multi-paradigm systems programming language focused on safety, especially safe concurrency. Rust
is syntactically similar to C++ but is designed to provide better memory safety while maintaining high
performance. Rust solves a lot of problems faced by the typical Operating Systems that are mainly
designed in system programming languages such as c and c++. This is the sole reason why Rust is used as
a primary programming language in this project. As the Raspberry Pi uses ARM Cortex-A53 CPU, the
Operating System in this project is designed primarily for the ARM architecture.
INTRODUCTION
Ever since it was first created in 1971 the UNIX operating system has been a fixture of software
engineering. One of its largest contributions to the world of OS engineering, and software engineering in
general, was the C programming language created to write it. In the 4 decades that have passed since
being released, C has changed relatively little but the state-of-the-art in programming language design and
checking, has advanced tremendously. Thanks to the success of unix almost all operating system kernels
have been written largely in C or similar languages like C++. This means that these advances in language
design have largely passed by one of the fields that could most benefit from the more expressive, more
verifiable languages that have come after C. The main goal of this project is to try to create a unix like
operating system using the Rust programming language for the ARM architecture.
Modern Desktop Operating Systems are mostly written in languages such as c or c++. Although these
languages have been proven to be widely effective in their domain, they do have a lot of disadvantages.
One of the main disadvantage of c++ is that it does not provide very strong type-checking. c++ code is
easily prone to errors related to data types, their conversions, for example, while passing arguments to
functions. Also c++ does not provide efficient means for garbage collection. Similarly when new is used
to gain a block of memory, the size reserved by the operating system may be bigger than your request, but
never smaller. Because of this and the fact that delete doesn't immediately return the memory to the
operating system, when you inspect the whole memory that your program is using you may be made to
believe that your application has serious memory leaks. So inspecting the number of bytes the whole
program is using should not be used as a way of detecting memory errors. Only if the memory manager
indicates a large and continuous growth of memory used should you suspect memory leaks.
Much of these problems are solved by a programming language called Rust. As discussed above Rust is a
multi-paradigm systems programming language focused on safety, especially safe concurrency. Rust was
originally designed by Graydon Hoare at Mozilla Research, with contributions from Dave Herman,
Brendan Eich, and others. The designers refined the language while writing the Servo layout engine and
the Rust compiler. The compiler is free and open-source software dual-licensed under the MIT License
and Apache License 2.0.
Why Rust?
Rust is intended to be a language for highly concurrent and highly safe systems, and programming in the
large, that is, creating and maintaining boundaries that preserve large-system integrity. This has led to a
feature set with an emphasis on safety, control of memory layout, and concurrency.
Performance:
Rust is blazingly fast and memory-efficient: with no runtime or garbage collector, it can power
performance-critical services, run on embedded devices, and easily integrate with other languages.
Performance of idiomatic Rust is comparable to the performance of idiomatic C++.
Memory safety:
The system is designed to be memory safe, and it does not permit null pointers, dangling pointers, or data
races in safe code. Data values can only be initialized through a fixed set of forms, all of which require
their inputs to be already initialized. To replicate the function in other languages of pointers being either
valid or NULL, such as in linked list or binary tree data structures, the Rust core library provides an
option type, which can be used to test if a pointer has Some value or None. Rust also introduces added
syntax to manage lifetimes, and the compiler reasons about these through its borrow checker.
Memory management:
Rust does not use an automated garbage collection system like those used by Go, Java, or the .NET
Framework. Instead, memory and other resources are managed through resource acquisition is
initialization (RAII), with optional reference counting. Rust provides deterministic management of
resources, with very low overhead. Rust also favors stack allocation of values and does not perform
implicit boxing. There is also a concept of references (using the & symbol), which do not involve runtime
reference counting. The safety of using such pointers is verified at compile time by the borrow checker,
preventing dangling pointers and other forms of undefined behavior.
Ownership
Rust has an ownership system where all values have a unique owner, where the scope of the value is the
same as the scope of the owner. Values can be passed by immutable reference using &T, by mutable
reference using &mut T or by value using T. At all times, there can either be multiple immutable
references or one mutable reference. The Rust compiler enforces these rules at compile time and also
checks that all references are valid.
LITERATURE REVIEW
As Rust is modern and new, there have been many successful attempts to rewrite an entire OS in Rust.
One such example is the Redox operating system. Redox is a Unix-like microkernel operating system
written in Rust. Redox aims to be secure, usable, and free. Redox is inspired by prior kernels and
operating systems, such as SeL4, MINIX, Plan 9, and BSD. It is similar to the GNU or BSD ecosystem,
but in a memory-safe language and with modern technology. It is free and open-source software
distributed under an MIT License.
Redox is a full-featured operating system, providing packages (memory allocator, file system, display
manager, core utilities, etc.) that together make up a functional and convenient operating system. Redox
relies on an ecosystem of software written in Rust by members of the project.
● Redox kernel – largely derives from the concept of microkernels, with heavy inspiration from
MINIX
● Ralloc – memory allocator
● TFS file system – inspired by the ZFS file system
● Ion shell – the underlying library for shells and command execution in Redox, and the default
shell
● Magnet – package manager
● Orbital windowing system – display and window manager, sets up the Orbital: scheme, manages
the display, and handles requests for window creation, redraws, and event polling
Redox uses Rust for its kernel-level code to provide more memory safety considerations than C allows by
default. But the project doesn't simply rewrite Linux in a new language. Redox discards as much from
Linux's version of the Unix tradition as it keeps. Redox uses a minimal set of syscalls -- a deliberately
smaller subset than what Linux supports so as to avoid legacy bloat. The OS also uses a microkernel
design to stay slender, in contrast to Linux's monolithic kernel. Many of the OS's internal behaviors have
also been rethought. Unix and Linux both use the notion of every item as a file. Redox goes a step further
and treats everything like a URL, so it's simple to register handlers for events, and it provides a consistent
manner to perform other kinds of abstractions.
Redox's developers also admit that it won't be possible to establish "complete 1:1 Posix compatibility"
(because the OS omits many Unix syscalls), so existing Linux software will probably need a support layer
on Redox to run -- a roadblock to its adoption.
Still, a project like Redox is valuable. If Redox can make good on its promise of being more secure by
design, many of the embedded-device scenarios currently targeted by Linux might be better served by
Redox. Mozilla has already talked about Rust as a language for Internet of things devices, so this would
be a natural extension.
Redox can also serve as an example for approaching operating system issues differently, exerting
long-term evolutionary pressure on Linux.
Another great example is the Tock OS. Tock is an embedded operating system designed for running
multiple concurrent, mutually distrustful applications on Cortex-M based embedded platforms. Tock’s
design centers around protection, both from potentially malicious applications and from device drivers.
Tock uses two mechanisms to protect different components of the operating system. First, the kernel and
device drivers are written in Rust. Tock uses Rust to protect the kernel (e.g. the scheduler and hardware
abstraction layer) from platform specific device drivers as well as isolate device drivers from each other.
Second, Tock uses memory protection units to isolate applications from each other and the kernel.
Most operating systems provide isolation between components using a process-like abstraction: each
component is given its own slice of the system memory (for its stack, heap, data) that is not accessible by
other components. Processes are great because they provide a convenient abstraction for both isolation
and concurrency. However, on resource-limited systems, like microcontrollers with much less than 1MB
of memory, this approach leads to a trade-off between isolation granularity and resource consumption.
Tock
Tock’s architecture resolves this trade-off by using a language sandbox to isolated components and a
cooperative scheduling model for concurrency in the kernel. As a result, isolation is (more or less) free in
terms of resource consumption at the expense of preemptive scheduling (so a malicious component could
block the system by, e.g., spinning in an infinite loop).
To first order, all component in Tock, including those in the kernel, are mutually distrustful. Inside the
kernel, Tock achieves this with a language-based isolation abstraction called capsules that incurs no
memory or computation overhead. In user-space, Tock uses (more-or-less) a traditional process model
where process are isolated from the kernel and each other using hardware protection mechanisms.
Tock includes three architectural components. A small trusted kernel, written in Rust, implements a
hardware abstraction layer (HAL), scheduler and platform-specific configuration. Other system
components are implemented in one of two protection mechanisms: capsules, which are compiled with
the kernel and use Rust’s type and module systems for safety, and processes, which use the MPU for
protection at runtime.
Capsules
A capsule is a Rust struct and associated functions. Capsules interact with each other directly, accessing
exposed fields and calling functions in other capsules. Trusted platform configuration code initializes
them, giving them access to any other capsules or kernel resources they need. Capsules can protect
internal state by not exporting certain functions or fields.
Capsules run inside the kernel in privileged hardware mode, but Rust’s type and module systems protect
the core kernel from buggy or malicious capsules. Because type and memory safety are enforced at
compile-time, there is no overhead associated with safety, and capsules require minimal error checking.
For example, a capsule never has to check the validity of a reference. If the reference exists, it points to
valid memory of the right type. This allows extremely fine-grained isolation since there is virtually no
overhead to splitting up components.
Rust’s language protection offers strong safety guarantees. Unless a capsule is able to subvert the Rust
type system, it can only access resources explicitly granted to it, and only in ways permitted by the
interfaces those resources expose. However, because capsules are cooperatively scheduled in the same
single-threaded event loop as the kernel, they must be trusted for system liveness. If a capsule panics, or
does not yield back to the event handler, the system can only recover by restarting.
For a capsule to safely allocate memory from a process, the kernel must enforce three properties:
● Allocated memory does not allow capsules to break the type system.
● Capsules can only access pointers to process memory while the process is alive.
● The kernel must be able to reclaim memory from a terminated process.
Tock provides a safe memory allocation mechanism that meets these three requirements through memory
grants. Capsules can allocate data of arbitrary type from the memory of processes that interact with them.
This memory is allocated from the grant segment.
METHODOLOGY
This section of the paper primarily discusses the implementation phase of the project. There are several
things to consider when building an entirely new operating system. The first step in creating our own
operating system kernel is to create a Rust executable that does not link the standard library. This makes it
possible to run Rust code on the bare metal without an underlying operating system.
To write an operating system kernel, we need code that does not depend on any operating system features.
This means that we can't use threads, files, heap memory, the network, random numbers, standard output,
or any other features requiring OS abstractions or specific hardware. Which makes sense, since we're
trying to write our own OS and our own drivers.
This means that we can't use most of the Rust standard library, but there are a lot of Rust features that we
can use. For example, we can use iterators, closures, pattern matching, option and result, string
formatting, and of course the ownership system. These features make it possible to write a kernel in a
very expressive, high level way without worrying about undefined behavior or memory safety.
In order to create an OS kernel in Rust, we need to create an executable that can be run without an
underlying operating system. Such an executable is often called a “freestanding” or “bare-metal”
executable.
The Boot Process
Writing a bootloader can be a challenging process as it requires a great understanding of assembly
language. Therefore we won’t write a seperate bootloader for the OS, instead we are going to use a tool
named bootimage that automatically prepends a bootloader to the kernel.
Target Specification
Cargo(Rust package manager) supports different target systems through the --target parameter. The target
is described by a so-called target triple, which describes the CPU architecture, the vendor, the operating
system, and the ABI. For example, the x86_64-unknown-linux-gnu target triple describes a system with a
x86_64 CPU, no clear vendor and a Linux operating system with the GNU ABI. Rust supports many
different target triples, including arm-linux-androideabi for Android or wasm32-unknown-unknown for
WebAssembly.
For our target system, however, we require some special configuration parameters (e.g. no underlying
OS), so none of the existing target triples fits. Fortunately, Rust allows us to define our own target
through a JSON file. For example, a JSON file that describes the x86_64-unknown-linux-gnu target looks
like this:
Printing to Screen
The easiest way to print text to the screen at this stage is the VGA text buffer. It is a special memory area
mapped to the VGA hardware that contains the contents displayed on screen. It normally consists of 25
lines that each contain 80 character cells. Each character cell displays an ASCII character with some
foreground and background colors. The screen output looks like this:
Testing & Implementation
The major part of the testing and implementation of the project will be done on QEMU. QEMU (short for
Quick Emulator) is a free and open-source emulator that performs hardware virtualization.
QEMU is a hosted virtual machine monitor: it emulates the machine's processor through dynamic binary
translation and provides a set of different hardware and device models for the machine, enabling it to run
a variety of guest operating systems. It also can be used with KVM to run virtual machines at near-native
speed (by taking advantage of hardware extensions such as Intel VT-x). QEMU can also do emulation for
user-level processes, allowing applications compiled for one architecture to run on another.
Reference:
1. Ownership is Theft: Experiences Building an Embedded OS in Rust By Amit Levy, Michael P
Andersen, Bradford Campbell, David Culler, Prabal Dutta, Branden Ghena, Philip Levis, and Pat
Pannuto
2. Reenix: Implementing a Unix-Like Operating System in Rust B y Alex Light
3. System programming in Rust: beyond safety By Brian Anderson, Lars Bergstrom, Manish
Goregaokar, Josh Matthews, Keegan McAllister, Jack Moffitt and Simon Sapin.
4. Hephaestus: a Rust runtime for a distributed operating system By Andrew Scull
5. https://www.tockos.org
6. https://www.rust-lang.org/
7. https://www.redox-os.org/
8. https://www.raspberrypi.org/