Bare Metal

OS in Rust

Announcements

  • Action Items:
    • malloc stands eternal
      • Possibly the coolest assignment ever
      • I’m glad you all love it
    • Homework this week is more complicated but also more supported.
      • Prioritize malloc I think.

Today

  • Bare metal
    • Bare metal
    • Ranting about how cool this is
    • Simulation
    • Emulation
  • A bare metal binary
    • Runtimes
    • std

Citations

Motivation

Throwback ThMonday

Bare Metal

A computer which has no operating system. The software executed by a bare machine, commonly called a “bare metal program” or “bare metal application”, is designed to interact directly with hardware. Bare machines are widely used in embedded systems, particularly in cases where resources are limited or high performance is required.

Some Terms

  • Should introduce a few terms.
    • Not required for OS but useful to know.
  • Terms
    • Simulation
    • Emulation
    • Cross-compilation
    • QEMU

Simulation

  • Models a system’s behavior
  • Focuses on high-level results, not internal logic
  • “Close enough” for performance or logic testing
  • Simulating hardware components in software
  • Faster but less precise

Emulation

  • Replicating exact internal behavior of hardware
  • Software acting as hardware (CPU, registers, memory)
  • Accuracy vs. Speed:
    • Much slower than simulation… unless?
    • Goal: Guest software doesn’t know it’s not on actual silicon

Example

  • x86-64 - the Intel/AMD architecture common for Linux and Windows - supports a “long double” float with 80 bits of precision.
  • ARM64 - a competing formulation most popularized as “Apple silicon” with the M1 - lacks long doubles.

Context

  • It is trivial to implement a float in software…
  • It is relatively not-trivial to implement an architecture in software, though possible.
    • Here’s the smallest example I know:
    • PicoRV32

Cross-Compiling

  • Example:” compiling code on an x86 laptop for an ARM chip
    • Common in embedded applications (e.g. program a thermostat)
  • Compiler runs on Host A, produces binary for Target B
    • Essential for “bare metal” development
    • Transmit the binary over wires (the bus!) to another device’s memory.

Considerations

  • We must:
    • Target a specific architecture
    • Link against specific hardware memory maps
  • Somehow you also need the actual hardware, unless…

QEMU

  • I used this a lot in grad school; less now.
    • Supports both emulation and virtualization
      • Virtualization emulates a device rather than a binary.
      • Used in cloud; can be fast; huge research area.
  • Let’s us run bare metal binaries without physical chips (which would cost $)
  • Write locally \(\rightarrow\) Cross-compile \(\rightarrow\) Run in QEMU

Binary

First Steps

  • 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.

Introduction

  • 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 files, the heap, networks, random numbers, standard output, etc.
  • We’re trying to write our own OS!

On std

  • 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
    • Recall the official Calvin Deutschbein position on option and result:
    • “It’s why Rust is good.” - me

On std

  • While not initially helpful, we can use string formatting, and…
  • … the ownership system.
    • Beats malloc!

Quoth The Blog

These features make it possible to write a kernel in a very expressive, high level way without worrying about undefined behavior or memory safety.

  • Well, we’ll see.

We keep

Onward!

  • We now enumerate the necessary steps to create a freestanding Rust binary…
  • …and explains why the steps are needed.

no_std

  • By default, all Rust crates link the standard library.
    • It depends on the operating system for features such as threads, files, or networking.
    • It also depends on the C standard library libc, which closely interacts with OS services.
      • That is, part of GNU but not part of Linux.

no_std

  • Since our plan is to write an operating system, we can’t use any OS-dependent libraries.
    • That would be recursive, which is only good sometimes!
  • So we have to disable the automatic inclusion of the standard library through the no_std attribute.

To begin

  • We can start creation the same way we make anything else in Rust…
  • Cargo (sighs heavily)
cargo new ???? 

Example

  • Personally, I would expect you to maintain different OS versions with same crate name but within distinctly named directories (32,42 etc.)
cargo new 32 --name osirs --vcs none

Branches

  • A competing formulation would be to use git branch to create different developmental branches.
  • This is the industry standard and I wanted to introduce it but felt a time crunch.
  • If you are looking for something to do, figure it out.
    • Don’t worry about me finding things; its worth it to me for you to learn.

Name

