Compiling a custom Linux Kernel

Last week, I finally gave in and tried my luck by compiling a custom Linux kernel. After several days of work and a lot of hair-pulling…

Compiling a custom Linux Kernel
Photo by Yancy Min / Unsplash
A Linux user, a vegan, and an atheist walk into a bar. I know because they told everyone.

During these past few months, I have been switching between various kernels depending on the work I wanted to do. Sometimes I needed an IOMMU patch, some other times I wanted to try some Linux gaming, and, more often than not, I was too slow to select the correct GRUB entry and ended up using the default Arch Linux kernel.

Last week, I finally gave in and tried my luck by compiling a custom kernel tailored specifically for my new laptop. After several days of work and a lot of hair-pulling, I managed to boot into a kernel which only had support for the exact hardware I needed.

cat boot.txt
Startup finished in 4.793s (firmware) + 1.253s (loader) + 1.408s (kernel) + 3.794s (userspace) = 11.251s
graphical.target reached after 3.794s in userspace
cat boot-eirene.txt
Startup finished in 4.781s (firmware) + 1.264s (loader) + 1.138s (kernel) + 3.365s (userspace) = 10.551s
graphical.target reached after 3.365s in userspace

Besides the slightly improved boot times, I also saw a surprising increase in battery life (7.9 Watts used when idle compared to 11) and about 500MBs more free RAM.

In this post, I will be explaining the main steps I followed when creating this kernel, hopefully helping anyone that finds themselves in a similar situation. You can find the finished configuration in this project's Git repo.

Post contents

Preparation

⚠️
Disclaimer: I am not an expert and I don't claim to be one. I'm just a CompSci student who enjoys tinkering with his system and trying to optimize it as much as possible while sacrificing my sanity in the process. I am not responsible if you break your system or lose important files.

Before you begin, I recommend installing a normal Linux distribution with good support for your hardware and making sure that everything is working how it's supposed to. In my case, I used Arch Linux with the latest kernel.

Once you confirm that everything is working, you should also store the output of some commands so that you have a working reference point when booting with your custom kernel:

  • lspci -nnkkvvv for verbose hardware and driver details
  • lsmod for loaded kernel modules
  • lsusb for connected devices on various USB busses
  • lscpu for CPU information

You should also have some clear goals before you start working on your kernel's configuration. I originally made the mistake of going in blind, which made me waste several hours debugging boot errors, changing previously-set options, and eventually forcing me to restart from scratch.

Installing build dependencies

Once you are ready to start, install the packages you need for building the kernel. If you are using Arch, this can be easily done by running:

pacman -S base-devel xmlto kmod inetutils bc libelf git cpio perl tar xz

Acquiring the source code

After installation is complete, go ahead and download the latest kernel source code. You can do that by either going to https://www.kernel.org/, grabbing the latest tarball, and unpacking it, or by using git and pulling from the official repo:

git clone https://github.com/torvalds/linux

You should also make sure that the kernel tree is clean by running make mrproper in the downloaded directory.

Configuration

Now it's finally time to configure the kernel. What I recommend doing is getting your distribution's default .config by running zcat /proc/config.gz > .config, and compiling it to act as a sanity check for further .config changes.

Advanced configuration

Once you've made sure that the default config is working, you can go ahead and run make clean to remove the previous build and make nconfig to open the configuration menu.

The sheer number of choices that will appear in front of you will be overwhelming, but don't let that discourage you. If you use a slow and methodic approach you'll realize that while compiling a custom kernel is time-consuming, it is certainly not hard.

Just a word of advice: Do not try and configure everything in one run. There is a high chance that you will break something and spend more time debugging it than if you were to make small, gradual changes, recompiling every once in a while to check if everything is working.

When I was creating my config, I had 3 main goals in mind:

  • Keep it as minimal as possible
  • Improve performance as much as possible without harming battery life
  • Disable any logging / debugging capabilities that might increase runtime overhead

I'm not going to go through every choice I made since most of the options are architecture-dependent and analyzing all use-cases would take months. Even if I did, if you copied my config without understanding the functionality behind each feature, you would most likely end up with an unbootable system.

There are a couple of resources you can use to learn what each option does:

  • The in-built help text, available by pressing h
  • DOTSLASHLINUX's kernel configuration guide
  • The Gentoo Wiki
  • Good ol' Google

