Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Implementing the Driver within the Guest Kernel

Here we will modify the guest Linux kernel again; this time we will implement a proper driver that manipulates the device and exposes its functionality to applications in user space.

User-Kernel Space Communication with ioctl

The goal of an operating system (OS) is to provide user space applications with safe and controlled access to the hardware. To that end, the OS implements a driver that manipulates the hardware directly, and that driver offers an interface to user space applications. It is possible to use different types of interfaces, such as implementing a new system call or using virtual files. The one that we will use for this exercise is called input/output control (ioctl).

With ioctl, the driver will create a virtual file on the VM’s root filesystem representing the device, /dev/my_rng_driver. A user space application wishing to access our random number generator device will open that file and perform operations on it through a particular system call, ioctl, as illustrated below:

As the hardware device provides two functionalities, there will be two ioctl operations available: one to generate a new random number, and another to seed the RNG.

Adding a Source File to the Linux Kernel Sources

Our driver should be implemented in its own source file. So first we need to create a new C file in the kernel sources and add it to the build system so that it gets compiled with the rest of the kernel sources. To do so, let’s first navigate to the kernel sources directory and create a file in the drivers/misc/ directory:

cd ~/virt-101-exercise/linux-6.6
touch drivers/misc/my-rng.c

Next let’s add it to the build system. Edit drivers/misc/Makefile and add this line at the top of the file:

obj-y += my-rng.o

This instructs the kernel build process to add my-rng.c to every build of the kernel.

Implementing the Driver

In drivers/misc/my-rng.c, let’s start by including the necessary headers:

#include <linux/ioctl.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>

These will let us define ioctl operations, create the virtual file corresponding to the device, map the physical memory corresponding to the device’s registers into virtual memory, and access these registers.

Next, we define the two ioctl operations our driver will support:

#define MY_RNG_IOCTL_RAND _IOR('q', 1, unsigned int)
#define MY_RNG_IOCTL_SEED _IOW('q', 1, unsigned int)

With _IOR we define an ioctl operation MY_RNG_IOCTL_RAND that will allow the application to read data from the device’s RNG register, i.e. to get a random number. With _IOW we define an operation for the application to write data to the device’s SEED register, i.e. to seed the random number generator. The parameters are not particularly important, but note that the last one specifies the size of what is read/written: an unsigned int, i.e. a 32-bit unsigned integer.

Next, we define a macro with the base physical address where the device’s registers are mapped into memory:

#define DEVICE_BASE_PHYS_ADDR 0xfebf1000

Please note that this value may be different on your computer. To find the proper value you can use lspci -v within the VM as previously explained.

We also need a pointer that will hold the location where the device’s registers are mapped in virtual memory (recall that the CPU can only access virtual memory):

void *devmem = 0x0;

Next, we implement the function that will access the device. It will be called when the user space application invokes ioctl on the virtual file /dev/my_rng_driver.

static long my_rng_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {

    switch (cmd) {
        
        case MY_RNG_IOCTL_RAND:
            /* Application requests a new random number */
            /* TODO implement that feature */

            break;

        case MY_RNG_IOCTL_SEED:
            /* Application requests to seed the RNG */
            /* TODO implement that feature */
            break;

        default:
            return -ENOTTY; // unknown command
    }

    return 0;
}

static struct file_operations my_rng_fops = {
    .unlocked_ioctl = my_rng_ioctl,
};

Here the cmd parameter contains the exact ioctl command that was issued by the application. We use a switch statement to separate the processing according to what the application requests, either MY_RNG_IOCTL_RAND or MY_RNG_IOCTL_SEED.

It will be your responsibility to implement these commands. A few important things to note:

  • When either reading or writing data from/to the device through ioctl, the arg parameter will contain:
    • The address in user space of the data to write to the device, in the case of a write operation.
    • The address in user space where the data to be read should be stored, in the case of a read operation.
  • It is unsafe to read/write from/to user space addresses directly (the user space application could have, for example, passed the kernel a NULL pointer). To properly access these addresses, you need to use:
    • copy_to_user when copying data read from the device into user space memory; and
    • copy_from_user when reading data from user space in order to write it to the device.
  • At that stage you can assume that the memory pointed to by devmem has already been properly mapped somewhere in virtual memory by the driver initialisation function (presented below), and you don’t need to call ioremap in my_rng_ioctl.
  • You should access the device by drawing inspiration from the test code we wrote in the previous step.

The handler my_rng_ioctl is embedded in a file_operations data structure that we will use to indicate the operations possible on the virtual file the driver will create in /dev.

Finally, we can implement the initialisation and exit functions for our driver:

static int __init my_rng_driver_init(void) {
    devmem = ioremap(DEVICE_BASE_PHYS_ADDR, 4096);

    if(!devmem) {
        printk(KERN_ERR "Failed to map device registers in memory");
        return -1;
    }

    if (register_chrdev(250, "my_rng_driver", &my_rng_fops) < 0) {
        printk(KERN_ERR "Failed to register my_rng_driver\n");
        return -1;
    }

    printk("my_rng_driver loaded, registered ioctls 0x%lx (get a random "
        "number) and 0x%lx (seed the generator) \n", MY_RNG_IOCTL_RAND,
        MY_RNG_IOCTL_SEED);
    return 0;
}

static void __exit my_rng_driver_exit(void) {
    unregister_chrdev(250, "my_rng_driver");

    if(devmem)
        iounmap(devmem);

    printk(KERN_INFO "my_rng_driver unloaded\n");
}

module_init(my_rng_driver_init);
module_exit(my_rng_driver_exit);

The initialisation function my_rng_driver_init is executed when the kernel boots. It starts by mapping the device’s registers into virtual memory with ioremap, as we have seen in the test code we wrote previously. Next, it registers a character device named my_rng_driver with the kernel, with an identification number (called the major number) of 250. We’ll use that number later when we create, in the VM, the virtual file that will play the role of the interface between a user space application and the driver living in the kernel.

The exit function my_rng_driver_exit is executed when the kernel shuts down. It simply unregisters the character device and unmaps the device registers from virtual memory.

The initialisation and exit functions are declared with module_init and module_exit.

Once all the code is written, you can recompile Linux by typing the following at the root of the kernel sources:

make

Checking the Presence of the Driver

Once the kernel is recompiled, reboot the VM and check in the kernel log the line written by the driver when it loads. It may be a bit hard to find because a lot of output is printed in that log when the kernel boots. Once you get a shell, you can print and filter the kernel log with dmesg and grep:

dmesg | grep my_rng_driver
[    0.869353] my_rng_driver loaded, registered ioctls 0x80047101 (get a random number) and 0x40047101 (seed the generator) 

Note that the ioctl numbers may be different on your machine.