Refresh

  • When we run the command, cargo creates the following directory structure for us:
$ tree
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

Recall

  • Cargo.toml contains the crate configuration
    • Crate name
    • Crate author
    • Crate version
  • src/main.rs file contains our main function.
  • After cargo build, find the compiled osirs binary in the target/debug subfolder.

Blah blah blah

$ cargo build ; tree
   Compiling osirs v0.1.0 (/home/user/tmp/32)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
    └── debug
        ├── build
        ├── deps
        │   ├── osirs-43412975b38d059d
        │   └── osirs-43412975b38d059d.d
        ├── examples
        ├── incremental
        │   └── osirs-3gae52yq1943v
        │       ├── s-hfeunnoewq-0c8luu5-5dmhke08rl6h5l09ku3va3gkx
        │       │   ├── 1tq3ts5gahvv7j1hzrmfdrzi6.o
        │       │   ├── 6zk3flo890c0qhh6fykb6746g.o
        │       │   ├── 8z45o15v3gxm5hydv3o63x07l.o
        │       │   ├── 9itjtn00r7d8c6mknmav20oex.o
        │       │   ├── bh9pj42wzikjd1ilqutnjbrx7.o
        │       │   ├── dep-graph.bin
        │       │   ├── eymyqxruzdb24suchgzd8ygxb.o
        │       │   ├── query-cache.bin
        │       │   └── work-products.bin
        │       └── s-hfeunnoewq-0c8luu5.lock
        ├── osirs
        └── osirs.d

9 directories, 18 files

Running

  • Technically no one can stop you from using cargo run or even cargo run --release
  • But you can also just build and then directly run the executable.
$ ./target/debug/osirs
Hello, world!

The no_std Attribute

  • Initially, the crate implicitly links the standard library.
  • We can prepend the no_std attribute to src/main.rs to get the version of Rust that builds character!
src/main.rs
// main.rs

#![no_std]

fn main() {
    println!("ʙᴏᴡ ᴅᴏᴡɴ ʙᴇғᴏʀᴇ ᴛʜᴇ ɢᴏᴅ ᴏғ ᴅᴇᴀᴛʜ");
}

We Can Rebuild

  • Actually we can’t.
$ cargo build
   Compiling osirs v0.1.0 (/home/user/tmp/32)
error: cannot find macro `println` in this scope
 --> src/main.rs:6:5
  |
6 |     println!("ʙᴏᴡ ᴅᴏᴡɴ ʙᴇғᴏʀᴇ ᴛʜᴇ ɢᴏᴅ ᴏғ ᴅᴇᴀᴛʜ");
  |     ^^^^^^^

error: `#[panic_handler]` function required, but not found

error: unwinding panics are not supported without std
  |
  = help: using nightly cargo, use -Zbuild-std with panic="abort" to avoid unwinding
  = note: since the core library is usually precompiled with panic="unwind", rebuilding your crate with panic="abort" may not be enough to fix the problem

error: could not compile `osirs` (bin "osirs") due to 3 previous errors    ^^^^^^^

Enhance!

error: cannot find macro `println` in this scope
 --> src/main.rs:6:5
  |
6 |     println!("ʙᴏᴡ ᴅᴏᴡɴ ʙᴇғᴏʀᴇ ᴛʜᴇ ɢᴏᴅ ᴏғ ᴅᴇᴀᴛʜ");
  |     ^^^^^^^
  • Oh right, we can’t print without an OS.

Background

  • The println macro is part of the standard library std.
  • We said no_std.
  • So we can no longer print things.
  • I hope it is clear how this is character-building!
  • Read more:

Rip it

  • Remove the printing and try again:
src/main.rs
// main.rs

#![no_std]

fn main() {
    // println!("ʙᴏᴡ ᴅᴏᴡɴ ʙᴇғᴏʀᴇ ᴛʜᴇ ɢᴏᴅ ᴏғ ᴅᴇᴀᴛʜ");
}

Problems remain

$ cargo build
   Compiling osirs v0.1.0 (/home/user/tmp/32)
error: `#[panic_handler]` function required, but not found

error: unwinding panics are not supported without std
  |
  = help: using nightly cargo, use -Zbuild-std with panic="abort" to avoid unwinding
  = note: since the core library is usually precompiled with panic="unwind", rebuilding your crate with panic="abort" may not be enough to fix the problem

