Skip to content

Add basic error strings for deserialization errors #280

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ You need a nightly [Rust](https://www.rust-lang.org) compiler with the `llvm-too

## Usage

To use this crate, you need to adjust your kernel to be bootable first. Then you can create a bootable disk image from your compiled kernel. These steps are explained in detail below.

If you're already using an older version of the `bootloader` crate, follow our [migration guides](doc/migration).

### Kernel

To make your kernel compatible with `bootloader`:

- Add a dependency on the `bootloader_api` crate in your kernel's `Cargo.toml`.
Expand All @@ -29,24 +35,37 @@ To make your kernel compatible with `bootloader`:
};
bootloader_api::entry_point!(kernel_main, config = &CONFIG);
```
- Compile your kernel as normal to an ELF executable. The executable will contain a special section with metadata and the serialized config, which will enable the `bootloader` crate to load it.
- Compile your kernel to an ELF executable by running **`cargo build --target x86_64-unknown-none`**. You might need to run `rustup target add x86_64-unknown-none` before to download precompiled versions of the `core` and `alloc` crates.
- Thanks to the `entry_point` macro, the compiled executable contains a special section with metadata and the serialized config, which will enable the `bootloader` crate to load it.

### Booting

To combine your kernel with a bootloader and create a bootable disk image, follow these steps:

- Create a new runner crate, e.g. through `cargo new runner --bin`.
- Add the `bootloader` crate as a `dependency` in the `runner/Cargo.toml`.
- In the `main.rs`, invoke the build commands for your kernel.
- Alternatively, you can set up an [artifact dependency](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies) on your kernel, provided that you use a `rustup`-supported target for your kernel:
```toml
[dependencies]
my-kernel = { path = "..", artifact = "bin", target = "x86_64-unknown-none" }
```
- After building your kernel, obtain the path to the kernel executable.
- When using an artifact dependency, you can retrieve this path using `env!("CARGO_BIN_FILE_MY_KERNEL_my-kernel")`
- Use the `bootloader::create_boot_partition` function to create a bootable FAT partition at some chosen path.
- Use one or multiple `bootloader::create_*_disk_image` functions to transform the bootable FAT partition into a disk image.
- Use the `bootloader::create_uefi_disk_image` function to create an UEFI-compatible GPT-formatted disk image.
- Use the `bootloader::create_bios_disk_image` function to create a BIOS-compatible MBR-formatted disk image.
- Move your full kernel code into a `kernel` subdirectory.
- Create a new `os` crate at the top level that defines a [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html).
- Add a `build-dependencies` on the `bootloader` crate.
- Create a [`build.rs`](https://doc.rust-lang.org/cargo/reference/build-scripts.html) build script.
- Set up an [artifact dependency](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies) to add your `kernel` crate as a `build-dependency`:
```toml
# in Cargo.toml
[build-dependencies]
kernel = { path = "kernel", artifact = "bin", target = "x86_64-unknown-none" }
```
```toml
# .cargo/config.toml

