Skip to main content

Linux Kernel Internals - creating a bare minimal device driver

To start with writing a linux kernel driver, we need to know about some of the fundamentals. This post (or series of post) will deal with starting with a basic kernel module with writing Kconfig, IOCTL's. memory address (virtual & physical), dynamic memory allocation, mutex/spinlock way of synchronization, kernel data structure, sys/class interface of a module, etc.

1. Types of Linux kernel Drivers

Based on the access of amount of data, the Kernel drivers are classified into two types:
  1. Character Driver
  2. Block Driver
Character driver provides access of data only as a stream, generally of character i.e bytes. Block Driver on the other hand are addressable in device specific chunks (also called blocks). 
Example of Character driver may be an I2C driver, which provides data at a byte at a time.
On the other hand a filesystem driver (say ext3, ext4) is a good example of a block driver where access of data is done on chunk basis like 1KB, 512MB etc.
Infact all the device driver that are neither filesystem related nor network related are one form of character driver.

In this article we will focus on writing a character driver.

2. Writing a very basic Kernel Driver

To start with writing the basic device driver, we need to know the entry point of the driver. An entry point is like a constructor, where we need to do all the necessary initialization required for the driver to work, like allocating memory, initialization some set of registers etc.

i) Module Entry-points

#include <linux/module.h>

module_init(module_constructor)
int module_constructor(void);
module_exit(module_destructor)
void module_destructor(void);

The module_init/exit are required to initialize/deinitialize the module. The function pointer is the argument for both the Kernel Macro. Both are defined inside linux/module.h, thus the header must be included.

MODULE_LICENSE("GPL");
A module may carry a license which specify the rights of the user of the license. For an example some vendor could restrict the right up-to using the license, without modifying it.

MODULE_AUTHOR("Sourabh Das <sourabhemsec@outlook.com>");
MODULE_DESCRIPTION("Our First Character Driver");
This provides the information of owner of the module and description.

ii) Putting the codes altogether


#include <linux/module.h>

static int module_constructor(void) /* Constructor */
{
        printk(KERN_INFO "Initializing demo module\n");
        return 0;
}
static void module_destructor(void) /* Destructor */
{
        printk(KERN_INFO "Deinitializing the module\n");
}

module_init(module_constructor);
module_exit(module_destructor);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sourabh Das <sourabhemsec@outlook.com>");
MODULE_DESCRIPTION("Our First Character Driver");

Note that printk is very likely (you can say big-brother) to printf (in standard C library), only syntactical difference being the macro (here KERN_INFO) specifies what kind of information it is. For example KERN_DEBUG specifies debug level message. The advantage of this kind of implementation (which is unlike printf) is that the unnecessary logs can be suppressed by setting the proper loglevel in the bootloader, that could ultimately helpful to reduce the kernel boot time.

iii) Makefile changes

Since my driver is under drivers/misc/demo.c, I will make changes inside the drivers/misc/Makefile in order to build the module:

obj-$(CONFIG_DEMO_MOD)          += demo.o

Kindly note that the CONFIG_DEMO_MOD is the flag that could be enabled or disabled in order to tell the kernel build system that to compile demo.c or not.

iii) Writing the Kconfig file

A Kconfig file consist the description about the way the the kernel module could be builded (modular of static), the dependent modules and the descriptions. I have added the contents inside drivers/misc/Kconfig:

config DEMO_MOD
        tristate "A kernel device driver for demonstration purpose"
        default y
        help
        This is a demo kernel module.

Note that tristate means my module could be either statically compiled with kernel (y), or dynamic loadable moule (m) or not selected (n). A driver could be bool also, when the dynamic loading is not applicable. 
default selects the default build-able property.

The menuconfig window for the module would look like:

3. Linux Character Drivers 

A character driver which we discussed above is a driver which is used to do byte oriented operation. Now let's say we have a I2C driver which is a character driver. How the communication will be done between the linux application to the I2C device?
For the whole operation the major players are:

  1. Application (linux compiled a.out)
  2. Character Device file (/dev/<node_name>)
  3. Character Device Driver
  4. Device
Application get connected to the device file by invoking the open() system call. Device file are lined to the driver by specific registration by the driver.  Note that in between device file and the driver the Virtual File System comes in picture. That means the application operates on device file, those operation are translated into the corresponding function into the linked character device driver by the VFS. Finally device driver does the low level operation on the device by setting the corresponding registers. 


ii) Major and Minor number

The connection between the application and the device file is established with the name of the device file. However the device driver don't recognize the device file with the name. It is recognized by the device driver with the help of pair of number which are Major and minor number of a driver.
After Linux Kernel 2.6, a major number could be common between multiple driver, however in this case the distinction is based on the Minor number.

#include <linux/types.h>
static dev_t;
This is a 32 bit integer value which contain both the Major and Minor number.

The major and minor number could be specify for a driver using any of the two ways:
1. Fixed number Specified by the driver developer
int register_chrdev_region(dev_t first, unsigned int cnt, char *name);

2. Let the kernel allocate some unused pairs (recommended):
int alloc_chrdev_region(
dev_t *first, unsigned int firstminor, unsigned int cnt, char *name);


We have used the second option to register the major and minor number.
As of now still after building and running the kernel, we will not be able to find the character device file inside /dev/. We need to create that using mknod command, whose Usage can be read from here. 

To check and ensure the major and minor number allocated, we can do a cat /proc/devices
As we can find that we have got major number as 251, for our device named demodevt, we can now create the device using the following command:
$ mknod /dev/demodevt c 251 0

