Create a Bootable Image for a Custom Kernel with GRUB as Bootloader for Legacy x86 Boot (e.g. Multiboot2 Kernel)

Published by Philipp Schuster on

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.


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.


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.


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.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

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() {

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

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:

├── 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: There you can find a working example.

Philipp Schuster

Hi, I'm Philipp and interested in Computer Science. I especially like low level development, making ugly things nice, and de-mystify "low level magic".


Leave a Reply

Your email address will not be published.