|
| 1 | +# Migration from bootloader `v0.9` |
| 2 | + |
| 3 | +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). |
| 4 | + |
| 5 | +## Kernel |
| 6 | + |
| 7 | +- Replace the `bootloader` dependency of your kernel with a dependency on the `bootloader_api` crate: |
| 8 | + ```diff |
| 9 | + -bootloader = { version = "0.9.23", features = [...]} |
| 10 | + +bootloader_api = "0.11" |
| 11 | + ``` |
| 12 | +- In your `main.rs`, adjust the import path and change the signature of the entry point function: |
| 13 | + ```diff |
| 14 | + -use bootloader::{entry_point, BootInfo}; |
| 15 | + +use bootloader_api::{entry_point, BootInfo}; |
| 16 | + |
| 17 | + entry_point!(kernel_main); |
| 18 | + |
| 19 | + -fn kernel_main(boot_info: &'static BootInfo) -> ! { |
| 20 | + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { |
| 21 | + ``` |
| 22 | +- If you used optional features, such as `map_physical_memory`, you can enable them again through the `entry_point` macro: |
| 23 | + ```rust |
| 24 | + use bootloader_api::config::{BootloaderConfig, Mapping}; |
| 25 | + |
| 26 | + pub static BOOTLOADER_CONFIG: BootloaderConfig = { |
| 27 | + let mut config = BootloaderConfig::new_default(); |
| 28 | + config.mappings.physical_memory = Some(Mapping::Dynamic); |
| 29 | + config |
| 30 | + }; |
| 31 | + |
| 32 | + // add a `config` argument to the `entry_point` macro call |
| 33 | + entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); |
| 34 | + ``` |
| 35 | + See the [`BootloaderConfig`](https://docs.rs/bootloader_api/0.11/bootloader_api/config/struct.BootloaderConfig.html) struct for all configuration options. |
| 36 | +- 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. |
| 37 | +- 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: |
| 38 | + - 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. |
| 39 | + - 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. |
| 40 | + - 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. |
| 41 | +- 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). |
| 42 | + |
| 43 | +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. |
| 44 | + |
| 45 | +## Booting |
| 46 | + |
| 47 | +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. |
| 48 | + |
| 49 | +A good way to implement this is to move your kernel into a `kernel` subdirectory. Then you can create |
| 50 | +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. |
| 51 | + |
| 52 | +The files could look like this: |
| 53 | + |
| 54 | +```toml |
| 55 | +# .cargo/config.toml |
| 56 | + |
| 57 | +[unstable] |
| 58 | +# enable the unstable artifact-dependencies feature, see |
| 59 | +# https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies |
| 60 | +bindeps = true |
| 61 | +``` |
| 62 | + |
| 63 | +```toml |
| 64 | +# Cargo.toml |
| 65 | + |
| 66 | +[package] |
| 67 | +name = "os" # or any other name |
| 68 | +version = "0.1.0" |
| 69 | + |
| 70 | +[build-dependencies] |
| 71 | +bootloader = "0.11" |
| 72 | +test-kernel = { path = "kernel", artifact = "bin", target = "x86_64-unknown-none" } |
| 73 | + |
| 74 | +[dependencies] |
| 75 | +# used for UEFI booting in QEMU |
| 76 | +ovmf_prebuilt = "0.1.0-alpha.1" |
| 77 | + |
| 78 | +[workspace] |
| 79 | +members = ["kernel"] |
| 80 | +``` |
| 81 | + |
| 82 | +```rust |
| 83 | +// build.rs |
| 84 | + |
| 85 | +fn main() { |
| 86 | + // set by cargo, build scripts should use this directory for output files |
| 87 | + let out_dir = env::var_os("OUT_DIR").unwrap(); |
| 88 | + // set by cargo's artifact dependency feature, see |
| 89 | + // https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies |
| 90 | + let kernel = env!("CARGO_BIN_FILE_KERNEL"); |
| 91 | + |
| 92 | + // create an UEFI disk image (optional) |
| 93 | + let uefi_path = out_dir.join("uefi.img"); |
| 94 | + bootloader::UefiBoot::new(&kernel).create_disk_image(uefi_path).unwrap(); |
| 95 | + |
| 96 | + // create a BIOS disk image (optional) |
| 97 | + let out_bios_path = out_dir.join("bios.img"); |
| 98 | + bootloader::BiosBoot::new(&kernel).create_disk_image(uefi_path).unwrap(); |
| 99 | + |
| 100 | + // pass the disk image paths as env variables to the `main.rs` |
| 101 | + println!("cargo:rustc-env=UEFI_PATH={}", uefi_path.display()); |
| 102 | + println!("cargo:rustc-env=BIOS_PATH={}", bios_path.display()); |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +```rust |
| 107 | +// src/main.rs |
| 108 | + |
| 109 | +fn main() { |
| 110 | + // read env variables that were set in build script |
| 111 | + let uefi_path = env!("UEFI_PATH"); |
| 112 | + let bios_path = env!("BIOS_PATH"); |
| 113 | + |
| 114 | + // choose whether to start the UEFI or BIOS image |
| 115 | + let uefi = true; |
| 116 | + |
| 117 | + let mut cmd = std::process::Command::new("qemu-system-x86_64"); |
| 118 | + if uefi { |
| 119 | + cmd.arg("-bios").arg(ovmf_prebuilt::ovmf_pure_efi()); |
| 120 | + cmd.arg("-drive").arg(format!("format=raw,file={uefi_path}")); |
| 121 | + } else { |
| 122 | + cmd.arg("-drive").arg(format!("format=raw,file={bios_path}")); |
| 123 | + } |
| 124 | + let mut child = cmd.spawn()?; |
| 125 | + child.wait()?; |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +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). |
0 commit comments