The Linux kernel makes use of multiple different tools for compilation. To
ensure you have the latest version of all these tools installed, run the
following (\ is used by bash to keep track of newlines, keep them):
sudo apt update && sudo apt install -y \
build-essential \ # `gcc`, `make`, and other basic build tools
libncurses-dev \ # Gives a pretty TUI for `make menuconfig`
flex \ # Lexer
bison \ # Parser
libssl-dev \ # Library for cryptography (SSL)
libelf-dev \ # Library for ELF files
dwarves \ # For kernel debugging
bc \ # A calculator, for some math
rsync \ # A useful tool for moving files
git # I'd be surprised if you didn't have this
Knowing what each of these dependencies do is not required, but as a general rule of thumb, it’s good to know what exactly you are downloading onto your computer before you download it.
Clone the source code for the kernel. The source code that you will use is
located in the linux/ folder in the main branch of your team repository.
This directory will be the root of your kernel tree. All subsequent commands in
this guide should be run in that directory.
Before you do anything, you should verify the version of the kernel. The first
6 lines of Linux’s top-level Makefile will show you the version. For this
class, we will be using Linux 6.8.0:
$ head -n 6 Makefile
# SPDX-License-Identifier: GPL-2.0
VERSION = 6
PATCHLEVEL = 8
SUBLEVEL = 0
EXTRAVERSION =
NAME = Hurr durr I'ma ninja sloth
The kernel build is configured using a file called .config. This file should
be located at the root of the kernel tree.
To create your kernel .config, use the following steps:
Remove any existing .config files:
make mrproper
Create a config file based on the config file of your current kernel. Make
sure that you’re running the stock Ubuntu kernel before you do this step.
You don’t want to copy a bad config! You can verify this by running uname
-r. You should get something like 6.8.0-41-generic.
The config file that was used to build your current kernel is located in
the /boot/ directory. The following command copies over that file, and
updates any missing options with default values.
make olddefconfig
It’s okay if you see some warnings like this:
.config:12661:warning: symbol value 'm' invalid for ANDROID_BINDER_IPC
.config:12662:warning: symbol value 'm' invalid for ANDROID_BINDERFS
You should now see a .config file in the root of your kernel tree.
Around half of Linux’s source code is just code for device drivers. Since your VM will (hopefully) not be using every device Linux supports, not compiling the device drivers for devices not in use by your VM will greatly reduce compilation time. This is done by running the following command1:
yes '' | make localmodconfig
Edit your .config file. The .config file consists of different options
like CONFIG_LOCALVERSION, each of which is set to a certain value. Try
running cat on your .config file to see some examples. There are two
main ways to edit your .config file (apart from directly modifying it):
Run make menuconfig, which will open up a TUI (Terminal User
Interface) for editing .config.
A backup of your previous .config will be created at .config.old
every time you select Save in the TUI. If you choose this method, we
recommend that you save once, only after making all of the changes listed
below. This will allow you to take a clear diff of the updated .config
vs the original .config file saved in .config.old.
Use the config script in the scripts/ directory of the kernel source
code:
scripts/config --set-str <option> <value> # Sets <option>
scripts/config --state <option> # Retrieves <option>
Note that this method does not create a .config.old file. However,
since you’re modifying the .config file created in step 2, you can find
the original .config file in /boot/ if necessary.
Make the following changes, using either of the above methods:
CONFIG_LOCALVERSION: This setting gives your custom kernel a unique
name to distinguish it from other kernels present in your system. The
local version will be appended to your kernel version to form your kernel
name. For example, if we build a 6.8.0 kernel with the local version set
to -cs4118, it will be named 6.8.0-cs4118. For your pristine kernel
build, set this to -cs4118.
When using make menuconfig, this can be found under General setup, in
the Local Version - append to kernel release option. Alternatively, you
can run scripts/config --set-str CONFIG_LOCALVERSION "-cs4118" as
mentioned above.
CONFIG_BLK_DEV_LOOP: In the immortal words of Michael the Mouse, “it’s
a secret tool that will help us later”. More specifically, it enables a
device that we will make use of in a later assignment. Ensure that this
is set to y.
When using make menuconfig, this can be found under Device Drivers in
the Block devices section, specifically as Loopback device support.
Alternatively, you can run scripts/config --set-str CONFIG_BLK_DEV_LOOP
y.
SYSTEM_TRUSTED_KEYS: This is used to bake additional trusted keys
directly into the kernel image, which can be used to verify kernel
modules before loading them. You can mostly trust yourself, so set this
to an empty string.
When using make menuconfig, this can be found by opening the
Cryptographic API section, then opening the Certificates for signature
checking section at the bottom. The specific field is Additional X.509
keys for default system keyring. Alternatively, you can run
scripts/config --set-str SYSTEM_TRUSTED_KEYS "" as mentioned above.
SYSTEM_REVOCATION_KEYS: Set this to the empty string as well.
When using make menuconfig, this can be found by opening the
Cryptographic API section, then opening the Certificates for signature
checking section at the bottom. The specific field is X.509
certificates to be preloaded into the system blacklist keyring.
Take a moment to inspect the contents of the .config file. Make sure that the
options you configured are set to what you expect them to be.
Note that apart from running cat on the .config file, Linux also provides
the scripts/diffconfig utility, which can be used to compare different
.config files. For example, if you used make menuconfig, you could do
something like this:
$ scripts/diffconfig .config.old .config
LOCALVERSION "" -> "-cs4118"
SYSTEM_TRUSTED_KEYS "debian/canonical-certs.pem" -> ""
SYSTEM_REVOCATION_KEYS "debian/canonical-revoked-certs.pem" -> ""
If you used scripts/config, you can do a diff against the stock .config
file in the /boot/ directory. For instance, run scripts/diffconfig
/boot/config-6.8.0-41-generic .config. If you do this, you’ll probably see
some extra changes besides the three lines listed above. That’s okay, because
make olddefconfig also updates some of the other .configs. Just make sure
your desired changes are reflected in the output.
To build the kernel, run the following as a non-root user:
$ make -j$(nproc)
In the command above, nproc is a command that returns the number of cores in
your VM. You can also set the parallelization level to a different value by
using make -jN, where N is the number of parallel compilation jobs to run.
Note that setting N to greater than nproc won’t necessarily make things
faster, and may even lead to more overhead. The first time that you build the
kernel, it should take up to half an hour on a modern machine.
When performing the first compilation, temporarily increasing the number of cores the VM has access to can help further reduce compilation times. Just be sure to test your submission with your VM at 4 cores before you submit, as that is the number of cores we will test with!
In order to install your kernel so that you can actually boot from it, run the following command:
sudo make modules_install && sudo make install
Make sure that the installation actually succeeds (i.e. your output ends
with something like done).
If the above command errors out, i.e. your output
ends with something like make: *** [Makefile:240: __sub-make] Error 2, try
running the following:
sudo apt remove initramfs-tools
sudo apt clean
sudo apt install initramfs-tools
Verify that you have the following 3 files in /boot/:
initrd.img-6.8.0-cs4118
System.map-6.8.0-cs4118
vmlinuz-6.8.0-cs4118
IMPORTANT: You should ALWAYS take a snapshot of your VM before executing this step. If you boot into a buggy kernel and you do not have any snapshots, you will have to set up your VM from scratch again.
If you have not done so before, configure your bootloader (in our case, grub)
so that you can select the kernel you want to boot into. In particular:
/etc/default/grub as a root user, for instance with sudo vim
/etc/default/grubGRUB_TIMEOUT_STYLE=hidden with GRUB_TIMEOUT_STYLE=menuGRUB_TIMEOUT=0 with GRUB_TIMEOUT=30sudo update-grubReboot your VM:
sudo reboot
When your VM boots up, select Advanced options for Ubuntu and choose the
kernel you want. In this case, you’ll choose the kernel whose name ends with
-cs4118 (the CONFIG_LOCALVERSION identifier you set in .config).
Now verify that you’re running your own custom kernel by running:
$ uname -r
6.8.0-cs4118
Cryptographic API section, then opening the Certificates for signature
Instead of 6.8.0-41-generic, you should now see your kernel version string,
6.8.0-cs4118!
When you are hacking kernel code, you’ll often make simple changes to only a
handful of .c files. If you did not touch any header files, the modules will
not be rebuilt when you run make; thus there is no reason to reinstall all
modules every time you rebuild your kernel. In this case, when compiling and
installing your kernel, you can simply run the following:
make -j$(nproc)
sudo make install
Again, this assumes that you did NOT modify any header files potentially used
by kernel modules. This also assumes you have not changed your kernel
configuration since you last ran sudo make modules_install.
Then, reboot your VM and select your kernel in grub (there is no need to go
through the sections Configuring your kernel build or Optimizing your kernel
compilation time each time you build your kernel). Those steps only need to be
done once. Of course, if you do a fresh clone of the kernel source code, you’ll
need to go through all of these steps again.
If you’re wondering, yes is a command that outputs y infinitely. In
our case, we specify that it should output '', which is equivalent to
outputting nothing (or pressing Enter) infinitely. It’s a useful command for
programs that have many configuration prompts that halt execution, make
localmodconfig being an example of such a command. ↩