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.
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
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.
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.
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!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 function
0x87hto transfer it to the appropriate address. After loading the stage3, initrd, and compressed kernel images, jump to address
0x07C0: 0x6000. The kernel command line is held in the area from address
0x280` to 120 bytes..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.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.
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.
Rust is far easier than C to write low-level code. That is my personal impression.
Even if a problem occurs, you really only have to doubt the ʻunsafe` part. This time this was true.
You don't have to be frustrated with which object file to link to, like C.
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.
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.
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).
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
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).
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.
i will study this to understand better low level programing and try "upgrading " templeOS
— rotten lung (@satanacio666) February 8, 2020
Very cool seeing @rustlang being used to modernize the lower levels of the OS stack https://t.co/CpXXoWDt2t
— Dino A. Dai Zovi (@dinodaizovi) February 8, 2020
Awesome! I will definitely check it out when I have some time.
— Philipp Oppermann (@phil_opp) February 4, 2020
... 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.
I think it will be helpful. (Hereafter, I am working on CentOS7)
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.
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
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
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
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