The general strategy I followed when working on this project was to first get the easy things out of the way: These included options like CPU architecture support, compression, filesystems, etc. I then worked my way through one submenu at a time, recompiling the kernel to make sure I didn't break anything. I recommend leaving the behemoth that is the Device Drivers menu for last since that will take the most time and contains multiple options dependent on other settings.

Applying Patches

Now would also be a good time to apply any needed patches. In my case, I wanted to apply an ACS Override patch so that I could have more control over my IOMMU Groups. You can easily apply .patch files by moving them to the kernel source directory and running:

patch -Np1 < [name-of-patch]

If you don't get any rejects, you are good to go for compilation.

Compilation

Now that you've made some changes to your config, it's time to compile it. Go ahead and run the following commands:

make
make modules
sudo make modules_install

Each step should take a decent amount of time, depending on how large the kernel is. In my case, it took about 10 minutes on a Ryzen 7 5800H. If everything went well, you should notice a new arch/x86/boot/bzImage file.

Installation

Now that you have finished compiling your kernel, it is finally time to install it.

Kernel

Go ahead and run the following command. You can name the resulting file as you wish, provided that it is prefixed with vmlinuz. In my case, I named it vmlinuz-linux-eirene:

cp -v arch/x86/boot/bzImage /boot/vmlinuz-linux-eirene

Initial RAM disk

Once you have copied your kernel to the /boot directory, you should also generate an initial RAM disk. You can easily do that by copying the default kernel's preset:

cp /etc/mkinitcpio.d/linux.preset /etc/mkinitcpio.d/linux-eirene.preset

After that, edit the new file and make sure to change it so it matches the name you selected.

# mkinitcpio preset file for the 'linux-eirene' package

ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-linux-eirene"

PRESETS=('default' 'fallback')

#default_config="/etc/mkinitcpio.conf"
default_image="/boot/initramfs-linux-eirene.img"
#default_options=""

#fallback_config="/etc/mkinitcpio.conf"
fallback_image="/boot/initramfs-linux-eirene-fallback.img"
fallback_options="-S autodetect"
/etc/mkinitcpio.d/linux-eirene.preset

Bootloader

Finally, add an entry for your new kernel in your bootloader's configuration file. If you are using GRUB, you can simply run the following command:

grub-mkconfig -o /boot/grub/grub.cfg

If you didn't get any errors, you should be able to select the newly-compiled kernel the next time you boot your computer!

Troubleshooting

One easy way to check for errors is to look at your system's journal:

journalctl -p 3 -b
Dec 31 17:44:50 eirene kernel: Spectre V2 : Spectre mitigation: kernel not compiled with retpoline; no mitigation available!
Dec 31 17:44:50 eirene kernel: ACPI BIOS Error (bug): Could not resolve symbol [\_SB.PCI0.PB2], AE_NOT_FOUND (20210930/dswl>
Dec 31 17:44:50 eirene kernel: ACPI Error: AE_NOT_FOUND, During name lookup/catalog (20210930/psobject-220)
Dec 31 17:44:52 eirene systemd-modules-load[318]: Failed to find module 'nvidia'
Dec 31 17:44:52 eirene systemd-modules-load[318]: Failed to find module 'nvidia-uvm'
Dec 31 17:44:52 eirene systemd-backlight[718]: Failed to get backlight or LED device 'backlight:acpi_video0': No such device
Dec 31 17:44:52 eirene systemd[1]: Failed to start Load/Save Screen Backlight Brightness of backlight:acpi_video0.
Dec 31 17:44:53 eirene kernel: Bluetooth: hci0: Failed to read codec capabilities (-56)
Dec 31 17:46:08 eirene lightdm[1491]: gkr-pam: unable to locate daemon control file

Keep in mind that not everything shown here is important, but it can be a good lead for finding the root cause of an issue. In the worst-case scenario, you can simply backtrack and try recently-changed config options one by one to see which one creates the problem.

Conclusion

Even though configuring a kernel is challenging, the feeling of booting it for the first time and seeing the resulting performance/battery life increase is definitely satisfying.

Maintaining that kernel is an entirely different story, but that can be fun as well, depending on how much you hate yourself, your free time, and your mental health 😀.

Happy coding and happy new year!