error: could not compile `osirs` (bin "osirs") due to 2 previous errors

Enhance!

error: cannot find macro `println` in this scope
 --> src/main.rs:6:5
  |
6 |     println!("ʙᴏᴡ ᴅᴏᴡɴ ʙᴇғᴏʀᴇ ᴛʜᴇ ɢᴏᴅ ᴏғ ᴅᴇᴀᴛʜ");
  |     ^^^^^^^
  • Sometimes, Rust explodes and calls the OS (written in C!) for help.
  • It can’t do that without std and is sad 😭

Panic

  • The panic_handler attribute defines the function that the compiler should invoke when a panic occurs.
  • std provides its own panic handler function, but in a no_std environment we need to define it ourselves:
  • panic

Our Approach

src/main.rs
// main.rs

/// This function is called on panic.
#[panic_handler]
#[allow(unconditional_recursion)]
fn panic(info: &core::panic::PanicInfo) -> ! {
    panic(info)
}

Panics

  • The PanicInfo parameter contains:
    • file and line where the panic happened
    • panic message (e.g. panic!("YOLO")
  • The function should never return.
    • So it is marked as a “diverging function”
    • It returns the “never” type !.
  • Not much we can do in this function for now, so just recurse to prevent a return.

Read more…

Retry

  • I bet it works now!
$ cargo build
   Compiling osirs v0.1.0 (/home/user/tmp/32)
error: unwinding panics are not supported without std
  |
  = help: using nightly cargo, use -Zbuild-std with panic="abort" to avoid unwinding
  = note: since the core library is usually precompiled with panic="unwind", rebuilding your crate with panic="abort" may not be enough to fix the problem

error: could not compile `osirs` (bin "osirs") due to 1 previous error
  • They should make a version of the OS class that is easy.
    • (They did - this class)

Panic abort

  • Fun fact - back when I was an OS engineer slash rocket scientist my first launch was “PA-1” for “Pad Abort 1”
    • Blew up a rocket on the launch pad to make sure it was safe for humans.

Panic abort

  • The use of the term “abort” which in some nation-states is a hot-button political issue has come up from time-to-time in the discourse.
  • Read more from 2018

Quoth Stallman

“The point of this joke is even more important now than it was when I first wrote it,” [Free Software Foundation president] Stallman wrote in a note posted to project mailing list, in reference to today’s political climate. “Please do not remove it. GNU is not a purely technical project, so the fact that this is not strictly and grimly technical is not a reason to remove this.”

Now in Rust

  • We can oppose fascism and
    • (looks into the history of NASA and Lockheed Martin)
    • (clears throat)
    • Moving on!
  • We can abort programs… in Rust

How?

  • Read carefully:
= help: using nightly cargo, use -Zbuild-std with panic="abort" to avoid unwinding
= note: since the core library is usually precompiled with panic="unwind", rebuilding your crate with panic="abort" may not be enough to fix the problem
  • Geniuses will recognize panic="abort" syntax

TOML

  • It’s .toml
Cargo.toml
[package]
name = "osirs"
version = "0.1.0"
edition = "2024"

[dependencies]

Crate options

  • Nominally there are use cases for which unwinding is undesirable
    • My take: All cases.
  • So Rust provides an option to “abort on panic” instead.
  • Our reference materials claims this disables the generation of unwinding symbol information and thus considerably reduces binary size.
    • I could not verify this independently.

Update Cargo.toml

  • Add the following:
Cargo.toml
[package]
name = "osirs"
version = "0.1.0"
edition = "2024"

[dependencies]

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

Now…

  • This sets the panic strategy to abort for both the dev profile (used for cargo build) and the release profile (used for cargo build --release).

  • abort on panic

  • I bet it will work now.

Whoops!

$ cargo build
   Compiling osirs v0.1.0 (/home/user/tmp/32)
error: using `fn main` requires the standard library
  |
  = help: use `#![no_main]` to bypass the Rust generated entrypoint and declare a platform specific entrypoint yourself, usually with `#[no_mangle]`

error: could not compile `osirs` (bin "osirs") due to 1 previous error
  • We don’t have and can’t use a main!

To be continued

  • I am 99% sure we run out of time here…
  • And will continue with the lab on linker errors!