[unstable]
# enable the unstable artifact-dependencies feature, see
# https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies
bindeps = true
```
Alternatively, you can use [`std::process::Command`](https://doc.rust-lang.org/stable/std/process/struct.Command.html) to invoke the build command of your kernel in the `build.rs` script.
- Obtain the path to the kernel executable. When using an artifact dependency, you can retrieve this path using `env!("CARGO_BIN_FILE_MY_KERNEL_my-kernel")`
- Use `bootloader::UefiBoot` and/or `bootloader::BiosBoot` to create a bootable disk image with your kernel.
- Do something with the bootable disk images in your `main.rs` function. For example, run them with QEMU.

See our [disk image creation template](doc/create-disk-image.md) for a more detailed example.

## Architecture

Expand Down
30 changes: 15 additions & 15 deletions api/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,16 @@ impl BootloaderConfig {
/// ELF file.
///
/// TODO: return error enum
pub fn deserialize(serialized: &[u8]) -> Result<Self, ()> {
pub fn deserialize(serialized: &[u8]) -> Result<Self, &'static str> {
if serialized.len() != Self::SERIALIZED_LEN {
return Err(());
return Err("invalid len");
}

let s = serialized;

let (uuid, s) = split_array_ref(s);
if uuid != &Self::UUID {
return Err(());
return Err("invalid UUID");
}

let (version, s) = {
Expand All @@ -169,7 +169,7 @@ impl BootloaderConfig {
let pre = match pre {
[0] => false,
[1] => true,
_ => return Err(()),
_ => return Err("invalid pre version"),
};

let version = ApiVersion {
Expand Down Expand Up @@ -206,27 +206,27 @@ impl BootloaderConfig {
physical_memory: match physical_memory_some {
[0] if physical_memory == [0; 9] => Option::None,
[1] => Option::Some(Mapping::deserialize(&physical_memory)?),
_ => return Err(()),
_ => return Err("invalid phys memory value"),
},
page_table_recursive: match page_table_recursive_some {
[0] if page_table_recursive == [0; 9] => Option::None,
[1] => Option::Some(Mapping::deserialize(&page_table_recursive)?),
_ => return Err(()),
_ => return Err("invalid page table recursive value"),
},
aslr: match alsr {
1 => true,
0 => false,
_ => return Err(()),
_ => return Err("invalid aslr value"),
},
dynamic_range_start: match dynamic_range_start_some {
[0] if dynamic_range_start == [0; 8] => Option::None,
[1] => Option::Some(u64::from_le_bytes(dynamic_range_start)),
_ => return Err(()),
_ => return Err("invalid dynamic range start value"),
},
dynamic_range_end: match dynamic_range_end_some {
[0] if dynamic_range_end == [0; 8] => Option::None,
[1] => Option::Some(u64::from_le_bytes(dynamic_range_end)),
_ => return Err(()),
_ => return Err("invalid dynamic range end value"),
},
};
(mappings, s)
Expand All @@ -242,19 +242,19 @@ impl BootloaderConfig {
minimum_framebuffer_height: match min_framebuffer_height_some {
[0] if min_framebuffer_height == [0; 8] => Option::None,
[1] => Option::Some(u64::from_le_bytes(min_framebuffer_height)),
_ => return Err(()),
_ => return Err("minimum_framebuffer_height invalid"),
},
minimum_framebuffer_width: match min_framebuffer_width_some {
[0] if min_framebuffer_width == [0; 8] => Option::None,
[1] => Option::Some(u64::from_le_bytes(min_framebuffer_width)),
_ => return Err(()),
_ => return Err("minimum_framebuffer_width invalid"),
},
};
(frame_buffer, s)
};

if !s.is_empty() {
return Err(());
return Err("unexpected rest");
}

Ok(Self {
Expand Down Expand Up @@ -509,17 +509,17 @@ impl Mapping {
}
}

fn deserialize(serialized: &[u8; 9]) -> Result<Self, ()> {
fn deserialize(serialized: &[u8; 9]) -> Result<Self, &'static str> {
let (&variant, s) = split_array_ref(serialized);
let (&addr, s) = split_array_ref(s);
if !s.is_empty() {
return Err(());
return Err("invalid mapping format");
}

match variant {
[0] if addr == [0; 8] => Ok(Mapping::Dynamic),
[1] => Ok(Mapping::FixedAddress(u64::from_le_bytes(addr))),
_ => Err(()),
_ => Err("invalid mapping value"),
}
}
}
Expand Down
File renamed without changes.
85 changes: 85 additions & 0 deletions docs/create-disk-image.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Template: Create a Disk Image

The [`bootloader`](https://docs.rs/bootloader/0.11) crate provides simple functions to create bootable disk images from a kernel. The basic idea is to build your kernel first and then invoke a builder function that calls the disk image creation functions of the `bootloader` crate.

A good way to implement this is to move your kernel into a `kernel` subdirectory. Then you can create
a new `os` crate at the top level that defines a [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html). The root package has build-dependencies on the `kernel` [artifact](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies) and on the bootloader crate. This allows you to create the bootable disk image in a [cargo build script](https://doc.rust-lang.org/cargo/reference/build-scripts.html) and launch the created image in QEMU in the `main` function.

The files could look like this:

```toml
# .cargo/config.toml

[unstable]
# enable the unstable artifact-dependencies feature, see
# https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies
bindeps = true
```

```toml
# Cargo.toml

[package]
name = "os" # or any other name
version = "0.1.0"

[build-dependencies]
bootloader = "0.11"
test-kernel = { path = "kernel", artifact = "bin", target = "x86_64-unknown-none" }

[dependencies]
# used for UEFI booting in QEMU
ovmf_prebuilt = "0.1.0-alpha.1"

