Building and Running the Linux Kernel
These are mostly notes for myself.
TL;DR
# fetch the source
git clone --depth 1 git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
# configure the kernel
cd linux
make defconfig ARCH=x86_64
# Enable two options:
# 1. Kernel hacking -> Memory Debugging -> KASAN
# 2. Kernel hacking -> Compile-time checks and compiler options -> Debug information -> Generate DWARF 5 Debug Info
make menuconfig
# build the kernel
make -j $(nproc)
# run the kernel
sudo mkinitramfs -o initfs
qemu-system-x86_64 \
-kernel arch/x86_64/boot/bzImage \
-nographic \
-append "console=ttyS0" \
-m 1024 \
-initrd initfs \
--enable-kvm \
-cpu host \
-s -S \
-fsdev local,path=$(pwd),security_model=none,id=test_dev \
-device virtio-9p,fsdev=test_dev,mount_tag=test_mount
# debug the kernel (in another shell from the same "linux" directory)
gdb vmlinux -ex "target remote :1234" -ex "c"
Getting the Linux Source
TL;DR
git clone --depth 1 git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
The linux kernel source is found at git.kernel.org, with the
main branch being Linus’: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
Clone this with --depth 1
, unless you want the entire history:
git clone --depth 1 git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
Building the Linux Kernel
TL;DR
cd linux
make defconfig ARCH=x86_64
# Enable two options:
# 1. Kernel hacking -> Memory Debugging -> KASAN
# 2. Kernel hacking -> Compile-time checks and compiler options -> Debug information -> Generate DWARF 5 Debug Info
make menuconfig
make -j $(nproc)
You’ve got the code, now to build it.
The first thing to know is that the linux source has a massive amount of options
that are controlled by the config file at .config
. This file doesn’t exist with
a fresh copy of the linux source. A number of options exist to create one from
some default settings, a menu config, etc:
$> make help | grep config
clean - Remove most generated files but keep the config and
mrproper - Remove all generated files + config + various backup files
config - Update current config utilising a line-oriented program
nconfig - Update current config utilising a ncurses menu based program
menuconfig - Update current config utilising a menu based program
xconfig - Update current config utilising a Qt based front-end
gconfig - Update current config utilising a GTK+ based front-end
oldconfig - Update current config utilising a provided .config as base
localmodconfig - Update current config disabling modules not loaded
localyesconfig - Update current config converting local mods to core
defconfig - New config with default from ARCH supplied defconfig
savedefconfig - Save current config as ./defconfig (minimal config)
allnoconfig - New config where all options are answered with no
allyesconfig - New config where all options are accepted with yes
allmodconfig - New config selecting modules when possible
alldefconfig - New config with all symbols set to default
randconfig - New config with random answer to all options
yes2modconfig - Change answers from yes to mod if possible
mod2yesconfig - Change answers from mod to yes if possible
mod2noconfig - Change answers from mod to no if possible
listnewconfig - List new options
helpnewconfig - List new options and help text
olddefconfig - Same as oldconfig but sets new symbols to their
tinyconfig - Configure the tiniest possible kernel
testconfig - Run Kconfig unit tests (requires python3 and pytest)
kselftest-merge - Merge all the config dependencies of
kselftest to existing .config.
configuration. This is e.g. useful to build with nit-picking config.
kvm_guest.config - Enable Kconfig items for running this kernel as a KVM guest
xen.config - Enable Kconfig items for running this kernel as a Xen guest
i386_defconfig - Build for i386
x86_64_defconfig - Build for x86_64
make O=dir [targets] Locate all output files in "dir", including .config
Or you could copy the config that your current linux system was built with, and tweak it from there:
cp /boot/config-$(uname -r) .config
# edit the config you copied from your current linux system
make menuconfig
Or you could use a default configuration for a specific architecture:
make defconfig ARCH=x86_64
With the linux source configured, time to build! On a somewhat beefy desktop (48 threads, 128GB of RAM, Intel(R) Xeon(R) CPU E5-2670 v3 @ 2.30GHz), this is how long a fresh build took:
$> time make -j $(nproc)
Kernel: arch/x86/boot/bzImage is ready (#1)
real 1m27.860s
user 45m30.886s
sys 4m37.614s
Yay, we built something!
Side note: finding the default makefile target
The default Makefile target (the target built when you run make
with no arguments)
is the first target you see after Updating goal targets
when you run make -d
:
make -d | grep "goal targets" -A 2 | head -n 10
Adding Debug symbols, KASAN
We’ve successfully built the linux kernel, but we don’t have any debug symbols. Since my usual purpose in working with the linux kernel is to debug, develop, or test some security idea with it, I also want KASAN enabled for the build.
This is done through the .config
file that we created earlier. In
make menuconfig
, you can search for options using the /
key.
KASAN
-related options are in Main menu -> Kernel Hacking -> Memory Debugging
secton.
Debug-info-related options (searching for DEBUG_INFO
) are in the
Kernel hacking -> Compile-time checks and compiler options -> Debug information -> Generate DWARF 5 Debug Info
section.
Alternatively, individual options can be set with the scripts/config
command. This
can be rather tedious though. If you compare the default config with the resulting
config after setting the KASAN and DEBUG_INFO_DWARF5 options through make menuconfig
,
you’ll find that there are many other changes to the config that resulted from
setting those two options.
scripts/config -e KASAN -e DEBUG_INFO_DWARF5 # ... and add the rest of the settings
You should notice a change in the output of the file
command when inspecting
the vmlinux
binary after rebuilding:
# before
$> file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=f90935181a31e429b7d27b284e1653783d4f12a9, not stripped
# after
$> file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=20312a83d39426267a334abd28f518017d9b64c4, with debug_info, not stripped
Running & Debugging the Linux Kernel
TL;DR
sudo mkinitramfs -o initfs
# in one shell - use CTRL+A,x to exit
qemu-system-x86_64 \
-kernel arch/x86_64/boot/bzImage \
-nographic \
-append "console=ttyS0" \
-m 1024 \
-initrd initfs \
--enable-kvm \
-cpu host \
-s -S \
-fsdev local,path=$(pwd),security_model=none,id=test_dev \
-device virtio-9p,fsdev=test_dev,mount_tag=test_mount
# in another shell
gdb vimlinux -ex "target remote :1234" -ex "c"
QEMU can be used to run the linux kernel with qemu-system-x86_64
. If you’re
cross compiling for different architectures, qemu can also be used to run those.
The main build output that we’ll be booting into is found in arch/x86_64/boot/bzImage
:
$> file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 5.18.0-rc7-g210e04ff7681 (guts@ungeheuer) #1 SMP PREEMPT_DYNAMIC Wed May 18 06:42:46 PDT 2022, RO-rootFS, swap_dev 0xA, Normal VGA
Skipping ahead to all of the options needed for qemu-system-x86_64
to run
the linux boot image:
qemu-system-... Option |
Notes |
---|---|
-kernel |
Path to the kernel bzImage |
-nographic |
Do not start a GUI for this VM |
-append "console=ttyS0" |
Needed to have console output from the kernel |
-m 1024 |
Megabytes of memory to give the VM (will error if too low) |
-initrd initfs |
The init filesystem used (will error if not present) |
--enable-kvm |
SPEED |
-cpu host |
Use a KVM processor with all of the supported host features |
-s |
Shorthand for -gdb tcp::1234 |
-S |
Freeze the CPU at startup - need to continue in gdb |
-fsdev ... |
|
-device virtio-9p,.. |
This and the previous config setup host <-> guest sharing |
The initfs
file used with the -initrd
option can be created with the mkinitramfs -o initfs
command. I have to use sudo to create this on my machine:
$> sudo mkinitramfs -o initfs
$> file initfs
initfs: ASCII cpio archive (SVR4 with no CRC)
Notice that this is a cpio
archive. You can use the cpio command to build your
own root filesystem from a directory that you manually created.
The full command:
qemu-system-x86_64 \
-kernel arch/x86_64/boot/bzImage \
-nographic \
-append "console=ttyS0" \
-m 1024 \
-initrd initfs \
--enable-kvm \
-cpu host \
-s -S \
-fsdev local,path=$(pwd),security_model=none,id=test_dev \
-device virtio-9p,fsdev=test_dev,mount_tag=test_mount
Once this runs, you should see nothing - it’s waiting for gdb to attach and tell it
to continue (because of the -s
and -S
flags).
In a separate shell, run:
$> gdb vmlinux
...
Reading symbols from vmlinux...
(gdb) target remote :1234
Remote debugging using :1234
0x000000000000fff0 in gdt_page ()
(gdb) c
This will attach us to the qemu-system-x86_64
process so that we can debug it!
After telling gdb to continue (c
), you should see the kernel booting up and then
drop into a basic busybox shell:
[ 1.397467] Run /init as init process
Loading, please wait...
Starting version 246.6-1ubuntu1.7
[ 1.547386] e1000 0000:00:03.0 enp0s3: renamed from eth0
[ 1.957531] input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3
Begin: Loading essential drivers ... done.
Begin: Running /scripts/init-premount ... done.
Begin: Mounting root file system ... Begin: Running /scripts/local-top ... done.
Begin: Running /scripts/local-premount ... Scanning for Btrfs filesystems
[ 2.884022] btrfs (169) used greatest stack depth: 13984 bytes left
done.
No root device specified. Boot arguments must include a root= parameter.[ 2.893655] random: fast init done
BusyBox v1.30.1 (Ubuntu 1:1.30.1-4ubuntu9.1) built-in shell (ash)
Enter 'help' for a list of built-in commands.
(initramfs)