When doing kernel development, you’ll often work with multiple kernel builds: you may be testing different implementations/features, and ideally you also have at least one pristine fallback kernel in case you (almost inevitably) really mess up and build an unbootable kernel.
We use a bootloader to manage these kernels; we use it to choose which kernel we boot into, and what parameters we pass to that kernel. This guide will walk you through some good practices for using the GNU GRand Unified Bootloader (GRUB).
These instructions should allow you to do kernel development entirely headlessly: you’ll be able to work fully remotely. And if you’re working locally in a VM, you won’t have to open your VMWare serial console.
Better safe than sorry, especially if you’re working with something as fragile as a kernel! Unlike with regular user processes, there’s no operating system sitting underneath your kernel to keep things running if it crashes. The last thing you want to end up with is a bricked kernel.
If you don’t already, make sure you have GRUB 2 installed. If you followed our Debian VM setup tutorial, it should already be installed for you. You may make sure that GRUB 2 is installed by running the following:
# grub-install --version
It should indicate that you are on some version of GRUB 2:
grub-install (GRUB) 2.06-3~deb11u5
If for some reason you don’t have GRUB 2 installed, you should install the
grub2
package.
Just to give you a better idea of where things are located and which files you’re going to be touching, here’s an overview of some important filepaths you should be aware of.
/boot/
The /boot/
directory contains everything your machine needs
during the boot process, including the kernel images that contain your
operating system (that’s why you need to copy them here to boot into them).
This is also where GRUB reads its configuration file from at boot time, and
where update-grub
looks to generate the bootloader configuration file.
/boot/grub/grub.cfg
This is the configuration file that GRUB reads from during the boot process. It is written as a shell script, and defines the menu entries that appear on your bootloader menu.
This configuration file is usually generated from /etc/default/grub
, so any
changes made here will be lost next time you generate a new configuration file
(by running update-grub
). While we won’t be editing this file, we will be
reading it to make sure update-grub
did what we expected it to.
/etc/default/grub
This GRUB file is what you’ll be editing in order to set up your bootloader.
When you run update-grub
, it will generate the /boot/grub/grub.cfg
configuration file from the options you set in your GRUB file and the kernels it
finds in your /boot/
directory.
The syntax of your GRUB file is declarative. Each option is identified by a key,
which you set with KEY=value
. Here are some useful options:
GRUB_DEFAULT
: the default menu entry your bootloader will boot into. The
value assigned to this option can be one of three types:
Some integer N
: set the default to be N
th menu entry.
Some string ID
: set the default to be the kernel idenfied by the at
menu entry ID
.
saved
: when used in tandem with GRUB_SAVEDEFAULT
, your bootloader
will boot into the last kernel it booted into by default.
GRUB_TIMEOUT
: how long your bootloader menu waits before booting into the
menu entry.
GRUB_DISABLE_SUBMENU
: setting this to y
will prevent the GRUB
bootloader menu from grouping and folding menu entries. This is
required to choose a default kernel hidden in a subdirectory.
For more information about configuring your GRUB file, you can check out the official GRUB documentation.
Each kernel image menu entry has its own menu entry ID. We can inspect the GRUB configuration file to see what the menu entry ID for each kernel is by running the following shell incantation:
# grep '\$menuentry_id_option' /boot/grub/grub.cfg | sed 's/menuentry //g' | sed 's/--class.*menuentry_id_option//g' | nl -v 0
The output should look something like this:
0 'Debian GNU/Linux, with Linux 5.10.158-cs4118' 'gnulinux-5.10.158-cs4118-advanced-a376ec0b-e707-4282-973e-1a9e9dbe017b' {
1 'Debian GNU/Linux, with Linux 5.10.158-cs4118 (recovery mode)' 'gnulinux-5.10.158-cs4118-recovery-a376ec0b-e707-4282-973e-1a9e9dbe017b' {
2 'Debian GNU/Linux, with Linux 5.10.0-20-amd64' 'gnulinux-5.10.0-20-amd64-advanced-a376ec0b-e707-4282-973e-1a9e9dbe017b' {
3 'Debian GNU/Linux, with Linux 5.10.0-20-amd64 (recovery mode)' 'gnulinux-5.10.0-20-amd64-recovery-a376ec0b-e707-4282-973e-1a9e9dbe017b' {
We want to use the 5.10.158-cs4118
kernel we built as our
fallback kernel, so we’ll note its ID is
gnulinux-5.10.158-cs4118-advanced-a376ec0b-e707-4282-973e-1a9e9dbe017b
.
Now that we have the menu entry ID for our kernel, we can reliably set it as
the default in our GRUB file, inpendent of its index. For this to work, add
the following to /etc/default/grub
with root privileges:
GRUB_DISABLE_SUBMENU=y
Next, change GRUB_DEFAULT
to the desired kernel image:
GRUB_DEFAULT='gnulinux-5.10.158-cs4118-advanced-a376ec0b-e707-4282-973e-1a9e9dbe017b'
After you’ve saved these changes, make sure to regenerate your GRUB config file:
# update-grub
Now when you reboot, you should see that you boot into this kernel regardless of what you may have previously selected. You may verify what kernel you’ve booted into by running this command:
$ uname -r
5.10.158-cs4118
Now that you’ve set up your bootloader to boot into your cs4118
fallback
kernel by default, we can tell the GRUB bootloader to reboot into an
experimental kernel on the next boot only.
Let’s say that we installed a new kernel into our /boot/
directory named
5.10.158-dev
, and ran update-grub
to generate our new config file. We can
run the menu entry list incantation again, to find something like this:
0 'Debian GNU/Linux, with Linux 5.10.158-cs4118' 'gnulinux-5.10.158-cs4118-advanced-a376ec0b-e707-4282-973e-1a9e9dbe017b' {
1 'Debian GNU/Linux, with Linux 5.10.158-cs4118 (recovery mode)' 'gnulinux-5.10.158-cs4118-recovery-a376ec0b-e707-4282-973e-1a9e9dbe017b' {
3 'Debian GNU/Linux, with Linux 5.10.158-dev' 'gnulinux-5.10.158-dev-advanced-c9052917-0eaf-4792-9af6-555e787bd6f4' {
4 'Debian GNU/Linux, with Linux 5.10.158-dev (recovery mode)' 'gnulinux-5.10.158-dev-recovery-c9052917-0eaf-4792-9af6-555e787bd6f4' {
5 'Debian GNU/Linux, with Linux 5.10.0-20-amd64' 'gnulinux-5.10.0-20-amd64-advanced-a376ec0b-e707-4282-973e-1a9e9dbe017b' {
6 'Debian GNU/Linux, with Linux 5.10.0-20-amd64 (recovery mode)' 'gnulinux-5.10.0-20-amd64-recovery-a376ec0b-e707-4282-973e-1a9e9dbe017b' {
We can instruct the bootloader to boot into 5.10.158-dev
on the next boot only:
# grub-reboot 'gnulinux-5.10.158-dev-advanced-c9052917-0eaf-4792-9af6-555e787bd6f4'
Then reboot:
# reboot
And verify that we’ve rebooted into our dev kernel with the following:
$ uname -r
5.10.158-dev
When we boot yet again, we should find ourselves back in the 5.10.158-cs4118
kernel we set as our default.