bootloader
bootloader copied to clipboard
An experimental pure-Rust x86 bootloader
bootloader
An experimental x86_64 bootloader that works on both BIOS and UEFI systems. Written in Rust and some inline assembly, buildable on all platforms without additional build-time dependencies (just some rustup components).
Requirements
You need a nightly Rust compiler with the llvm-tools-preview component, which can be installed through rustup component add llvm-tools-preview.
Usage
See our documentation. Note that the bootimage crate is no longer used since version 0.10.0.
Architecture
This project consists of three separate entities:
- A library with the entry point and boot info definitions that kernels can include as a normal cargo dependency.
- BIOS and UEFI binaries that contain the actual bootloader implementation.
- A
builderbinary to simplify the build process of the BIOS and UEFI binaries.
These three entities are currently all combined in a single crate using cargo feature flags. The reason for this is that the kernel and bootloader must use the exact same version of the BootInfo struct to prevent undefined behavior (we did not stabilize the boot info format yet, so it might change between versions).
Build and Boot
The build and boot process works the following way:
- The
builderbinary is a small command line tool that takes the path to the kernel manifest and binary as arguments. Optionally, it allows to override the cargo target and output dirs. It also accepts a--quietswitch and allows to only build the BIOS or UEFI binary instead of both. - After parsing the arguments, the
builderbinary invokes the actual build command for the BIOS/UEFI binaries, which includes the correct--targetand--featuresarguments (and-Zbuild-std). The kernel manifest and binary paths are passed asKERNEL_MANIFESTandKERNELenvironment variables. - The next step in the build process is the
build.rsbuild script. It only does something when building the BIOS/UEFI binaries (indicated by thebinaryfeature), otherwise it is a no-op.- The script first runs some sanity checks, e.g. the kernel manifest and binary should be specified in env variables and should exist, the correct target triple should be used, and the
llvm-toolsrustup component should be installed. - Then it copies the kernel executable and strips the debug symbols from it to make it smaller. This does not affect the original kernel binary. The stripped binary is then converted to a byte array and provided to the BIOS/UEFI binaries, either as a Rust
staticor through a linker argument. - Next, the bootloader configuration is parsed, which can be specified in a
package.metadata.bootloadertable in the kernel manifest file. This requires some custom string parsing since TOML does not support unsigned 64-bit integers. Parse errors are turned intocompile_error!calls to give nicer error messages. - After parsing the configuration, it is written as a Rust struct definition into a new
bootloader_config.rsfile in the cargoOUT_DIR. This file is then included by the UEFI/BIOS binaries.
- The script first runs some sanity checks, e.g. the kernel manifest and binary should be specified in env variables and should exist, the correct target triple should be used, and the
- After the build script, the compilation continues with either the
bin/uefi.rsor thebin/bios.rs:- The
bin/uefi.rsspecifies an UEFI entry point function calledefi_main. It uses theueficrate to set up a pixel-based framebuffer using the UEFI GOP protocol. Then it exits the UEFI boot services and stores the physical memory map. The final step is to create some page table abstractions and call intoload_and_switch_to_kernelfunction that is shared with the BIOS boot code. - The
bin/bios.rsfunction does not provide a direct entry point. Instead it includes several assembly files (asm/stage-*.rs) that implement the CPU initialization (from real mode to long mode), the framebuffer setup (via VESA), and the memory map creation (via a BIOS call). The assembly stages are explained in more detail below. After the assembly stages, the execution jumps to thebootloader_mainfunction inbios.rs. There we set up some additional identity mapping, translate the memory map and framebuffer into Rust structs, detect the RSDP table, and create some page table abstractions. Then we call into theload_and_switch_to_kernelfunction like thebin/uefi.rs.
- The
- The common
load_and_switch_to_kernelfunction is defined insrc/binary/mod.rs. This is also the file that includes thebootloader_config.rsgenerated by the build script. Theload_and_switch_to_kernelfunctions performs the following steps:- Parse the kernel binary and map it in a new page table. This includes setting up the correct permissions for each page, initializing
.bsssections, and allocating a stack with guard page. The relevant functions for these steps areset_up_mappingsandload_kernel. - Create the
BootInfostruct, which abstracts over the differences between BIOS and UEFI booting. This step is implemented in thecreate_boot_infofunction. - Do a context switch and jump to the kernel entry point function. This involves identity-mapping the context switch function itself in both the kernel and bootloader page tables to prevent a page fault after switching page tables. This switch step is implemented in the
switch_to_kernelandcontext_switchfunctions.
- Parse the kernel binary and map it in a new page table. This includes setting up the correct permissions for each page, initializing
- As a last step after a successful build, the
builderbinary turns the compiled bootloader executable (includes the kernel) into a bootable disk image. For UEFI, this means that a FAT partition and a GPT disk image are created. For BIOS, thellvm-objcopytool is used to convert thebootloaderexecutable to a flat binary, as it already contains a basic MBR.
BIOS Assembly Stages
When you press the power button the computer loads the BIOS from some flash memory stored on the motherboard. The BIOS initializes and self tests the hardware then loads the first 512 bytes into memory from the media device (i.e. the cdrom or floppy disk). If the last two bytes equal 0xAA55 then the BIOS will jump to location 0x7C00 effectively transferring control to the bootloader.
At this point the CPU is running in 16 bit mode, meaning only the 16 bit registers are available. Also since the BIOS only loads the first 512 bytes this means our bootloader code has to stay below that limit, otherwise we’ll hit uninitialised memory! Using Bios interrupt calls the bootloader prints debug information to the screen.
For more information on how to write a bootloader click here. The assembler files get imported through the global_asm feature. The assembler syntax definition used is the one llvm uses: GNU Assembly.
The purposes of the individual assembly stages in this project are the following:
- stage_1.s: This stage initializes the stack, enables the A20 line, loads the rest of the bootloader from disk, and jumps to stage_2.
- stage_2.s: This stage sets the target operating mode, loads the kernel from disk,creates an e820 memory map, enters protected mode, and jumps to the third stage.
- stage_3.s: This stage performs some checks on the CPU (cpuid, long mode), sets up an initial page table mapping (identity map the bootloader, map the P4 recursively, map the kernel blob to 4MB), enables paging, switches to long mode, and jumps to stage_4.
Future Plans
- [ ] Create a
multiboot2compatible disk image in addition to the BIOS and UEFI disk images. This would make it possible to use it on top of the GRUB bootloader. - [ ] Rewrite most of the BIOS assembly stages in Rust. This has already started.
- [ ] Instead of linking the kernel bytes directly with the bootloader, use a filesystem (e.g. FAT) and load the kernel as a separate file.
- [ ] Stabilize the boot info format and make it possible to check the version at runtime.
- [ ] Instead of searching the bootloader source in the cargo cache on building, use the upcoming "artifact dependencies" feature of cargo to download the builder binary separately. Requires doing a boot info version check on build time.
- [ ] Transform this "Future Plans" list into issues and a roadmap.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.