Have you ever wanted to assemble a Linux distribution from scratch? Installing a “do-it-yourself” distribution like Arch or Gentoo is fun, but it's nowhere near as good a learning experience as building a functioning Linux system entirely from scratch, with no scripts or package managers to help you along. Plus, maybe you'll end up building a distribution that people will actually want to use!

Now, you can assemble a Linux distribution on a spare partition and boot into it bare-metal to test it, which is what the book Linux From Scratch tells you to do, but it's less convenient and takes a lot of time. Using a virtual machine to test your distribution is a much better approach. This article will show you how to set up a development environment for testing out your distribution with QEMU. After you finish this article, you will be able to assemble the filesystem hierarchy of your Linux distribution in a directory of your choosing and then spin up a QEMU virtual machine to test your changes.

kernel and initrd

Technically, a running Linux system only consists of two files: the Linux kernel binary, and an initrd. An initrd, also known as an initramfs, is a basic filesystem that lives in RAM and provides some basic utilities so that the kernel can find and mount the actual root filesystem. That's why it's called the “initramfs” or “initrd”: it's the initial RAM filesystem, or initial RAM disk!

It's possible to run a Linux system without an initrd but you would have to compile all of the modules into the binary itself, which introduces a bunch of headaches since the kernel modules necessary are specific to each hardware configuration and some kernel modules can't be compiled into the binary (e.g. LVM2). That's why most distributions opt to use an initrd.

I have chosen to use the kernel and initrd from my existing operating system (Gentoo) for my own development environment instead of compiling them from scratch since I don't think it's very educational, fun, or productive to compile a kernel, but you may just as well compile yours (if you're building a real Linux distribution you'll have to compile a kernel eventually). Your operating system kernel and initrd likely live somewhere in /boot . The kernel binary on my system is located at /boot/vmlinuz-5.4.28-gentoo-x86_64 , and the initrd on my system is located at /boot/initramfs-5.4.28-gentoo-x86_64.img .

I will not explain how to compile a kernel in this article, because there are many guides on the internet (check out the Gentoo Wiki page on kernel compilation/configuration for a very well-written, in-depth guide). If you compile your own kernel you can specify the location of your compiled kernel binary instead of the system kernel binary in the QEMU command line options. If you do choose to compile a kernel you will also need to build your own initrd, which is usually done these days using a tool called dracut. That's also beyond the scope of this article, and you would be better served reading the official dracut documentation.

To test out your kernel, you can run it inside a QEMU virtual machine (if you're on Wayland you may have to export SDL_VIDEODRIVER=wayland before running this command):

$ qemu-system-x86_64 -kernel [kernel path] -initrd [initrd path] --enable-kvm -display sdl -m 1G # Run a QEMU virtual machine with 1GB RAM and SDL graphics output (this will spawn a new window)

You should see some text flying across the screen and then a message that says “determining root device (trying)...” It's printing out this message because there's no root filesystem; in fact, there's no operating system! You can type “shell” to enter a minimal in-memory shell provided by the initrd , though.

The next step is to create a root filesystem for the kernel to boot to.

root filesystem

One option is to use a physical, bare-metal disk partition to build the system on. This option has the advantage of being able to boot the system onto bare-metal when you're finished. Just partition out a little bit of space (again, there are many guides online for this), format it as ext4, and mount it into your working directory:

$ mkfs.ext4 /dev/[partition identifier] $ mkdir root $ mount /dev/[partition identifier] root

Another option, the option I have chosen to use myself, is to create a virtual disk image and mount it. This option is slightly more difficult but is more flexible, since you don't have to reboot to create/delete/resize it and there's a smaller chance that you will accidentally wipe important data on your disk.

Basically, we're going to tell QEMU to create a virtual disk image in our working directory, then tell the kernel to create a loop device that points to this virtual disk image, then partition the disk image and mount it to our working directory so we can read and write to it.

$ qemu-img create -f raw root.img 4G # Create a virtual disk image with type raw, size 4 gigabytes, and filename root.img $ losetup /dev/loop0 root.img # Create a loop device from the virtual disk image $ parted /dev/loop0 # Use the parted disk partitioning tool to partition the virtual disk image (parted) mklabel gpt # Create a GPT partition table on the virtual disk image (parted) mkpart primary ext4 1MiB 100% # Create an ext4 partition that takes up the entirety of the virtual disk image $ mkfs.ext4 /dev/loop0p1 # Format the partition as ext4 $ mkdir root # Create a directory called root $ mount /dev/loop0p1 root # Mount the partition into the directory

(if any of these commands are not found, try adding /sbin to your PATH).

This is what your working directory should look like after following the above steps:

|-root.img |-root |-lost+found # <-- This is important. This shows that we have an ext4 filesystem mounted inside the directory

Now, test it out with QEMU again:

$ qemu-system-x86_64 -kernel [kernel path] -initrd [initrd path] --enable-kvm -display sdl -m 1G -drive file=root.img,format=raw -append "root=/dev/sda1"

The drive option adds the virtual disk image to the VM and the append option uses kernel parameters to tell the kernel which device is the root partition (since there's only one disk loaded, it's /dev/sda , and since there's only one partition on that disk, it's /dev/sda1 ). If you are booting from a physical partition, replace the -drive option with -drive file=/dev/[partition identifier] (I haven't tested this personally so you may have to tinker around to get it working).

The output should be “the filesystem mounted at /dev/sda1 does not appear to be a valid /”. This is progress! This means that the kernel has booted successfully and is able to load the ext4 filesystem, but can't find an init program to load.

next steps

The Linux kernel looks for an init program at /sbin/init to launch at system startup. On a modern Linux system, /sbin/init is usually a program like systemd or OpenRC that starts and manages system services.

Good luck!