Kyle Edwards

Writing Kernel Modules

In the first few chapters of Operating System Concepts, there’s quite a bit of emphasis in the hybrid monolithic/plugin-style architecture of the Linux kernel. The ability to write and install kernel modules is incredibly powerful for changing the behavior of the system without having to recompile the entire kernel.

Running code in kernel space can often be more performant, but due to the increased difficulty in debugging and the possibility for introducing security holes, it’s typical to mostly write kernel modules for device drivers.

Because these modules are run in protected kernel space, if you do need to communicate with user space, you have to use a built-in method like copy_to_user() or copy_from_user() from the <linux/uaccess.h> module.

To compile a kernel module, you should ideally use a Makefile relevant to your running kernel.

I had luck using make -C /usr/src/$(uname -r) M=$PWD modules, however the Makefile at /lib/modules/$(uname -r)/build did not work for me.

# Install compiled kernel module
sudo insmod {module}.ko

# Read logs from the kernel ring buffer
dmesg

# Uninstall module
sudo rmmod {module}

Modules allow you to tap into Linux functionality and add to the process file system (proc_fs) to make user-readable pseudo-files that take actions in the kernel when accessed. Below is a simple “Hello world!” example:

Notice that functions are supplied to the module_init and module_exit macros, which are run when initializing and teardown.

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <asm/param.h>
#include <asm/uaccess.h>

#define BUFFER_SIZE 128
#define PROC_NAME "mynewproc"

ssize_t proc_read(struct file *file, char __user *usr_buf, size_t count, loff_t *pos);

static struct file_operations proc_ops = {
	.owner = THIS_MODULE,
	.read = proc_read,
};

int proc_init(void) {
	printk(KERN_INFO "Setting up kernel module!\n");
	proc_create(PROC_NAME, 0666, NULL, &proc_ops);
	return 0;
}

void proc_exit(void) {
	printk(KERN_INFO "Removing kernel module!\n");
	remove_proc_entry(PROC_NAME, NULL);
}

ssize_t proc_read(struct file *file, char __user *usr_buf, size_t count, loff_t *pos) {
	char buffer[BUFFER_SIZE];
	static int completed = 0;
	if (completed) {
		completed = 0;
		return 0;
	}

	completed = 1;
	int rv = sprintf(buffer, "Hello World\n");
	copy_to_user(usr_buf, buffer, rv);
	return rv;
}

module_init(proc_init);
module_exit(proc_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Hello World Module");
MODULE_AUTHOR("Kyle");

Setting Up a VM for Kernel Hacking

# Install build dependencies
sudo apt-get install build-essential gnupg2 libncurses-dev flex bison
sudo apt install libelf-dev
sudo apt install libssl-dev

# Retrieve the kernel and its signature
mkdir ~/kernel
cd ~/kernel
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.7.tar.xz
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.7.tar.sign

# Load signing keys
gpg2 --locate-keys torvalds@kernel.org gregkh@kernel.org

# Unarchive the tarball
unxz linux-6.6.7.tar.xz

# Verify the signature
# It's a good call to verify the
# fingerprint with those found on
# https://www.kernel.org/category/signatures.html
gpg2 --verify linux-6.6.7.tar.sign

tar -xf linux-6.6.7.tar
cd linux-6.6.7

make menuconfig
# Remove signature certificate entries
# Enable .config support
# Enable access to .config through /proc/config.gz
# Enable loadable module support
# Enable module unloading and forced module unloading
# Get network driver
# - ip addr show
# - ethtool -i {networkDeviceName} (for qeum, i got virtio_net)

make -j4 bzImage
make -j4 modules
sudo make modules_install
sudo make install

Tracing and Debugging

GDB /sys/kernel/debug/tracing ftrace

Device Drivers

/dev is a temporary RAM file system managed by the kernel

Character devices allow read, write, and seek operations, while block devices are filesystems.

Device files are a way for user-space code to interact with the device drivers.

While you can create device files manually using the mknod [name] b|c [maj] [min] command, most devices are created by the kernel or the systemd process.

struct file_operations {
	struct module *owner;
	ssize_t (*read)...
}

file table points to file object, holds state in file and flags

Licensing

Modules can define their license using the module_license macro. Depending on the license, some parts of the Linux API may not be accessible. For example, symbols exported using the EXPORT_SYMBOL_GPL macro will only be accessible to modules using the GPL (the GNU General Public License).