Compiling the Linux kernel and creating a bootable ISO

Guide to compile the Linux kernel from source, make the file system from BusyBox and then run it using QEMU and make a bootable ISO and boot it on a computer. There are two parts, one is booting the kernel and the other is the filesystem. It begins with using a ramdisk to boot to a shell or an installation script because we do not know and cannot assume the hard drive situation.

Make the workspace directory to start:

mkdir -p ~/custom_linux/
cd custom_linux

Set up the environment to compile the kernel, other packages not mentioned might also be required:

sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev grub2 xorriso

The Linux Kernel

Firstly, configure, build, compile the Linux kernel, https://kernel.org/ and grab the latest Linux kernel source. To configure the kernel use make decofig which takes the default config file from your running Linux system. To enter a TUI to edit the config file. One thing for older devices is unchecking 64 bit kernel. Use make -j $(nproc) to compile using all the CPU cores.

# Replace with updated version numbers when required
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.7.1.tar.xz
tar -xvf linux-6.7.1.tar.xz 
cd linux-6.7.1

make defconfig
make menuconfig

make -j4

After the build completes successfully, you should have a bzImage in the arch/x86_64/boot directory (your architecture may differ). If not, check the make file log.

That's done. cd ~/custom_linux

Now, create a directory structure: We could manually create the Linux directory structure using mkdir and copy the over the system programs like cat and ls, the majority of files on a modern Linux system are not statically linked, they rely on of libs. This can be checked with command ldd. BusyBox combines tiny versions of many common UNIX utilities into a single small executable and ln the commands.

wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar xf busybox-1.36.1.tar.bz2
cd busybox-1.36.1

Run...

make defconfig

to make a default configuration for the BusyBox. Then...

make menuconfig

to bring the TUI and edit the configs. The option which you must edit is located in Settings, and then Build static binary (no shared libs). The reason to enable this option is we do not want to compile Glibc into the Linux distro.

Then...

make -j $(nproc)

to compile BusyBox. To check if the compiled file is fine, use command...

file busybox

The file must be statically linked.

Creating The File System

Busybox also generates the diretories, run make install to create a folder called _install and change directory to _install and you will see a hierarchy like Linux file system.

In this directory, run the following command to create the folders needed for kernel.

mkdir dev proc sys

Now create a file called init and open it with a text editor. Copy and paste the following data to it:

#!/bin/sh
mount -t devtmpfs none /dev
mount -t proc none /proc
mount -t sysfs none /sys
echo "Welcome to my Linux!"
exec /bin/sh

Then make the script executable with...

chmod +x init

Done. BusyBox made one executable which is capable of providing us a lot of Linux utilities such as sh , echo , vi and so on. With make install we made a filesystem hierarchy which contains these programs as links to BusyBox executable. Next, we make a shell script called init. This script will be ran after kernel loads. At first, it mounts dev, proc and sys directories. It then prints a welcome message and runs sh to open a shell.

You can put ANY executable as init file. You might also want to mount dev, sys and proc directories when the program starts using mount.h header. Last thing we need to do is to create the filesystem itself... To do so, run these commands inside _install directory.

find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz

This will make the file initramfs.cpio.gz in upper directory.

Testing The Compiled Kernel With QEMU

Before creating the ISO file let us check if the kernel is fine or not. To do so, we use QEMU. Just run the following command: (make sure that you change the path of bzImage and initramfs)

qemu-system-x86_64 -kernel bzImage -initrd initramfs.cpio.gz
qemu-system-x86_64 -kernel ../../linux-6.7.1/arch/x86/boot/bzImage -initrd ../initramfs.cpio.gz

Running our Linux in QEMU, we know that our kernel boots, we can create a bootable ISO for it.

Creating a Bootable ISO

Create a folder somewhere with any name. I name it iso, then create a folder called boot in it and inside boot create a folder called grub. Then copy bzImage and initramfs.cpio.gz into the boot folder.

mkdir iso
mkdir iso/boot
mkdir iso/boot/grub
cp ../linux-6.7.1/arch/x86_64/boot/bzImage boot/
cp ../busybox-1.36.1/initramfs.cpio.gz  boot/

Creating the Grub Config File

We will use grub-mkrescue to create our bootable ISO. But before doing so, we have to know if our current host is booted with UEFI or BIOS. To do so, check if the folder /sys/firmware/efi exists on your system or not. If it does, your computer uses UEFI otherwise it’s BIOS. So why knowing this is important? The grub-mkrescue uses the currently installed grub stuff to create the ISO image. This means that if your operating system is booted in BIOS, the chances are that the ISO created from grub-mkrescue does not support UEFI at all. In some cases, UEFI motherboards support booting BIOS images using CMS. But that is not always the case. If you want to make images for BIOS from UEFI host or vice versa, I suggest you to create a Debian virtual machine in VirtualBox. VirtualBox supports both BIOS and UEFI in it’smotherboard settings. After choosing the appropriate one, install Debian (net install is sufficient) and move the folder which contains boot and grub folders to virtual machine. Then continue reading the guide to configure the grub and create the ISO file.