iii) Character device file creation by driver

In a linux kernel system, /sys/class/ are the hardware specific entries and information. These information are maintained by the kernel to be used by or signal the user level application for any hardware change. Furthermore they also contain information about the hierarchy of the hardware.
A common example is udev that create the runtime devices w.r.t the rules.
One more example is how the /dev/sdX nodes appear when we plug the pendrive into our linux PC. So these are the hotplug feature that udev or similar user application use to track on the hardware changes in /sys/class entry, thus creating the device files. We will not focus more on this topic.

If we want our driver to create the /sys/class and /dev entry, we need to add some more steps. 

struct class *cl = class_create(THIS_MODULE, "<device class name>");

device_create(cl, NULL, demodevt, NULL, "<device name format>");

device_destroy(cl, demodevt);

class_destroy(cl);

Putting them all together:

#include <linux/module.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>

static dev_t demo_devt;
struct class *cl;

static int module_constructor(void) /* Constructor */
{
        int ret;
        struct device *dev_ret;
        printk(KERN_INFO "Initializing demo module\n");
        ret = alloc_chrdev_region(&demo_devt, 0, 3, "demodevt");
        if (ret) {
                printk(KERN_ERR "failed to allocate char device\n");
                return ret;
        }
        printk(KERN_INFO "Major = %d minor = %d\n", MAJOR(demo_devt), MINOR(demo_devt));
        if (IS_ERR(cl = class_create(THIS_MODULE, "democharcl"))) {
                unregister_chrdev_region(demo_devt, 1);
                return PTR_ERR(cl);
        }
        if (IS_ERR(dev_ret = device_create(cl, NULL, demo_devt, NULL, "demochardrv"))) {
                class_destroy(cl);
                unregister_chrdev_region(demo_devt, 1);
                return PTR_ERR(dev_ret);
        }
        printk(KERN_INFO"DEMO module initialization SUCCESS\n");
        return 0;
}

static void module_destructor(void) /* Destructor */
{
        printk(KERN_INFO "Deinitializing the module\n");
        device_destroy(cl, demo_devt);
        class_destroy(cl);
        unregister_chrdev_region(demo_devt, 3);
}

So far we have developed our very minimal character device driver. Our device's class is listed under /sys/class/democharcl/ and device is listed as /dev/demochardrv.
Now in this bare-minimal rocket skeleton we need to add the engine, payload, wings, and a lot of thing to make the rocket land in the Moon.

References:


  1. https://www.kernel.org/doc/html/latest/admin-guide/sysfs-rules.html
  2. https://sysplay.in/blog/linux-device-drivers/2013/06/character-device-files-creation-operations/

Comments

Popular posts from this blog

ARM Trustzone - An overview on how SMC calls are handled by the EL3 Monitor

In this write up, we will focus mainly on the ARMv8-A exceptions, the role of ARM Trusted Firmware (that provides Secure Monitor functionality) and how the World Switch happens between Secure and Normal. If we look on the the architectural diagram of ARM Trustzone w.r.t ARMv8-A, the Execution Level is divided into four levels namely: EL0 (Secure & Non-Secure) - User Application EL1 (Secure & Non-Secure) - Kernel EL2 - Hypervisor for running different OS's simuntaneously EL3 - Security Monitor Now, whenever a normal world User Application calls for some Secure Operation, the calls goes via IOCTL call to the Linux Driver, which ultimately calls the smc instruction. To understand what the smc instruction, we have to look on the Exceptions in ARMv8 ARMv8 Exceptions In ARMv8 the exceptions are divided into two categories: Synchronous & Asynchronous.  An exception is described as synchronous if it is generated as a result of execution or attempted executi

Setting my Yocto qemu environment for reverse engineering experimental purpose

In this post I have discussed about, how I set my ARM reverse engineering platform in Yocto Qemu. Generally when we are talking about reverse engineering then we need a target platform where we could exercise our experiments. We Can choose Raspberry pi, Beagle Bone etc. for these kind of experimentation. But what we can do in these platforms, can also be performed in some virtual environments. Only for experimenting on some kind of side channel attacks, we would need the actual hardware. For making the setup, you would need some PC with very good configuration likely atleast 4 GB of RAM, 100 GB of free space, and with atleast Quad Core Processor. I have installed VMWare (non-commercial version) which is running Ubuntu 18.04. If you have ubuntu installed in your PC itself then it is well and good. I have followed this link to install Ubuntu. The following steps would help: 1. First clone the source code of Yocto. You might also require some dependencies to get it installed: $ c

An overview of ARM Memory Management Unit

The scope of this documentation is to understand the Memory Management Unit for ARMv8 Based processor. Memory management Unit converts the virtual Address (in CPU's logical space) into Physical Address. For an example let us suppose in the following program: int variable; printf("Addrss of variable = 0x%x\n", &variable); The address could be anything (Let's assume  0x40000200 ). Now 0x40000200 may or may not the actual memory address in the Physical Memory (RAM). It could be anything thing (lets assume  0xA0000200 ). Thus the CPU produce the logical address 0x40000200 which is converted into the physical address 0xA0000200 by the Memory Management Unit. Now the question remains Why we require an Address Translation, or in other word in the above program why we don't operate on actual physical memory 0xA0000200? Let us suppose a program that requires a huge amount of contagious memory in the RAM. Now our external memory would have that much memory requ