[workspace]
members = ["kernel"]
```

```rust
// build.rs

fn main() {
// set by cargo, build scripts should use this directory for output files
let out_dir = env::var_os("OUT_DIR").unwrap();
// set by cargo's artifact dependency feature, see
// https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies
let kernel = env!("CARGO_BIN_FILE_KERNEL_kernel");

// create an UEFI disk image (optional)
let uefi_path = out_dir.join("uefi.img");
bootloader::UefiBoot::new(&kernel).create_disk_image(uefi_path).unwrap();

// create a BIOS disk image (optional)
let out_bios_path = out_dir.join("bios.img");
bootloader::BiosBoot::new(&kernel).create_disk_image(uefi_path).unwrap();

// pass the disk image paths as env variables to the `main.rs`
println!("cargo:rustc-env=UEFI_PATH={}", uefi_path.display());
println!("cargo:rustc-env=BIOS_PATH={}", bios_path.display());
}
```

```rust
// src/main.rs

fn main() {
// read env variables that were set in build script
let uefi_path = env!("UEFI_PATH");
let bios_path = env!("BIOS_PATH");

// choose whether to start the UEFI or BIOS image
let uefi = true;

let mut cmd = std::process::Command::new("qemu-system-x86_64");
if uefi {
cmd.arg("-bios").arg(ovmf_prebuilt::ovmf_pure_efi());
cmd.arg("-drive").arg(format!("format=raw,file={uefi_path}"));
} else {
cmd.arg("-drive").arg(format!("format=raw,file={bios_path}"));
}
let mut child = cmd.spawn()?;
child.wait()?;
}
```

Now you should be able to use `cargo build` to create a bootable disk image and `cargo run` to run in QEMU. Your kernel is automatically recompiled when it changes. For more advanced usage, you can add command-line arguments to your `main.rs` to e.g. pass additional arguments to QEMU or to copy the disk images to some path to make it easier to find them (e.g. for copying them to an thumb drive).
41 changes: 41 additions & 0 deletions docs/migration/v0.10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Migration from bootloader `v0.10`

This guide summarizes the steps for migrating from `bootloader v0.10.X` to `bootloader v0.11`.

## Kernel

- Replace the `bootloader` dependency of your kernel with a dependency on the `bootloader_api` crate and adjust the import path in your `main.rs`:
```diff
# in Cargo.toml

-bootloader = { version = "0.10.13" }
+bootloader_api = "0.11"
```
```diff
// in main.rs

-use bootloader::{entry_point, BootInfo};
+use bootloader_api::{entry_point, BootInfo};
```
- If you used optional features, such as `map-physical-memory`, you can enable them again through the `entry_point` macro:
```rust
use bootloader_api::config::{BootloaderConfig, Mapping};

pub static BOOTLOADER_CONFIG: BootloaderConfig = {
let mut config = BootloaderConfig::new_default();
config.mappings.physical_memory = Some(Mapping::Dynamic);
config
};

// add a `config` argument to the `entry_point` macro call
entry_point!(kernel_main, config = &BOOTLOADER_CONFIG);
```
See the [`BootloaderConfig`](https://docs.rs/bootloader_api/0.11/bootloader_api/config/struct.BootloaderConfig.html) struct for all configuration options.

To build your kernel, run **`cargo build --target x86_64-unknown-none`**. Since the `x86_64-unknown-none` target is a Tier-2 target, there is no need for `bootimage`, `cargo-xbuild`, or `xargo` anymore. Instead, you can run `rustup target add x86_64-unknown-none` to download precompiled versions of the `core` and `alloc` crates. There is no need for custom JSON-based target files anymore.

## Booting

The `bootloader v0.11` release simplifies the disk image creation. The [`bootloader`](https://docs.rs/bootloader/0.11) crate now provides simple functions to create bootable disk images from a kernel. The basic idea is to build your kernel first and then invoke a builder function that calls the disk image creation functions of the `bootloader` crate.

See our [disk image creation template](../create-disk-image.md) for a detailed explanation of the new build process.
49 changes: 49 additions & 0 deletions docs/migration/v0.9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Migration from bootloader `v0.9`

This guide summarizes the steps for migrating from `bootloader v0.9.X` to `bootloader v0.11`. Note that some bigger changes are required to support UEFI booting (to support the framebuffer and APIC).

## Kernel

- Replace the `bootloader` dependency of your kernel with a dependency on the `bootloader_api` crate:
```diff
-bootloader = { version = "0.9.23", features = [...]}
+bootloader_api = "0.11"
```
- In your `main.rs`, adjust the import path and change the signature of the entry point function:
```diff
-use bootloader::{entry_point, BootInfo};
+use bootloader_api::{entry_point, BootInfo};

