Writing an x86 bootloader in Rust that can launch vmlinux

I've been developping an x86 bootloader in Rust that can use Linux boot protocol. In this article, I'd like to write about my motivation, features of this project, and issues.

KRaBs - Kernel Reader and Booters

KRaBs is a 4-stage chain loader for x86/x86_64 written in Rust.
It can boot an ELF-formatted kernel placed on a FAT32 filesystem in the EFI System Partition. The ELF-formatted kernel is read from the filesystem and relocated, and then the kernel is booted.
It is all implemented in Rust.

GitHub - o8vm/krabs: An x86 bootloader written in Rust.

It has the following features:

  1. Currently, only legacy BIOS is supported.
  2. Both 64 bit and 32 bit system are supported.
  3. Both 64 bit long mode and 32 bit protected mode kernel are supported.
  4. GPT format partition table is supported.
  5. FAT32 file system support.
  6. The boot-time behavior can be controlled by CONFIG.TXT, which is placed on the FAT32 filesystem.
  7. Minimal x86/x86_64 Linux boot protocol is supported.
  8. kernel command line setting in CONFIG.TXT is supported.
  9. Some modules such as initramsfs/initrd are supported.
  10. 10. The multi-boot specification is not supported.

An example of starting 64bit vmlinux with kernel command line and initrd is described in this article.

Just git clone the project and run a cargo run to experience after some preparation:


cargo run -- -we disk.img

demo3.gif

What motivated me to develop KRaBs?

I thought that lower level programming below the OS stack could be also made more modern by using Rust. I wanted to extract the minimum essentials from the process of booting the Linux kernel and finally make up original bootloader where there is no black box for me.

In addition:

  • It's not easy for me to read the source code of an existing chain loader.
  • Reading large amounts of assembly and C source code is tough for a beginner. It takes a lot of time and effort to read it.
  • It is said that Rust binaries tend to be too big and not suitable for writing bootloaders, but I wondered if it is true.

Based on the above, I've decided to write down the bootloader in Rust from scratch.

How KRaBs Works

Linux kernel bootstrapping mechanism

While it may be difficult to unravel the Linux kernel bootstrapping mechanism from the bzImage and GRUB bootloader sources, The mechanism itself is surprisingly simple.
There are four basic things: Loading the ELF-formatted image from the file system, Relocating it according to the program headers, and initializing system and setting parameters according to The Linux/x86 Boot Protocol. That's all there is to it.

Specifically, the following four types of initialization are performed:

Hardware initialization:

  • Setting the keyboard repeat rate.
  • Disable interrupts and mask all interrupt levels.
  • Setting Interrupt descriptor (IDT) and segment descriptor (GDT). As a result,
  • all selectors (CS, DS, ES, FS, GS) refer to the 4 Gbyte flat linear address space.
  • Change the address bus to 32 bits (Enable A20 line).
  • Transition to protected mode.
  • If the target is ELF64, set the 4G boot pagetable and transition to long mode.

Software initialization:

  • Get system memory by BIOS call.

Information transmission to the kernel:

  • KRaBs mount the FAT32 EFI System Partition and Reading the CONFIG.TXT.
  • Setting Zero Page of kernel parameters and transmit it to the OS.

Load items and Relocate the kernel:

  • Load kernel, initrd and command line according to CONFIG.TXT.
  • The target is an ELF file, KRaBs do the ELF relocation.

The format of CONFIG.TXT is a simple matrix-oriented text file that looks like this:


main.kernel sample-kernel
main.initrd sample-initrd
main.cmdlin sample command line clocksource=tsc net.ifnames=0

To perform the above process, KRaBs uses a program that is divided into four stages.

Stages Overview

  1. stage1
  2. A 446 byte program written to the boot sector. The segment registers(CS, DS, ES, SS) are set to 0x07C0, and the stack pointer (ESP) is initialized to 0xFFF0. After that, stage2 is loaded to address 0x07C0:0x0200, and jumps to address 0x07C0:0x0206. In the latter half of stage1, there is an area for storing the sector position and length (in units of 512 bytes) of the stage2 program.
  3. stage2
  4. Load stage3 and stage4, then jump to stage3. The stage3 program is loaded at address 0x07C0:0x6000, the stage4 is loaded at address 0x0003_0000 in the extended memory area. The file is read from the disk using a 2K byte track buffer from address 0x07C0:0xEE00, and further transferred to an appropriate address using INT 15h BIOS Function 0x87h. A mechanism similar to this function is used in stage 4. When the loading of stage3 and stage4 is completed, jump to address 0x07C0:0x6000.
  5. stage3
  6. Do hardware and software initialization which need BIOS calls. After a series of initialization, empty_zero_page information is prepared in 0x07C0:0x0000 to 0x07C0:0x0FFF. Enable the A20 line, change the address bus to 32 bits, and shift to the protect mode. Then, jump to the Stage4.
  7. stage4
  8. Mount the FAT32 EFI System Partition. Then, read and parse the CONFIG.TXT on that partition. Load ELF kernel image, initrd, and kernel command line according to CONFIG.TXT. Drop to real mode when executing I/O. Set Command line and image informations in empty_zero_page. ELF kernel image is stored to the extended memory address 0x100000 or later, and then the ELF32/ELF64 file is parsed and loaded. If the target is ELF64, set the 4G boot pagetable and transition to long mode. Finally, jump to the entry point to launch the kernel. At this time, put the physical address (0x00007C00) of the empty_zero_page information prepared in the low-order memory into the ESI or RSI register.
  9. plankton🦠
  10. library common to stage1 ~ stage4.

How build KRaBs

The directory structure of the KRaBs project is as follows:


$ cd /path/to
$ tree . -L 3
.
β”œβ”€β”€ build.rs
β”œβ”€β”€ Cargo.toml
β”œβ”€β”€ rust-toolchain
β”œβ”€β”€ src
β”‚   β”œβ”€β”€ bios
β”‚   β”‚   β”œβ”€β”€ plankton
β”‚   β”‚   β”œβ”€β”€ stage_1st
β”‚   β”‚   β”œβ”€β”€ stage_2nd
β”‚   β”‚   β”œβ”€β”€ stage_3rd
β”‚   β”‚   └── stage_4th
β”‚   β”œβ”€β”€ main.rs
β”‚   └── uefi
...

All four stages that make up the bootloader for the legacy BIOS and a library called plankton are stored as a sub crate under a directory named src/bios.
Under the src/uefi directory, we plan to store UEFI-compatible bootloader crates.
All these sub-crates will be built by build.rs at cargo build time.

src/main.rs is not the main body of the bootloader, src/main.rs is the CLI program that places KRaBs on the disk. This main.rs will write each stage of the KRaBs to the appropriate location on the disk. The -w option is used to write the stages to disk.

With this directory structure, just run cargo buil to build the CLI and the boot loader, and cargo run -- -w disk.img to burn the boot loader to disk. You can also test it with qemu by running cargo run -- -e disk.

DISK Structure

KRaBs supports disks that are partitioned in GPT format.
The BIOS Boot Partition and the EFI System Partition are required. Place stage1 in the boot sector and stage2 ~ stage4 boot code for legacy BIOS in the BIOS Boot Partition. Place the CONFIG.TXT, Linux kernel, initrd on the FAT32 file system of the EFI System Partition.

Example:


$ gdisk -l disk.img 
...
Found valid GPT with protective MBR; using GPT.
Disk disk2.img: 204800 sectors, 100.0 MiB
Sector size (logical): 512 bytes
Disk identifier (GUID): 2A1F86BB-74EA-47C5-923A-7A3BAF83B5DF
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 204766
Partitions will be aligned on 2048-sector boundaries
Total free space is 2014 sectors (1007.0 KiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048            4095   1024.0 KiB  EF02  BIOS boot partition
   2            4096          106495   50.0 MiB    EF00  EFI system partition
   3          106496          204766   48.0 MiB    8300  Linux filesystem

Why use EFI System Partition?

The reason for this is to make this project compatible with the UEFI environment in the future.
I didn't support UEFI from the start because:

  • This bootloader was originally intended to be used on older PCs, such as the ThinkPad 600X.
  • Currently, Legacy BIOS support works in a wider range system than UEFI.
  • It is mainly intended to be used in the cloud environment except my PC. Legacy BIOS is the mainstream in x86 cloud environment, and there seems to be no merit to replace it with UEFI.

Is Rust good for writing a bootloader?

I know there are pros and cons, but for me, Rust has been so much easier and better than writing C and assemblies. Personally, I think Rust is also pretty good for low-level programming, like bootloaders.

  1. It's a great relief when the compilation is completed without problems
  2. When something goes wrong, most of the time I only need to suspect the unsafe part. This has made debugging a lot easier. I'm an amateur programmer, but thanks in part to this, I was able to complete my first prototype in a week.
  3. Rust's build system is the best
  4. In Rust, you don't have to wonder which object file to link with which, like in C.
  5. I can use my C experience
  6. Since the chain loader is a rocket structure, we always have to code the unsafe parts in order to move to the next stage, and I thought it would be nice to be able to use the same techniques I often use in C for the unsafe parts.
  7. I think even the low-level code in no_std can be written in a modern way.

Issues

(RESOLVED) Setting Page Tables

I tried to set up the page table with an alignment with a linker script or a struct attribute align, but none of these things worked. It looked like the alignment settings were breaking other data structures. It's possible that I wasn't doing it right, but I didn't understand why and gave up debugging. In the end, I dealt with it by manually allocating the page table to the area where I wanted to set up.

This code:


fn setup_page_tables() {
    use plankton::layout::PGTABLE_START;
    use plankton::mem::MemoryRegion;
    let mut pg_table = MemoryRegion::new(PGTABLE_START, 8 * 6 * 512);
    pg_table
        .as_mut_slice::<u64>(0x0000, 6 * 512)
        .copy_from_slice(&[0; 6 * 512]);

    // Build Level 4
    let level4 = pg_table.as_mut_slice::<u64>(0x0000, 512);
    for i in 0..1 {
        level4[i] = (PGTABLE_START + 0x1000) | 0x7;
    }

    // Build Level 3
    let level3 = pg_table.as_mut_slice::<u64>(0x1000, 512);
    for i in 0..4 {
        level3[i] = (PGTABLE_START + 0x2000 + 0x1000 * (i as u64)) | 0x7;
    }

    // Build Level 2
    let level2 = pg_table.as_mut_slice::<u64>(0x2000, 4 * 512);
    for i in 0..2048 {
        level2[i] = (0x00200000 * (i as u64)) | 0x00000183;
    }
}

(OPEN) KRaBs won't boot in the bochs

The stage4 can hang or fail copy_from_slice() on bochs.
There may be a problem with the process of switching between protected mode and real mode for doing I/O, or it may be something else. But unfortunately, I don't know what the problem is.
Here's an example of slice copy failing. I would be happy if someone could help me solve this problem.

This code:


    println!("sector_offset = {}, n_bytes = {}, buffer_offset = {}", sector_offset, n_bytes, buff_offset);
    let slice = track_buffer.as_slice::<u8>(sector_offset as u64, n_bytes as u64);
    buf[buff_offset..(buff_offset + n_bytes)].copy_from_slice(&slice);
    println!("slice = {:?}", slice);
    println!("buf = {:?}", buf);
    n_bytes

Although the slice and the target buf should be the same, the target is filled with 255 from some point.

fail.png

KRaBs works fine with qemu.

I'm developing this project as a hobby, but if anyone can help, please contact me o8@vmm.dev .