Now we have to configure grub itself. Create a file named grub.cfg in grub folder of the boot folder. If your host is booted using BIOS (and thus the output ISO is BIOS too) put these lines in the config file:

set default=0
set timeout=10
menuentry 'myos' --class os {
insmod gzio
insmod part_msdos
linux /boot/bzImage
initrd /boot/initramfs.cpio.gz
}

If you are using UEFI, put these lines in it:

set default=0
set timeout=10
# Load EFI video drivers. This device is EFI so keep the
# video mode while booting the linux kernel.
insmod efi_gop
insmod font
if loadfont /boot/grub/fonts/unicode.pf2
then
insmod gfxterm
set gfxmode=auto
set gfxpayload=keep
terminal_output gfxterm
fi
menuentry 'myos' --class os {
insmod gzio
insmod part_msdos
linux /boot/bzImage
initrd /boot/initramfs.cpio.gz
}

At last run this command to create the ISO file. Replace the last argument with the folder name which you created at first step.

grub-mkrescue -o myos.iso iso/

Testing The ISO With VirtualBox

Before testing the ISO on a real computer, boot it in VirtualBox. To do so, make a new virtual machine and choose the ISO you have just created as the content of the optical disk. Start the operating system. You must see the grub menu and then the operating system must boot. Do not forget to select EFI in motherboard settings if needed.

There are thousands of iterations to polish and improve upon, an installation script would set up partitions, format and install grub. There are so many Linux distributions out there that there is more gain in using an existing distro than starting a new one. Most distributions are doing little more other than maintaining over 20,000 packages with no pay to make sure they do not cause errors.

Installation script

The script...

1) Partition the Disk: Use a partitioning tool like parted or fdisk to create the necessary partitions on the target disk. For example:

parted /dev/sda mklabel msdos
parted /dev/sda mkpart primary ext4 1MiB 100%

2) Format Partitions: Use mkfs commands to format the partitions. For example:

mkfs.ext4 /dev/sda1

3) Mount Partitions: Create a mount point and mount the partitions to it.

mkdir /mnt
mount /dev/sda1 /mnt

4) Extract Distribution Files: Extract the contents of the distribution to the mounted partition.

mount -o loop /tmp/linux.iso /mnt
cp -r /mnt/* /mnt/.* /mnt/..?* /mnt/...?* /

5) Install Bootloader: Using Grub:

grub-install --boot-directory=/mnt/boot /dev/sda
update-grub

Cleanup, Unmount and Reboot: umount /mnt; reboot

Sample install script:

#!/bin/bash

#install.sh

# Check if the script is run as root
if [ "$(id -u)" -ne 0 ]; then
  echo "Please run as root"
  exit 1
fi

# Set variables for disk and partition
TARGET_DISK="/dev/sda"
PARTITION_NAME="${TARGET_DISK}1"
MOUNT_POINT="/mnt"

# Partitioning the disk
parted -s "$TARGET_DISK" mklabel msdos
parted -s "$TARGET_DISK" mkpart primary ext4 1MiB 100%

# Format the partition
mkfs.ext4 "$PARTITION_NAME"

# Mount the partition
mount "$PARTITION_NAME" "$MOUNT_POINT"

# Download and install the Linux distribution (adjust the URL as needed)
DISTRO_ISO_URL="https://example.com/path/to/linux.iso"
DISTRO_ISO_NAME="linux.iso"

wget "$DISTRO_ISO_URL" -O "/tmp/$DISTRO_ISO_NAME"
mount -o loop "/tmp/$DISTRO_ISO_NAME" "$MOUNT_POINT"
cp -r "$MOUNT_POINT"/* "$MOUNT_POINT"/.??* "$MOUNT_POINT"/..?* "$MOUNT_POINT"/...?* /

# Install the bootloader (Grub in this example)
grub-install --boot-directory="$MOUNT_POINT/boot" "$TARGET_DISK"
update-grub

# Cleanup
umount "$MOUNT_POINT"
rm -rf "/tmp/$DISTRO_ISO_NAME"

echo "Installation completed successfully."

Downloads

  1. Minimal busybox 1
  2. Minimal busybox 2
  3. Minimal non-busybox
  

📝 📜 ⏱️  ⬆️