entry_point!(kernel_main);

-fn kernel_main(boot_info: &'static BootInfo) -> ! {
+fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
```
- If you used optional features, such as `map_physical_memory`, you can enable them again through the `entry_point` macro:
```rust
use bootloader_api::config::{BootloaderConfig, Mapping};

pub static BOOTLOADER_CONFIG: BootloaderConfig = {
let mut config = BootloaderConfig::new_default();
config.mappings.physical_memory = Some(Mapping::Dynamic);
config
};

// add a `config` argument to the `entry_point` macro call
entry_point!(kernel_main, config = &BOOTLOADER_CONFIG);
```
See the [`BootloaderConfig`](https://docs.rs/bootloader_api/0.11/bootloader_api/config/struct.BootloaderConfig.html) struct for all configuration options.
- The `v0.11` version of the bootloader sets up a pixel-based framebuffer instead of using the VGA text mode. This means that you have to rewrite your screen output code. See the [`common/logger.rs`](../../common/src/logger.rs) module for an example implementation based on the [`noto-sans-mono-bitmap`](https://docs.rs/noto-sans-mono-bitmap/latest/noto_sans_mono_bitmap/index.html) and [`log`](https://docs.rs/log/latest) crates.
- If you want to use UEFI booting, you cannot use the [legacy PIC](https://wiki.osdev.org/8259_PIC) for interrupt handling. Instead, you have to set up the [`APIC`](https://wiki.osdev.org/APIC). Unfortunately, we don't have a guide for this yet, but the basic steps are:
- The `boot_info.rsdp_addr` field will tell you the physical address of the [`RSDP`](https://wiki.osdev.org/RSDP) structure, which is part of the [`ACPI`](https://en.wikipedia.org/wiki/ACPI) standard. Note that `ACPI` (_Advanced Configuration and Power Interface_) and `APIC` (_Advanced Programmable Interrupt Controller_) are completely different things that just have confusingly similar acronyms.
- Use the [`acpi`](https://docs.rs/acpi/4.1.1/acpi/index.html) crate and its [`AcpiTables::from_rsdp`](https://docs.rs/acpi/4.1.1/acpi/struct.AcpiTables.html#method.from_rsdp) function to load the ACPI tables. Then use the [`platform_info`](https://docs.rs/acpi/4.1.1/acpi/struct.AcpiTables.html#method.platform_info) method and read the [`interrupt_model`](https://docs.rs/acpi/4.1.1/acpi/platform/struct.PlatformInfo.html#structfield.interrupt_model) field of the returned `PlatformInfo`. This field gives you all the necessary information about the system's interrupt controller.
- Parse and set up the local and IO APIC. We're working on a [crate](https://github.com/rust-osdev/apic) for that, but it's still in a very early state. We would love contributions! Alternatively, there are some other crates on crates.io that might work too.
- If you reload the GDT at some point, ensure that all segment registers are written, including `ss` and `ds`. The `v0.9` version of the bootloader used to initialize some of them to 0, but this is no longer the case. If you don't do this, [a general protection fault might happen on `iretq`](https://github.com/rust-osdev/bootloader/issues/196).

To build your kernel, run **`cargo build --target x86_64-unknown-none`**. Since the `x86_64-unknown-none` target is a Tier-2 target, there is no need for `bootimage`, `cargo-xbuild`, or `xargo` anymore. Instead, you can run `rustup target add x86_64-unknown-none` to download precompiled versions of the `core` and `alloc` crates. There is no need for custom JSON-based target files anymore.

## Booting

The `bootloader v0.11` release does not use the `bootimage` tool anymore. Instead, the [`bootloader`](https://docs.rs/bootloader/0.11) crate provides functions to create bootable disk images from a kernel. The basic idea is to build your kernel first and then invoke a builder function that calls the disk image creation functions of the `bootloader` crate.

See our [disk image creation template](../create-disk-image.md) for a detailed explanation of the new build process.