diff --git a/README.md b/README.md index 6050792f..8099df4c 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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 diff --git a/api/src/config.rs b/api/src/config.rs index 4f69174c..5ca235ab 100644 --- a/api/src/config.rs +++ b/api/src/config.rs @@ -149,16 +149,16 @@ impl BootloaderConfig { /// ELF file. /// /// TODO: return error enum - pub fn deserialize(serialized: &[u8]) -> Result { + pub fn deserialize(serialized: &[u8]) -> Result { 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) = { @@ -169,7 +169,7 @@ impl BootloaderConfig { let pre = match pre { [0] => false, [1] => true, - _ => return Err(()), + _ => return Err("invalid pre version"), }; let version = ApiVersion { @@ -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) @@ -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 { @@ -509,17 +509,17 @@ impl Mapping { } } - fn deserialize(serialized: &[u8; 9]) -> Result { + fn deserialize(serialized: &[u8; 9]) -> Result { 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"), } } } diff --git a/doc/chainloading.md b/docs/chainloading.md similarity index 100% rename from doc/chainloading.md rename to docs/chainloading.md diff --git a/docs/create-disk-image.md b/docs/create-disk-image.md new file mode 100644 index 00000000..67302b4a --- /dev/null +++ b/docs/create-disk-image.md @@ -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). diff --git a/docs/migration/v0.10.md b/docs/migration/v0.10.md new file mode 100644 index 00000000..7536f231 --- /dev/null +++ b/docs/migration/v0.10.md @@ -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. diff --git a/docs/migration/v0.9.md b/docs/migration/v0.9.md new file mode 100644 index 00000000..856b2154 --- /dev/null +++ b/docs/migration/v0.9.md @@ -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.