Create a Bootable Image for a Custom Kernel with GRUB as Bootloader for Legacy x86 Boot (e.g. Multiboot2 Kernel)
In this blog post, I want to show you how you can boot your custom (i.e., self developed, non-linux) kernel with GRUB 2 in a legacy x86 boot flow. The tutorial is focused on Linux as build environment. A working example can be found on my GitHub.
Introduction
When you are an OS developer/low-level hacker and write your own kernels, you need a setup to boot your software stack (i.e., kernel and user applications). For UEFI, this is easy. You can create a simple directory in your file system, copy a bootloader EFI-file to /efi/boot/boot{x86, x64, A64, ...}.efi
, copy the other files into the directory, and finally move all the files to an FAT-32 formatted USB drive. Really easy, this can be done in a file manager without additional magic. The creation of a boot medium for the x86 legacy boot flow, however, is more complicated.
I don’t like the fact that many cool, interesting, and important low-level topics don’t show up Google on the first page because if there is any documentation, then it is on ugly, old, outdated websites. Therefore, I hope I can help someone with this.
Background
A BIOS firmware will load the first 512 bytes of the boot record from a MBR-partioned disk into the RAM and execute this code. Thus, you need a special image file that you flash onto an USB drive that stores the relevant boot code in those bytes. A simple copy & paste in a file manager won’t work. The image must contain a partition table so that it matches the BIOS prerequisites when it is flashed properly onto a medium.
From my research, the relevant specification for such an image are ISO9660, El-Torito, and MBR.
Tutorial
GRUB is a bootloader that supports several boot protocols. One example is Multiboot2. In the following, I’ll boot a custom kernel via Multiboot2. To build the bootable image, we use one of GRUB’s utilities: grub-mkrescue
. It creates an ISO9660-file that follows the El Torito specification. grub-mkrescue
can pack additional files into that image which is what we need.
Assume the following directory structure:
grub ├── grub.cfg ├── iso │ ├── boot │ │ └── grub │ │ └── grub.cfg │ ├── kernel │ └── roottask
grub/grub.cfg
is a source file, whereas grub/iso
is generated by a script. Everything in grub/iso
will be bundled into the final image. The grub/iso
directory contains a Multiboot2 kernel, an user-space application (the roottask), and a GRUB configuration at the default location where GRUB searches for config files during runtime. At first, let us look at the config file:
# GRUB 2 configuration that bootstraps the Hedron kernel and the roottask. set timeout=0 set default=0 # set debug=all # Bootstraps Hedron via Multiboot2 menuentry "Hedron with Minimal Roottask" { # The leading slash is very important. multiboot2 /hedron serial module2 /roottask boot }
It is important that we use leading slashes here. Otherwise, GRUB doesn’t find the referenced files. The GRUB installation generated by grub-mkrescue
is configured in a way that the GRUB configuration automatically looks up paths in the boot medium, thus, we don’t need a (memdisk)
or (hd0)
prefix in paths. After the flashing, you can insert the medium, for example a USB drive, into an x86 machine and boot it, as long as it supports the legacy BIOS boot flow. UEFI with CSM might also work.
Now let’s have a look at how I generate the resulting image file:
#!/usr/bin/env bash # This script generates a bootimage for the legacy x86 boot flow. It uses GRUB 2 as boot loader # to bootstrap Hedron. # # The ISO can be tested like this: # `$ qemu-system-x86_64 -boot d -cdrom grub/legacy_x86_boot.img -m 1024 -cpu host -machine q35,accel=kvm:tcg -serial stdio` set -e function fn_main() { fn_prepare_iso_dir fn_make_image } function fn_prepare_iso_dir() { rm -rf "grub/iso" # GRUB expects the config by default at /boot/grub/grub.cfg mkdir -p "${ISO_SRC_DIR}/boot/grub" cp "grub/grub.cfg" "grub/iso/boot/grub/grub.cfg" cp "/path/to/kernel" "grub/iso/hedron" cp "/path/to/roottask" "grub/iso/roottask" } function fn_make_image() { grub-mkrescue -o "grub/legacy_x86_boot.img" "grub/iso" echo "grub/legacy_x86_boot.img' is the bootable image (legacy x86 boot)" } # invoke main function fn_main
If we check the type of the resulting image file with the file
utility, we get: ./grub/legacy_x86_boot.img: DOS/MBR boot sector; GRand Unified Bootloader, stage1 version 0x79, boot drive 0xbb, stage2 address 0x8e70, 1st sector stage2 0xb8db31c3, stage2 segment 0x201; partition 1 : ID=0xee, start-CHS (0x0,0,2), end-CHS (0xb,15,8), startsector 1, 23015 sectors, extended partition table (last)
We can flash this image to an attached USB drive. For example, by using $ sudo dd if=grub/legacy_x86_boot.img of=/dev/sda
. ⚠ Be sure to double-check the destination device. Otherwise, you may damage/erase existing data on the specified drive.
If we mount the resulting image with $ sudo mount grub/legacy_x86_boot.img /mnt
, the image content looks like the following tree:
tree /mnt -L 3
:
/mnt
├── System
│ └── Library
│ └── CoreServices
├── boot
│ └── grub
│ ├── fonts
│ ├── grub.cfg
│ ├── i386-pc
│ ├── roms
│ └── x86_64-efi
├── boot.catalog
├── efi.img
├── hedron
├── mach_kernel
└── roottask
We can see that the grub/iso
directory was bundled into the image. There are also some other files automatically included by grub-mkrescue
but we do not have to cope with them. The resulting image produces a bootable USB drive (or CD-ROM) when you flash it into a device and boot it on a machine that performs legacy x86 boot.
Example Project on GitHub
Please check out my example project on GitHub: https://github.com/phip1611/hedron-minimal-roottask. There you can find a working example.
Outlook
I didn’t deep-dived too much into creating bootable ISO images, but it looks quite complicated. Especially when you want to create hybrid images that boot on legacy BIOS and UEFI systems. I didn’t figured out yet how to do this with GRUB. But here are some interesting pointers:
0 Comments