You Too Can Build A Kernel/Hypervisor Part 1 — Preliminaries


About a week or two ago I started following some how-tos on the internet for building kernels and/or hypervisors in Rust. It’s been a fun adventure and I thought I would compile my notes together in case someone wants to follow along or see how an OS starts up. Most of the examples out (probably to keep things simpler…) are based on x86_64 hardware. To spice things up a bit I decided that I’d be targeting aarch64 (ARMv8) instead using QEMU’s virt board.

The intended audience here is someone with a background in programming, but not necessarily kernel programming or C. A familiarity with assembly syntax is helpful, but certainly not required. The same can be said for a working knowledge of Rust. This is mostly a vehicle to talk about all the exciting concepts in lower-level programming as opposed to a “Here’s how to write Rust.”

Some of this work was inspired by Philipp Oppermann’s excellent Writing an OS In Rust (Second Edition ) https://os.phil-opp.com/ . If you would rather see and write a kernel for x86_64 you should go read his instead.

Ash Wilding has an excellent tour of hypervisor concepts on his blog at https://ashw.io/blog/arm64-hypervisor-tutorial/1. He also includes examples of the assembly needed to setup some core functionality on an Arm system.

Preliminaries on OSX (10.14.5)

  • homebrew
  • crosstoolng (1.24.0) via homebrew
  • Rustup if you’re following along with my code (not needed per se)
  • rustup nightly (because we need compiler features hidden behind nightly)

Aside about cross compiling

What’s the deal with a cross compiler?! I already have a compiler.

That’s true, but…

Compilers target a “triple” on their output. The format is machine-vendor- OS. In my case, the default “triple” is x86_64-apple-darwin. That is to say it will use the x86_64 instruction set, on hardware provided by Apple, with the Darwin/XNU ABI (application binary interface).

If I want to do operating system development I have to make a radically different set of assumptions, especially if I’m targeting a different instruction set like aarch64/ARMv8. What we really need is a compiler that can target aarch64-unknown-elf. That is, it will use the aarch64 (ARMv8) instruction set, without caring about hardware, and that the output need only conform to the ELF spec.

Install crosstool-ng & Cross-compiling Toolchain

This has an easy part and a tricky part. The easy part is installing it via homebrew. The tricky part is that crosstool-ng is a toolchain that compiles other toolchains so you have to meet all the requirements of those toolchains too.

You’re probably going to need to create a new volume with Disk Utility for storing the cross compiler toolchain. Most of the compiler toolchains assume a case sensitive filesystem and crosstool-ng won’t even attempt to build (for good reasons) until that’s true.

You’re going to need an APFS volume w/ case sensitivity and probably about 10GB. The final required size will be considerably less, but we need to make sure to have enough space to store all the compilation intermediates.

I followed the instructions at <https://medium.com/coinmonks/setup- gcc-8-1-cross-compiler-toolchain-for-raspberry-pi-3-on-macos-high-sierra- cb3fc8b6443e>

Luckily for OSX 10.14.5 (as of this writing) I didn’t need most of the instructions (for crosstool-ng 1.24.0). I started with creating the volume, skipped to menuconfig, and then went right to build and it all worked.

Once you have the mount point /Volumes/xtool-build-env/ __ you can skip to configuring crosstool-ng .

The key things that need to be set via ct-ng menuconfig:

  • Paths and misc options -> Working directory -> /Volumes/xtool-build-env/.build
  • Paths and misc options -> Prefix directory -> /Volumes/xtool-build-env/${CT_TARGET}
  • Target options -> Target architecture -> ARM
  • Target options -> Endianness -> Little endian
  • Target options -> Bitness -> 64
  • Operating System -> Target OS -> Bare-metal
  • Binary utilities -> binary format -> ELF
  • Debug facilities -> gdb (Not strictly required, but you will regret not having it if you decide to experiment)

Here’s a gist of my local config just in case you decide to dig around the config file and want a reference point.

You should be able to make the toolchain now with ct-ng build. Don’t be like me and do this step at home, or at least with a power cable. It took my relatively recent laptop about 20 minutes of work and 25% of my battery to finish this step.

When it’s done you should have a directory /Volumes/xtool-build- env/aarch64-unknown-elf/bin with a bunch of tools prefixed aarch64-unknown- elf.

Installing Rust

I did this project in Rust, so if you want to build along, you’ll need it. That being said, if you just want the details of how the pieces fit together, feel free to ignore it. Get the Rust toolchain installer and follow the instructions.

The only additional instructions you’ll need are to enable + set nightly, and add the aarch64-unknown-linux-gnu target.

rustup default nightly  
rustup target add aarch64-unknown-linux-gnu

Additional Rust tools

We need a few additional Cargo command line tools that aren’t strictly needed to compile our OS crate, but do need to be on the command line

rustup component add llvm-tools-preview  
cargo install xargo  
cargo install cargo-binutils

Installing QEMU

Super simple: brew install qemu.

Sanity Checks Before Continuing

If you’re trying to get everything working then you should now have (on $PATH):

  • aarch64-unknown-elf-gdb
  • qemu-system-aarch64
  • cargo-size (from cargo-binutils)