From zero to _start in detail
We now have a little kernel. But we used a lot of tools to make it happen. What do these tools actually do? This section is titled "in detail," but it's really "in more detail than we've seen thus far." We're going to talk about what happens, so you can dig in deeper if you're interested, but we can't possibly cover every single bit in depth.
Here's the basic set of steps:
- We write our kernel code.
- We build it with
bootimage build.- This invokes
cargo-xbuildto cross-compilelibcore. - It then invokes
cargoto take our configuration and set up a build. - Cargo invokes
rustcto actually build the code. - It then takes a precompiled 'bootloader' and our kernel and makes a
.binfile.
- This invokes
- We run it with
bootimage run.- This takes our
.binfile, and runs Qeumu, using that.binas its hard drive.
- This takes our
Whew! That's a lot of stuff. It's not completely different from writing any Rust program, however: you write code, you build it, then you run it. Easy peasy.
Let's dig in a bit!
We write our kernel code
This is the most straightforward step. Write the code! There is some subtlety here, but we talked about it earlier in the chapter: we have to cross-compile to our new platform. We remove the standard library. We configure both the panic handler and the behavior for when a panic happens.
The rest of this book is largely about what to do during this step of development, so we won't belabor it here. You type some code, hit save. Done.
Building our code with bootimage build
Normally, we build Rust code with cargo build, but for an OS, we use
bootimage instead. This is a tool written by Phil Opperman (who we mentioned
in the preface), and it wraps up another tool, also written by Phil,
cargo-xbuild. That tool wraps Cargo. So, in the end, running bootimage build is not too far away from running cargo build conceptually; it's mostly
that Cargo isn't extensible in the way we need at the moment, so we have
to wrap it.
Invoking cargo-xbuild to cross-compile libcore
cargo-xbuild's job is to cross compile Rust's core library. You see, Rust
has an interesting relationship between the language and libraries: some
important parts of the language are implemented as a library, not as a
built-in thing. These foundations, and some other goodies, are included in
the core library. So, before we can build our code, we need to build a copy
of core for our OS. cargo-xbuild makes this easy: it knows how to ask
rustup for a copy of core's source code, and then builds it with our
custom target JSON.
Invoking cargo to take our configuration and set up a build
Now that core is built, we can build our code! Cargo is the tool
in Rust for this task, so cargo-xbuild calls on it to do so. It
passes along our custom target JSON to make sure that we're outputting
a binary for the correct target.
Invoking rustc to build the code
Cargo doesn't actually build our code: it invokes rustc, the Rust compiler,
to actually do the building. Right now our OS is very simple, but as it
grows, and as we split our code into packages, and use external packages,
it's much nicer to let Cargo handle calling rustc rather than doing it by
hand.
Creating a .bin file
Now that we have our OS compiled, we need to prepare it for running. To do
so, bootimage creates a special file, called a .bin file. The .bin
stands for "binary", and it has no real format. It's just a bunch of binary
code. There's no structure, headings, layout, nothing. Just a big old bag of
bits.
However, that doesn't mean that what's in there is random. You see, when you start up your computer, something called the BIOS runs first. The BIOS is all-but hard-coded into your motherboard, and it runs some diagnostic checks to make sure that everything is in order. It then runs the 'bootloader'.
This is either the most interesting or most boring part of this whole enterprise. Almost all of this is piles and piles of backwards compatibility hacks. Since early computers were very small, the bootloader only gets to have 256 bytes of stuff inside it. The eventual goal is to run your OS, but there's a few other possibilities. For example, maybe you have more than one OS on your computer, so the bootloader invokes a program that lets you choose between them. Additionally, even on today's high-powered CPUs, when the bootloader is invoked, they're in a backwards-compatible mode that makes them think they're a processor from the 70s. That's right, we basically didn't ever change the foundations here, simply piled new things on top. "Oh, you think you're an 8-bit computer? Let's set up 16-bit mode. Oh, now you think you're a 16-bit computer? Let's set up 32-bit mode. Oh, now you think you're a 32-bit computer? Let's set up 64-bit mode." And then we can finally start our OS.
You may be wondering, "How does the bootloader do all this in only 256 bytes? This quesiton itself is like 90 bytes!" The answer? Compatibility hacks. Virutally all bootloaders today are multiple stages: the first tiny bootloader sets up a secondary bootloader, and that one then can be larger and do more work.
bootimage has a custom-written bootloader that puts your CPU into
64-bit mode, then calls the _start function of our OS. It assembles
the bootloader's code and our OSs' code into that one .bin file.
Running our code with bootimage run
bootimage run takes our .bin file and passes it to Qemu, the emulator
we discussed earlier in the chapter. Qemu uses the .bin file as the
hard drive, and so when it starts up, its BIOS calls the bootloader
which calls our kernel. It also emulates the screen, so when we start
printing stuff to the screen, we'll see it pop up!
Summary
There's more to explore here, but for now, we're not going to worry about this stuff. It's very platform-specific, and mostly papering over legacy. Instead, let's move forward and make our kernel actually do stuff, not worry about how to put a processor into a specific mode.
In the next chapter, we'll print some characters on the screen!