I don’t do a ton of Linux kernel development these days, but I’ve done a fair bit in the past, and picked up a number of useful techniques for doing kernel development in a relatively painless fashion. This blog post is a writeup of the tools and techniques I use when developing for the Linux kernel. Nothing I write here is “the one way” to do it, but this is a workflow I’ve found to work for me, that I hope others may find useful. I’d love to hear if anyone has incremental improvements or alternate strategies.
Assumptions 🔗︎
The instructions as-listed assume you’re developing an x86_64
kernel, from an x86_64
host (kernel and userspace). I will attempt
to mention and explain the tweaks necessary for 32-bit kernels and/or
hosts where appropriate, out-of-line. Even if you’re running a 32-bit
userspace, if you have a 64-bit kernel on the host, it’s perfectly
possible to develop on a 64-bit guest, following these instructions.
I’m assuming a Debian or Ubuntu host for development. If you’re not on
one of those platforms, the kvm
command-lines should still work, but
you may need something other than debootstrap
to build your images.
Kernel config 🔗︎
For kernel development, unless I have a specific reason to do
otherwise, I use an extremely lightweight .config
– no modules
(everything is built-in), and minimal driver support – in particular,
I use virtio drivers for
everything, and disable all other driver support. The advantage of
no-modules is that it is extremely easy to boot a VM (no fussing
around with an initrd), and a minimal config dramatically reduces
build times.
I started with an allnoconfig
, and manually enabled the features I
need. I recommend you just start with my .config
file (for kernel 3.12),
but I’ll include a detailed list at the end of this post explaining
which things I enabled, if you want to construct your own.
(For 32-bit: Just disable CONFIG_64BIT
(“64-bit kernel”))
Building a disk image 🔗︎
I use debootstrap to build a minimal Debian userspace, and then transplant that onto a disk image and boot off of that. Here’s an annotated sequence of command lines:
# Build a Wheezy chroot. Install an sshd, since it will be handy
# later.
mkdir wheezy
sudo debootstrap wheezy wheezy --include=openssh-server
# Perform some manual cleanup on the resulting chroot:
# Make root passwordless for convenience.
sudo sed -i '/^root/ { s/:x:/::/ }' wheezy/etc/passwd
# Add a getty on the virtio console
echo 'V0:23:respawn:/sbin/getty 115200 hvc0' | sudo tee -a wheezy/etc/inittab
# Automatically bring up eth0 using DHCP
printf '\nauto eth0\niface eth0 inet dhcp\n' | sudo tee -a wheezy/etc/network/interfaces
# Set up my ssh pubkey for root in the VM
sudo mkdir wheezy/root/.ssh/
cat ~/.ssh/id_?sa.pub | sudo tee wheezy/root/.ssh/authorized_keys
# Build a disk image
dd if=/dev/zero of=wheezy.img bs=1M seek=4095 count=1
mkfs.ext4 -F wheezy.img
sudo mkdir -p /mnt/wheezy
sudo mount -o loop wheezy.img /mnt/wheezy
sudo cp -a wheezy/. /mnt/wheezy/.
sudo umount /mnt/wheezy
# At this point, you can delete the "wheezy" directory. I usually
# keep it around in case I've messed up the VM image and want to
# recreate it.
(For different architectures: You can pass --arch={i386,amd64}
to
debootstrap to control the architecture of the bootstrapped chroot)
Booting to userspace 🔗︎
At this point, you can boot to a working userspace by using a simple
kvm
invocation, giving it your disk image (specifying
it should be exposed as a virtio device) and your kernel, and telling
the kernel to boot from the virtual disk:
kvm -kernel arch/x86/boot/bzImage \
-drive file=$HOME/vms/wheezy.img,if=virtio \
-append root=/dev/vda
(For 32-bit: Use qemu-system-i386
to emulate a 32-bit VM, instead)
Configuring networking 🔗︎
The easiest way to get working networking in your VM is to use the KVM user-mode networking driver, which makes KVM set up a virtual LAN and NAT your VM to the outside world. To get ssh access, you can tell KVM to set up a port-forward. Just add these arguments to your command-line:
kvm [...]
-net nic,model=virtio,macaddr=52:54:00:12:34:56 \
-net user,hostfwd=tcp:127.0.0.1:4444-:22
The MAC address I’ve chosen is the default address historically
assigned by KVM. It doesn’t overly matter what you specify, but you
want to be consistent, since if the MAC address changes, Debian will
renumber the ethernet device (to eth1
, eth2
, etc.), potentially
screwing up your networking config.
The hostfwd=tcp:127.0.0.1:4444-:22
fragment forwards port 4444 on
localhost on the host to the VM on port 22, so you can ssh to your VM
on localhost:4444
.
If you’ve followed the setup above (installing openssh-server
,
configuring /etc/network/interfaces
, and adding your pubkey to the
image), ssh should Just Work once the VM boots.
Headless operation 🔗︎
By default, KVM will pop up a graphical console for your VM. This is probably more annoying than useful. To get headless operation, add this to your command-line:
kvm [...]
-append 'root=/dev/vda console=hvc0' \
-chardev stdio,id=stdio,mux=on,signal=off \
-device virtio-serial-pci \
-device virtconsole,chardev=stdio \
-mon chardev=stdio \
-display none
Note that the -append
here should replace any other -append
lines
you have; If you’ve added any extra kernel options of your own, you’ll
need to merge them all into one -append
line.
This will multiplex the KVM console and a virtual console for the VM
onto stdout. Type Ctrl-A h
to get a help message about switching
between them. If everything works right, the kernel console output
will appear in your shell, and a getty will run there once you’ve
finished booting.
Note that you may need to wait a few seconds before any output shows up, while the kernel boots itself far enough to connect to the virtual console and redirect output.
(For some reason, using a virtconsole like this is not completely reliable for me – every few boots, the console will just fail to come up, somehow. Let me know if you figure it out…)
Sharing a filesystem tree with the VM 🔗︎
scp
ing file back and forth with your VM image isn’t too bad, but
it’s even easier to directly share a filesystem tree with the VM. To
do this, I use a virtio 9P mount. Say I want to share ~/code/linux
with the VM. I’d add this to my command-line:
kvm [...]
-fsdev local,id=fs1,path=$HOME/code/linux,security_model=none \
-device virtio-9p-pci,fsdev=fs1,mount_tag=host-code
Then, from within the VM, I could run
mkdir -p /mnt/host
mount host-code -t 9p /mnt/host
And I’d be sharing the directory back and forth!
A few notes:
-
security_model=none
causes the VM to map permissions between the host and the guest as best it can using theqemu
process’s uid, and ignore errors if it can’t (e.g. permission bits will be passed through on files you own, but chowning the file inside the VM will have no effect) -
If you’d rather, you can use
security_model=passthrough
, and runqemu
as root, to directly map permissions between the host and guest. -
You can add
,readonly
on the-fsdev
argument to pass the directory through read-only – the VM will be blocked from writing to it.
Building initrds 🔗︎
Booting to a complete userspace inside the VM is great, and super-convenient for exploration and experimentation, but also comparatively slow. One alternative, which is somewhat limited but super lightweight and flexible, is to build test programs into an initrd, and boot directly into that.
To build an initrd from a directory, you can just run:
( cd my-dir && find . | cpio -o -H newc ) | gzip > initrd.gz
In the most lightweight version, you’ll want to build a static binary
(gcc -static
should suffice), and store it as /init
in your
initrd. For example:
$ cat hello.c
#include <stdio.h>
#include <unistd.h>
#include <sys/reboot.h>
int main(void) {
printf("Hello, world!\n");
reboot(0x4321fedc);
return 0;
}
$ mkdir initrd
$ gcc -o initrd/init -static hello.c
$ ( cd initrd/ && find . | cpio -o -H newc ) | gzip > initrd.gz
1751 blocks
$ kvm -kernel arch/x86/boot/bzImage \
-initrd initrd.gz \
-append 'console=hvc0' \
-chardev stdio,id=stdio,mux=on \
-device virtio-serial-pci \
-device virtconsole,chardev=stdio \
-mon chardev=stdio \
-display none
input: AT Translated Set 2 keyboard as /devices/platform/i8042/serio0/input/input1
Freeing unused kernel memory: 676K (ffffffff81646000 - ffffffff816ef000)
Write protecting the kernel read-only data: 6144k
Freeing unused kernel memory: 1156K (ffff8800012df000 - ffff880001400000)
Freeing unused kernel memory: 1300K (ffff8800014bb000 - ffff880001600000)
Hello, world!
ACPI: Preparing to enter system sleep state S5
reboot: Power down
(For 32-bit: You may need to pass gcc -m32
or gcc -m64
if you want
to build binaries for a different bittedness than your host)
Kernel debugging with gdb 🔗︎
Add -s
to your KVM command line. Once the VM is running, you can
attach gdb via:
$ gdb vmlinux
(gdb) set architecture i386:x86-64
(gdb) target remote :1234
You can now set breakpoints, inspect memory, single-step, all that goodness, without ever having to think the letters “kgdb”.
You can step seamlessly between userspace and kernelspace, although getting symbols for everything may be tricky (Let me know if you figure out a good way to make gdb manage everything). I’m not aware of a good way to use gdb to inspect physical memory, or the virtual address space of non-running processes. If you need that, it may be worth playing with KVM/qemu’s own built-in console, although I have no expertise with that myself.
(For 32-bit: You may need “set architecture i386” instead, for a
32-bit target. If your host is 32-bit, you may need the
gdb-multiarch
package to debug a 64-bit target)
Appendix: Kernel config in detail 🔗︎
The .config
file I’m
using was generated by starting with make allnoconfig
and adding in
the following options. I’ve attached explanations to some of them.
CONFIG_64BIT
CONFIG_BLK_DEV_INITRD
For booting test programs in an initrd.CONFIG_BLK_DEV
CONGIG_BLK_DEV_RAM
Required for initrdCONFIG_SMP
Nearly every computer is SMP these days, so let’s build for testing on SMP.CONFIG_PCI
Virtio all runs over fake PCI devicesCONFIG_ELF
CONFIG_BINFMT_SCRIPT
We really need ELF and shebang support.CONFIG_IA32_EMULATION
Optional, but being able to run 32-bit binaries can be useful for testing.CONFIG_FILE_LOCKING
dpkg
and and many other essential userspace tools need file locking.CONFIG_NET
CONFIG_UNIX
CONFIG_PACKET
dhclient needsCONFIG_PACKET
CONFIG_INET
TCP/IPCONFIG_NETDEVICES
CONFIG_VIRTIO_NET
CONFIG_NET_9P
Virtual 9P networking deviceCONFIG_VIRTIO_PCI
Support virtio PCI devicesCONFIG_NET_9P_VIRTIO
CONFIG_9P_FS
Support 9P-based filesystem exports.CONFIG_EXT4_FS
CONFIG_EXT4_USE_FOR_EXT23
ext2/3/4 support for the rootCONFIG_TMPFS
Needed for udevCONFIG_INOTIFY_USER
Needed for udevCONFIG_VIRTIO_BLK
Needed for the virtio HDDCONFIG_VIRTIO_CONSOLE
Needed for the virtio consoleCONFIG_DEBUG_KERNEL
CONFIG_DEBUG_INFO
CONFIG_DEBUG_INFO_REDUCED
Build with debug symbols;_REDUCED
gives significantly better build times, but still keeps key symbols around. Drop it if you’re going to be doing serious kernel debugging.
Changelog 🔗︎
- 2014-09-23 – Added
CONFIG_FILE_LOCKING
, recommendedkvm
command instead ofqemu
, and mentionsignal=off
. Thanks to Martin Kelly for the suggestions