Manuel Odendahl bio photo

Manuel Odendahl

wesen

Email

I am getting back into linux kernel development after a pretty long hiatus. My muscle memory is gone, and while I wrote clever, very close to the hardware kernel modules, they were never sophisticated in their interaction with userland.

These are notes about learning linux kernel development using Dr. Jerry Cooperstein’s book “Writing Linux Device Drivers - a guide with exercises”.

Setting up the development environment

I set up a simple development environment using qt creator, in a parallels virtual machine running Ubuntu 14.04. I added the kernel headers to the project’s includes file:

/lib/modules/3.11.0-26-generic/build/include

I added the __KERNEL__ define to the project’s config file. This way, QT creator knows to chose the right header code when autocompleting.

I found that QT creator reacts weirdly on some of the kernel macros, but haven’t a workaround yet. The kernel code is heavy on macros. Despite that, QT creator remains very nice to use, as it autocompletes most of the useful functions, and allows me to quickly jump to the definition of a function I don’t know.

I compile and load the modules from the command-line, entirely manually for now. A more verbose compilation (with the command line for gcc calls) can be shown with:

KBUILD_VERBOSE=1 make

I used a very standard kernel module makefile:

ccflags-y := -Werror

obj-m += ldd-trivial.o
# ldd-objs := ldd_main.o ldd_file.o ldd_rw.o ldd_init.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all: default

clean:
	rm -f *.o *.ko

default:
	$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

I don’t like having very long C files, so I also included the (commented out) line to define multi-file modules. The linux kernel has absurdly long device driver source files (3000+ lines), which I am not comfortable with when writing code.

I check in my exercise results in a private git, and use the following .gitignore file:

*.o
*.ko
modules.order
.tmp_versions
*.cmd
*.mod.c
Module.symvers

First modules

The first module in the book is the trivial load/unload and printk example. Not much to see here.

Adding module parameters

The next step is to add module parameters. I am not sure how useful this will turn out to be, but I am going to use this as a jumping board for some exploration of the headers.

It is indeed surprisingly easy to add typechecked named parameters using the module_param_* family of macros. The defined parameters (which should be named the same as the variable they are going to set) are then accessible as command line arguments when loading the module, or through the /sys/module/ filesystem.

There are a lot of MODULE_* macros which can be used to add interesting metadata. The following caught my interest:

  • MODULE_SUPPORTED_DEVICE
  • MODULE_FIRMWARE
  • MODULE_VERSION

This information can then be queried by running the modinfo command:

parallels@ubuntu:~/ldd$ modinfo ldd-trivial.ko
filename:       /home/parallels/code/wesen/wesen-misc/linux/ldd/ldd-trivial.ko
license:        GLP v2
author:         A GENIUS
srcversion:     C030D2956A00B85FD276DB5
depends:
vermagic:       3.11.0-26-generic SMP mod_unload modversions
parm:           foobar:int

Creating a device file

A character device number can be allocated dynamically with alloc_chrdev_region. The resulting character device number can be found in /proc/devices.

$ grep cdrv /proc/devices
251 mycdrv

Device file handling is now done by the udev daemon. The kernel driver now registers what is called a class with the kernel. The kernel then ppulates the device class and device information into /sys. The udev daemon picks up on this information, and creates device files accordingly. This allows userland to tune the permissions, names and types of device files.

Using the functions class_create followed by device_create, a new device entry is signalled to udev. The udev then consults its intricate set of rules (under /etc/udev/rules.d in ubuntu) and creates a device file with the appropriate major and minor node numbers and the appropriate permissions.

Here is an entry to make usb module world read/writable:

SUBSYSTEM=="usb", ATTRS{idVendor}=="ffff", TAG+="udev-acl", TAG+="uaccess", MODE="0666"

Adding file operations

You add file operations for your device file using a struct file_operations.

The book mentions the ioctl entry, which has been deprecated in favor of unlocked_ioctl and compat_ioctl.

ioctl is this funky little way of doing IPC in an unchecked way. You pass it an integer along with another integer argument, and off you go. ioctl runs under the Big Kernel Lock, which can lead to stalls in completely different parts of the kernel when an ioctl takes too long. There is now an unlocked_ioctl that runs outside the BKL, and requires the driver to use its own locking. This function is preferred over the compat_ioctl call which still runs under the BKL.

The file operations are added to the device file by creating a cdev structure using cdev_alloc, cdev_init and cdev_add.

Upon an open call, you can allocate memory and store it in the private_data member of the struct file structure that is passed along. This allows to store per file-reader information. This memory needs to be released upon a release call.

This concludes chapter 4 of the book. I will continue with chapter 7 tomorrow, which includes working with linked lists. I worked on the examples of chapter 7 a week ago, made a stupid error in my linked list, crashed the kernel, but (due to a totally different issue), my ubuntu installation was corrupt. That stopped me dead in my tracks.