Bare Metal
OS in Rust
Announcements
- Action Items:
mallocstands eternal- Possibly the coolest assignment ever
- I’m glad you all love it
- Homework this week is more complicated but also more supported.
- Prioritize
mallocI think.
- Prioritize
Today
- Bare metal
- Bare metal
- Ranting about how cool this is
- Simulation
- Emulation
- A bare metal binary
- Runtimes
std
Citations
- Outright theft:
Motivation
Throwback ThMonday
- I did this in grad school.
- My researcher (Juni) did this in ’22
- I did this… last week.
Bare Metal
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
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.
- Supports both emulation and virtualization
- 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,
optionandresult- Recall the official Calvin Deutschbein position on
optionandresult: - “It’s why Rust is good.” - me
- Recall the official Calvin Deutschbein position on
On std
- While not initially helpful, we can use string formatting, and…
- … the ownership system.
- Beats
malloc!
- Beats
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_stdattribute.
To begin
- We can start creation the same way we make anything else in Rust…
- Cargo (sighs heavily)
Example
- Personally, I would expect you to maintain different OS versions with same crate name but within distinctly named directories (
32,42etc.)
Branches
- A competing formulation would be to use
git branchto 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
- You don’t have to name your OS anything in particular, I just thought
osirs(OS in Rust) sounded heckin’ rad. - ʙᴏᴡ ᴅᴏᴡɴ ʙᴇғᴏʀᴇ ᴛʜᴇ ɢᴏᴅ ᴏғ ᴅᴇᴀᴛʜ
- Also a cringe AI real estate firm!
![]()
Refresh
- When we run the command, cargo creates the following directory structure for us:
Recall
Cargo.tomlcontains the crate configuration- Crate name
- Crate author
- Crate version
src/main.rsfile contains ourmainfunction.- After
cargo build, find the compiledosirsbinary in thetarget/debugsubfolder.
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 filesRunning
- Technically no one can stop you from using
cargo runor evencargo run --release - But you can also just
buildand then directly run the executable.
The no_std Attribute
- Initially, the crate implicitly links the standard library.
- We can prepend the
no_stdattribute tosrc/main.rsto get the version of Rust that builds character!
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
printlnmacro is part of the standard librarystd. - 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:
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 errorsEnhance!
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
stdand is sad 😭
Panic
- The
panic_handlerattribute defines the function that the compiler should invoke when a panic occurs. stdprovides its own panic handler function, but in ano_stdenvironment we need to define it ourselves:- panic
Our Approach
Panics
- The
PanicInfoparameter 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…
- I had never heard of or used these but to me it was clear why they would have to exist in a type safe language.
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
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:
Now…
This sets the panic strategy to
abortfor both thedevprofile (used forcargo build) and thereleaseprofile (used forcargo build --release).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!