Remove some old content

This commit is contained in:
Philipp Oppermann
2021-02-11 17:08:41 +01:00
parent aaae70974f
commit 09f96c9221

View File

@@ -610,185 +610,6 @@ TODO
- Add `cargo disk-image` alias for `cargo run --package boot -- --no-run`
# PREVIOUS:
### Builder Binary
We now have a `create_bootimage` function, but no way to invoke it. Let's fix this by creating a `builder` executable in the `bootimage` crate. For this, we create a new `bin` folder in `bootimage/src` and add a `builder.rs` file with the following content:
```rust
// in bootimage/src/bin/builder.rs
use std::path::PathBuf;
use anyhow::Context;
fn main() -> anyhow::Result<()> {
let kernel_binary = build_kernel().context("failed to build kernel")?;
let bootimage = bootimage::create_bootimage(kernel_binary)
.context("failed to create disk image")?;
println!("Created disk image at `{}`", bootimage.display());
}
fn build_kernel() -> anyhow::Result<PathBuf> {
todo!()
}
```
The entry point of all binaries in Rust is the `main` function. While this function doesn't need a return type, we use the [`anyhow::Result`] type again as a simple way of dealing with errors. The implementation of the `main` method consists of two steps: building our kernel and creating the disk image. For the first step we define a new `build_kernel` function whose implementation we will create in the following. For the disk image creation we use the `create_bootimage` function we created in our `lib.rs`. Since cargo treats the `main.rs` and `lib.rs` as separate crates, we need to prefix the crate name `bootimage` in order to access it.
[`anyhow::Result`]: https://docs.rs/anyhow/1.0.33/anyhow/type.Result.html
One new operation that we didn't see before are the `context` calls. This method is defined in the [`anyhow::Context`] trait and provides a way to add additional messages to errors, which are also printed out in case of an error. This way we can easily see whether an error occurred in `build_kernel` or `create_bootimage`.
[`anyhow::Context`]: https://docs.rs/anyhow/1.0.33/anyhow/trait.Context.html
#### The `build_kernel` Implementation
The purpose of the `build_kernel` method is to build our kernel and return the path to the resulting kernel binary. As we learned in the first part of this post, the build command for our kernel is:
```
cargo build --target x86_64-blog_os.json -Z build-std=core \
-Z build-std-features=compiler-builtins-mem
```
Let's invoke that command using the [`process::Command`] type again:
```rust
// in bootimage/src/bin/builder.rs
fn build_kernel() -> anyhow::Result<PathBuf> {
// we know that the kernel lives in the parent directory
let kernel_dir = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let command_line_args: Vec<String> = std::env::args().skip(1).collect();
let mut cmd = Command::new(env!("CARGO"));
cmd.args(&[
"--target", "x86_64-blog_os.json",
"-Z", "build-std=core",
"-Z", "build-std-features=compiler-builtins-mem",
]);
cmd.args(&command_line_args);
cmd.current_dir(kernel_dir);
let exit_status = cmd.status()?;
if exit_status.success() {
let profile = if command_line_args.contains("--release") {
"release"
} else {
"debug"
};
Ok(
kernel_dir.join("target").join("x86_64-blog_os").join(profile)
.join("blog_os")
)
} else {
Err(anyhow::Error::msg("kernel build failed"))
}
}
```
Before constructing the command, we use the [`CARGO_MANIFEST_DIR`] environment variable again to determine the path to the kernel directory. We also retrieve the command line arguments passed to the `builder` executable by using the [`std::env::args`] function. Since the first command line argument is always the executable name, which we don't need, we use the [`Iterator::skip`] method to skip it. Then we use the [`Iterator::collect`] method to transform the iterator into a [`Vec`] of strings.
[`std::env::args`]: https://doc.rust-lang.org/std/env/fn.args.html
[`Iterator::skip`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.skip
[`Iterator::collect`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect
[`Vec`]: https://doc.rust-lang.org/std/vec/struct.Vec.html
Instead of [`Command::arg`], we use the [`Command::args`] method as a less verbose way to pass multiple string arguments at once. In addition to the build arguments, we also pass all the command line argument passed to the `builder` executable. This way, it is possible to pass additional command line arguments, for example `--release` to compile the kernel with optimizations. Similar to the bootloader build, we also use the [`Command::current_dir`] method to run the command in the root directory, which is required for finding the `x86_64-blog_os.json` file.
[`Command::args`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.args
After running the command and checking its exit status, we construct the path to the kernel binary. When compiling for a custom target, cargo places the executable inside a `target/<target-name>/<profile>/<name>` folder where `<target-name>` is the name of the custom target file, `<profile>` is either [`debug`] or [`release`], and `<name>` is the executable name. In our case, the target name is `x86_64-blog_os` and the executable name is `blog_os`. To determine whether it is a debug or release build, we looks through the `command_line_args` vector for a `--release` argument.
[`debug`]: https://doc.rust-lang.org/cargo/reference/profiles.html#dev
[`release`]: https://doc.rust-lang.org/cargo/reference/profiles.html#release
#### Running it
We can now run our `builder` binary using the following command:
```
cargo run --package bootimage --bin builder
```
The `--package bootimage` argument is optional when you run the command from within the `bootimage` directory. After running the command, you should see the `bootimage-*` files in your `target/x86_64-blog_os/debug` folder.
To pass additional arguments to the `builder` executable, you have to pass them after a special separator argument `--`, otherwise they are interpreted by the `cargo run` command. As an example, you have to run the following command to build the kernel in release mode:
```
cargo run --package bootimage --bin builder -- --release
```
Without the additional `--` argument, only the `builder` executable is built in release mode, not the kernel. To verify that the `--release` argument worked, you can verify that the kernel executable and the disk image files are available in the `target/x86_64-blog_os/release` folder.
#### Adding an Alias
Since we will need to run this `builder` executable quite often, it makes sense to add a shorter alias for the above command. To do that, we create a [cargo configuration file] at the root directory of our project. Cargo configuration files are named `.cargo/config.toml` and allow configuring the behavior of cargo itself. Among other things, they allow to define subcommand aliases to avoid typing out long commands. Let's use this feature to define a `cargo disk-image` alias for the above command:
[cargo configuration file]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[alias]
disk-image = ["run", "--package", "bootimage", "--bin builder", "--"]
```
Now we can run `cargo disk-image` instead of using the long build command. Since we already included the separator argument `--` in the argument list, we can pass additional arguments directly. For example, a release build is now a simple `cargo disk-image --release`.
You can of course choose a different alias name if you like. You can also add a one character alias (e.g. `cargo i`) if you want to minimize typing.
### Using `cargo run`
TODO:
- real machine
### Simplify Build Commands
TODO:
- xbuild/xrun aliases
- .cargo/config.toml files -> using not possible because of cargo limitations
There multiple ways to work with an `Option` types:
- Use the [`unwrap`] or [`expect`] methods to extract the inner value if present and [`panic`] otherwise.
- Use a use a [`match`] statement and [pattern matching] to deal with the `Some` and `None` cases individually.
- Use an [`if let`] statement to conditionally run some code if the `Option` is `Some`. This is equivalent to a `match` statement with an empty arm on `None`.
- Use the [`ok_or`]/[`ok_or_else`] methods to convert the `Option` to a `Result`.
@@ -815,19 +636,6 @@ For running `bootimage` and building the bootloader, you need to have the `llvm-
After executing the command, you should see a bootable disk image named `bootimage-blog_os.img` in your `target/x86_64-blog_os/debug` directory. You can boot it in a virtual machine or copy it to an USB drive to boot it on real hardware. (Note that this is not a CD image, which have a different format, so burning it to a CD doesn't work).
#### How does it work?
The `bootimage` tool performs the following steps behind the scenes:
- It compiles our kernel to an [ELF] file.
- It compiles the bootloader dependency as a standalone executable.
- It links the bytes of the kernel ELF file to the bootloader.
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
When booted, the bootloader reads and parses the appended ELF file. It then maps the program segments to virtual addresses in the page tables, zeroes the `.bss` section, and sets up a stack. Finally, it reads the entry point address (our `_start` function) and jumps to it.
@@ -843,97 +651,6 @@ When booted, the bootloader reads and parses the appended ELF file. It then maps
#### Set a Default Target
To avoid passing the `--target` parameter on every invocation of `cargo build`, we can override the default target. To do this, we add the following to our [cargo configuration] file at `.cargo/config.toml`:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
This tells `cargo` to use our `x86_64-blog_os.json` target when no explicit `--target` argument is passed. This means that we can now build our kernel with a simple `cargo build`. For more information on cargo configuration options, check out the [official documentation][cargo configuration].
We are now able to build our kernel for a bare metal target with a simple `cargo build`. However, our `_start` entry point, which will be called by the boot loader, is still empty. It's time that we output something to screen from it.
### 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:
[VGA text buffer]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
![screen output for common ASCII characters](https://upload.wikimedia.org/wikipedia/commons/f/f8/Codepage-437.png)
We will discuss the exact layout of the VGA buffer in the next post, where we write a first small driver for it. For printing “Hello World!”, we just need to know that the buffer is located at address `0xb8000` and that each character cell consists of an ASCII byte and a color byte.
The implementation looks like this:
```rust
static HELLO: &[u8] = b"Hello World!";
#[no_mangle]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
First, we cast the integer `0xb8000` into a [raw pointer]. Then we [iterate] over the bytes of the [static] `HELLO` [byte string]. We use the [`enumerate`] method to additionally get a running variable `i`. In the body of the for loop, we use the [`offset`] method to write the string byte and the corresponding color byte (`0xb` is a light cyan).
[iterate]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[raw pointer]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
Note that there's an [`unsafe`] block around all memory writes. The reason is that the Rust compiler can't prove that the raw pointers we create are valid. They could point anywhere and lead to data corruption. By putting them into an `unsafe` block we're basically telling the compiler that we are absolutely sure that the operations are valid. Note that an `unsafe` block does not turn off Rust's safety checks. It only allows you to do [five additional things].
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
[five additional things]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#unsafe-superpowers
I want to emphasize that **this is not the way we want to do things in Rust!** It's very easy to mess up when working with raw pointers inside unsafe blocks, for example, we could easily write beyond the buffer's end if we're not careful.
So we want to minimize the use of `unsafe` as much as possible. Rust gives us the ability to do this by creating safe abstractions. For example, we could create a VGA buffer type that encapsulates all unsafety and ensures that it is _impossible_ to do anything wrong from the outside. This way, we would only need minimal amounts of `unsafe` and can be sure that we don't violate [memory safety]. We will create such a safe VGA buffer abstraction in the next post.
[memory safety]: https://en.wikipedia.org/wiki/Memory_safety
## Running our Kernel
Now that we have an executable that does something perceptible, it is time to run it. First, we need to turn our compiled kernel into a bootable disk image by linking it with a bootloader. Then we can run the disk image in the [QEMU] virtual machine or boot it on real hardware using a USB stick.
### Booting it in QEMU
We can now boot the disk image in a virtual machine. To boot it in [QEMU], execute the following command:
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.img
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
```
This opens a separate window with that looks like this:
![QEMU showing "Hello World!"](qemu.png)
We see that our "Hello World!" is visible on the screen.
### Real Machine
It is also possible to write it to an USB stick and boot it on a real machine: