A story about making an x86 bootloader that can boot vmlinux with Rust

Since I started studying Rust, I made an x86 bootloader (self-proclaimed: Krabs) that speaks the Linux boot protocol during the winter vacation. In this article, I would like to write about the motivation for the development, the features and mechanism of the Krabs I created, and what I was happy about during the development.

What is Krabs

Krabs is a chain loader with a 4-stage rocket configuration for x86 / x86_64 (Legacy BIOS) written in Rust. You can boot a bzip2 compressed ELF kernel. After decompressing the bzip2 compressed image and rearranging the ELF image that was extracted next, the kernel will be started. Internally it uses the libbzip2 C library, but everything else is written in Rust.

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

It has the following features.

The following is an example of booting 64bit vmlinux that utilizes the kernel command line and initrd.

./tools/build.sh -k vmlinux -i initramfs.cpio.gz -c "clocksource=tsc" disk.img 
qemu-system-x86_64 --hda disk.img -m 1G

cmdline.gif

Motivation to make Krabs

The purpose is to practice Rust and familiarize yourself with Linux. I thought that coding at the level below the OS stack could be made more modern by using Rust. Also. From the process up to the booting of the Linux kernel, I also wanted to extract only the minimum necessary essence and finally create a boot environment that does not have a black box for me. Basically, the goal was to remove the following confusing and clogged parts.

With that in mind, I gave up reading the bootloader and tracking the bzImage code, and decided to write the bootloader myself in Rust.

How it works

Linux kernel boot mechanism

Looking at the Linux kernel boot mechanism from a bzImage or GRUB bootloader can be daunting, but the reality is surprisingly simple. The basic thing is two points. Extract the compressed image, extract the ELF format image, relocate it according to the program header, and then The Linux / x86 Boot Protocol. Perform initialization processing according to (/x86/boot.html) and set parameters. Only this.

It may seem incredibly easy, but specifically, the following four types of initialization processing are performed.

** Hardware initialization: **

** Software initialization: **

** Communicating to the kernel: **

** Image placement: **

Krabs handles the above processing with a program divided into four stages.

Structure and overview of Krabs

  1. stage1:
    stage1 is 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, load stage2 to address 0x07C0: 0x0200 and jump to address 0x07C0: 0x0280. At the end 2 bytes of stage1, there is an area for storing the sector length (512 bytes) of the stage2 program. Even Rust can write a 1st stage loader that fits in 446 bytes!
  2. stage2:
    Load stage3 at address 0x07C0: 0x6000. The bzip2 compressed kernel image is loaded at the extended memory area address 0x0350_0000 and the initrd is loaded at 0x0560_0000. A 4K byte track buffer was used to transfer these files from address 0x07C0: 0xEE00. Temporarily store what you read from the disk here and then use the ʻINT 15hBIOS function0x87hto transfer it to the appropriate address. After loading the stage3, initrd, and compressed kernel images, jump to address0x07C0: 0x6000. The kernel command line is held in the area from address 0x280` to 120 bytes.
  3. stage3 + stage4:
    Stage3 and Stage4 may use the bss area and must support zero clearing of the .bss section. After a series of hardware and software initializations, prepare the Zero Page information from 0x07C0: 0x0000 to 0x07C0: 0x0FFF. Enable the A20 line, change the address bus to 32-bit, and enter protected mode. Call the bzip2 decompression function to restore the bzip2 compressed ELF kernel image to extended memory address 0x100000 or later. Then parse and load the ELF32 / ELF64 file. If the target is ELF64, set the 4G boot page table and move to long mode. Finally, jump to the entry point and boot the kernel. At this time, set the physical address (0x00007C00) of the Zero Page information prepared in the lower memory in the ESI or RSI register.
  4. plankton:
    plankton is a library common to stage1 to stage4.

Disk structure

Krabs supports HDDs and SSDs, but you must have an MBR. Also, one of the partitions must have the boot flag set. Stage3,4, kernel, initrd are stored in the boot partition with the bootflag.

layout.png

Supplement

By the way, you may be wondering why it supports legacy BIOS, so let me add to this. Only legacy BIOS is supported this time for the following three reasons.

Impressions I wrote in Rust

Rust is far easier than C to write low-level code. That is my personal impression.

The sense of security when the compilation goes through is amazing

Even if a problem occurs, you really only have to doubt the ʻunsafe` part. This time this was true.

The best way of thinking about packages and modules

You don't have to be frustrated with which object file to link to, like C.

Easy to play with modern code

The low-level code of no_std is also relatively modern. Also, I don't have to worry so much about not being able to use it or not being able to use it. It's a feeling, but the development speed may be faster than C? Best of all, Rust is fun to write. It is good to write with the type in mind. It's also fun to use various functions.

You can write chain loaders in Rust

I will call you. 16bit / 32bit can be loaded by the chain loader without much inconvenience. Due to the rocket structure, the chain loader must describe the unsafe part in order to go to the next stage, but it is good that the technique often used in C can be used as it is in the unsafe part. I thought. However, be careful with macros and closures inside the boot sector. I feel that the size is easy to explode.

Utilization and provision of C's assets

I think it's great that FFI allows you to utilize C's assets without much awareness. I'm using libbzip2 internally this time, but it was easy to use from Rust. On the contrary, it was easy to provide C with Rust's assets. You need malloc to use libbzip2, but I was able to provide a simple implementation in Rust on the C side. /src/stage_4th/src/bz2d.rs#L45).

Where I was addicted

To set up the page table, align the alignment with Linker Script (https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_14.html) and struct Attributes (struct) I tried to set it at https://doc.rust-lang.org/reference/type-layout.html#representations), but none of them worked. .. .. (It looked like the other data structures had been corrupted). Rust's alignment is left as it is, wondering if something is wrong. After all, I manually secured the area where I wanted to set the page table and solidified the address there. Example

A super big engineer helped me!

For some time after I created Krabs and was able to boot a simple ELF kernel, vmlinux couldn't boot properly and development was stopped for a short time. During that time, I wrote the README in English, tested simple operation examples, supported long mode, and advertised in English on twitter. I was wondering why it didn't work every day, and one day I was loading the bzImage source, something very happy happened.

In a tweet that I casually muttered in Japanese, what a super big engineer of AWS @msw and x86 / x86-64 Linux kernel tree is famous as a long-time maintainer @LinuxHPA (Hans Peter Anvin --Wikipedia) responded and gave me some advice! I was so happy that I took a screenshot. Thanks to this advice, the problem was solved at once, and I renewed my determination not to support the multi-boot spec (I don't really like the multi-boot spec, which originally embeds parameters inside the kernel).

advice2.png

This time, he responded to Japanese, but I was connected after receiving a reaction to what was originally advertised in English. I thought again that it is important to send it in English.

Also, if I mutter in English, I feel that the frequency of receiving DMs directly on twitter and sending support messages by reply will increase. It will be very encouraging. I would like to introduce a happy message. Writing an OS in Rust also sent me a message.

... it's a waste to end, so I'd like to cover an example of finally building a minimal Linux system and booting it with Krabs so that you can experience Krabs.

Let's boot Minimal Linux

I think it will be helpful. (Hereafter, I am working on CentOS7)

Let's make minimal vmlinux!

1: Bring the Linux source.

wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.5.2.tar.gz
tar xf linux-5.5.2.tar.gz 
cd linux-5.5.2

2: Set the Linux config.

make allnoconfig
make menuconfig

Use the following settings.

64-bit kernel ---> yes
General setup ---> Initial RAM filesystem and RAM disk (initramfs/initrd) support ---> yes
General setup ---> Configure standard kernel features ---> Enable support for printk ---> yes
Executable file formats / Emulations ---> Kernel support for ELF binaries ---> yes
Executable file formats / Emulations ---> Kernel support for scripts starting with #! ---> yes
Enable the block layer ---> yes
Device Drivers ---> Generic Driver Options ---> Maintain a devtmpfs filesystem to mount at /dev ---> yes
Device Drivers ---> Generic Driver Options ---> Automount devtmpfs at /dev, after the kernel mounted the rootfs ---> yes
Device Drivers ---> Character devices ---> Enable TTY ---> yes
Device Drivers ---> Character devices ---> Serial drivers ---> 8250/16550 and compatible serial support ---> yes
Device Drivers ---> Character devices ---> Serial drivers ---> Console on 8250/16550 and compatible serial port ---> yes
Device Drivers ---> Block devices ---> yes
Device Drivers ---> PCI Support --> yes
Device Drivers ---> Serial ATA and Parallel ATA drivers (libata) ---> yes
Device Drivers ---> Serial ATA and Parallel ATA drivers (libata) ---> Intel ESB, ICH, PIIX3, PIIX4 PATA/SATA support ---> yes
Device Drivers ---> Serial ATA and Parallel ATA drivers (libata) ---> Generic ATA support ---> yes   
Device Drivers ---> SCSI device support ---> SCSI disk support
File systems ---> The Extended 4 (ext4) filesystem ---> yes
File systems ---> Pseudo filesystems ---> /proc file system support ---> yes
File systems ---> Pseudo filesystems ---> sysfs file system support ---> yes

Or, I have prepared my recommended config with the above already set, so put this in .config I will copy it.

wget https://raw.githubusercontent.com/ellbrid/krabs/master/resources/.config -O .config
make menuconfig

3: Build vmlinux.

make vmlinux

4: You have vmlinux in your current directory.

Let's make an initramfs!

1: First, create the ./src/initramfs directory and build the basic directory here.

cd ..
mkdir --parents src/initramfs/{bin,dev,etc,lib,lib64,mnt/root,proc,root,sbin,sys}

2: Copy the basic device nodes as well.

	1.	sudo cp --archive /dev/{null,console,tty,tty[0-4],sda,sda[1-8],mem,kmsg,random,urandom,zero} src/initramfs/dev/

3: Use busybox instead of installing dynamic libraries and building the environment.

curl -L 'https://www.busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64' > src/initramfs/bin/busybox
sudo chmod +x src/initramfs/bin/busybox
./src/initramfs/bin/busybox --list | sed 's:^:src/initramfs/bin/:' | xargs -n 1 ln -s busybox

4: Prepare the ʻinit` script.

cat >> src/initramfs/init << EOF
#!/bin/sh
mount -t devtmpfs  devtmpfs  /dev
mount -t proc      proc      /proc
mount -t sysfs     sysfs     /sys
sleep 2
cat <<END
Boot took $(cut -d' ' -f1 /proc/uptime) seconds
                                             
_____           _        __    _             
|   __|___ ___ _| |_ _   |  |  |_|___ _ _ _ _ 
|__   | .'|   | . | | |  |  |__| |   | | |_'_|
|_____|__,|_|_|___|_  |  |_____|_|_|_|___|_,_|
                  |___|                       


Welcome to Sandy Linux
END
exec sh
EOF
sudo chmod +x src/initramfs/init

5: Create an initramfs.

cd src/initramfs
find . | cpio -o -H newc | gzip > ../../initramfs.cpio.gz

Let's make a disk image!

1: Create an image file with qemu-img. You can also use dd.

qemu-img create disk.img 512M

2: Create a partition with fdisk.

1st partition:

Command (m for help): n
Partition type:
   p   primary (0 primary, 0 extended, 4 free)
   e   extended
Select (default p): p
Partition number (1-4, default 1): 1
First sector (2048-1048575, default 2048): 2048
Last sector, +sectors or +size{K,M,G} (2048-1048575, default 1048575): 206848
Partition 1 of type Linux and of size 100 MiB is set

Create a boot flag on the first partition:

Command (m for help): a
Selected partition 1

2nd partition:

Command (m for help): n
Partition type:
   p   primary (1 primary, 0 extended, 3 free)
   e   extended
Select (default p): p
Partition number (2-4, default 2): 
First sector (206849-1048575, default 208896): 
Using default value 208896
Last sector, +sectors or +size{K,M,G} (208896-1048575, default 1048575): 
Using default value 1048575
Partition 2 of type Linux and of size 410 MiB is set

write out:

Command (m for help): w
The partition table has been altered!
Syncing disks.

3: Create an ext4 file system on the second partition

$ sudo kpartx -av disk.img 
lsblk
NAME            MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sr0              11:0    1 1024M  0 rom  
loop0             7:0    0  512M  0 loop 
├─loop0p1       253:2    0  100M  0 part 
└─loop0p2       253:3    0  410M  0 part 
$ sudo mkfs.ext4 /dev/mapper/loop0p2
$ sudo kpartx -d disk.img 

Start with Krabs!

Just run the following command: Vmlinux is now bzip2 compressed and written to disk.img with the bootloader.

$ pwd
path/to/krabs
$ ./tools/build.sh -k path/to/vmlinux -i path/to/initramfs.cpio.gz path/to/disk.img 

Let's start!

$ qemu-system-x86_64 --hda disk.img -m 1G

cmdline.gif

Afterword

By the way, in a comment from reddit, I was asked why I didn't use xz or gzip. I didn't think too much about it, but the reason is that it was easy to bring in bzip2. I've also heard news about porting bzip2 to Rust, which is one of the reasons I'm looking forward to it. However, as you can see from the above example of booting Minimal Linux, bzip2 is very slow. Therefore, it may move to gzip or xz in the future.

By the way, I like SpongeBob, and I borrow the name of this bootloader from Mr. Krabs and Plankton. My secret goal is to use Krabs instead of GRUB on my Linux boots. (I'm planning to start making my own OS named sponge).

I also aim to create an original joke Linux AMI that uses this bootloader on Amazon EC2 (also known as the Sandy Linux AMI lol). I wondered if I should use Rust's systemd rustysd for init for Rust's coreutils. thinking about. Is ssh cool with this? Your dreams will spread.

Also, I found that the star on github was really nice, so I decided to star more and more from now on.

Thank you for reading until the end. If you have any likes, comments, advice, issues, pull requests, etc., please do not hesitate to contact us.

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

Recommended Posts

A story about making an x86 bootloader that can boot vmlinux with Rust
A story about an amateur making a breakout with python (kivy) ②
A story about an amateur making a breakout with python (kivy) ①
A story about making 3D space recognition with Python
A story about making Hanon-like sheet music with Python
A story about installing matplotlib using pip with an error
A story about making a tanka by chance with Sudachi Py
The story of making a module that skips mail with python
A story about machine learning with Kyasuket
A memo for making a figure that can be posted to a journal with matplotlib
A story about building an IDE environment with WinPython on an old Windows OS.
The story of making a web application that records extensive reading with Django
[Hackathon] About making a tool that can be CD on Raspberry Pi [Convenient tool]
A story about how Windows 10 users created an environment to use OpenCV3 with Python 3.5
A story about an error when loading a TensorFlow model created with Google Colab locally
A story about using Resona's software token with 1Password
A story about predicting exchange rates with Deep Learning
A story that struggled with the common set HTTP_PROXY = ~
A story about trying a (Golang +) Python monorepo with Bazel
A story about how theano was moved with TSUBAME 2.0
A story about building a PyPI cache server (with Docker) and making me a little happy again
A story that stumbled when I made a chatbot with Transformer
A story about competing with a friend in Othello AI Preparation
A story about how to deal with the CORS problem
A story about a war when two newcomers developed an app
Create a web API that can deliver images with Django
I made a plug-in that can "Daruma-san fell" with Minecraft
Let's make a diagram that can be clicked with IPython
The story of making a question box bot with discord.py
A story about a python beginner stuck with No module named'http.server'
About the matter that torch summary can be really used when building a model with Pytorch
[Google Photo & Slack Photo Bot] A story about making a bot that acquires photos in Google Photos and sends them to Slack.
A story and its implementation that arbitrary a1 * a2 data can be represented by a 3-layer ReLU neural network with a1 and a2 intermediate neurons with an error of 0.