Skip to content


Android Automotive »

Kernel Module

Modules are pieces of code that can be loaded and unloaded into the kernel upon demand. HAL implementations are packaged into modules and loaded by the Android system at the appropriate time. A kernel module can be a device driver that handles or manages a hardware.

Last update: 2022-06-30


This guide is based on AOSP android-10.0.0_r47 and android-12.1.0_r8

Loadable Kernel Module#

As part of the module kernel requirements introduced in Android 8.0, all system-on-chip (SoC) kernels must support loadable kernel modules.

To support loadable kernel modules, android-base.cfg in all common kernels includes the following kernel-config options (or their kernel-version equivalent):

CONFIG_MODULES=y
CONFIG_MODULE_UNLOAD=y
CONFIG_MODVERSIONS=y

All device kernels must enable these options. Kernel modules should also support unloading and reloading whenever possible.

Kernel Menu Config#

Parsing the Kernel Menu Config needs a library, so install it:

sudo apt install libncurses5-dev

Next, set the BUILD_CONFIG to define the DEFCONFIG file then run the config script:

BUILD_CONFIG=goldfish/build.config.goldfish.x86_64 \
build/config.sh
BUILD_CONFIG=common/build.config.gki.x86_64 \
build/config.sh

The Kenel Menuconfig UI

Kernel Module#

Implement a Kernel module invcase that inverses the characters’ case, e.g. AbCaBc:


Change Overview:

  • kernel

    • common/drivers or goldfish/drivers

      • invcase

        • invcase.c

          invcase_init() {
              register a character device at /dev/invcase
          }
          invcase_exit() {
              remove /dev/invcase
          }
          invcase_receive() {
              read from harware to buffer
              copy from buffer to user
          }
          invcase_send() {
              copy from user to buffer
              write from buffer to hardware
          }
          file_operations {
              .read   = invcase_receive,
              .write  = invcase_send,
          };
          module_init(invcase_init);
          module_exit(invcase_exit);
          
        • Kconfig

          menuconfig INVCASE
              tristate "Inverse Characters Case"
              default y
          
        • Makefile or Kbuild

          obj-$(CONFIG_INVCASE) += invcase.o
          invcase-y := invcase.o
          
    • Kconfig

      += source "drivers/invcase/Kconfig"
      
    • Makefile or Kbuild

      + obj-$(CONFIG_INVCASE)     += invcase/
      


Module implementation:

  1. invcase_init: register a character device at /dev/invcase
  2. invcase_exit: remove the registered device
  3. invcase_receive: aka read, copy from an internal buffer to the user buffer

    • Use f_pos to know how many bytes have been read
    • Return the number of bytes for current read command
    • Return 0 to indicate end of data
  4. invcase_send: aka write, copy from the user buffer to the internal buffer

    • Use f_pos to know how many bytes have been written
    • Return the number of bytes for current write command
    • System write expects that total returned bytes must be equal to the total requested bytes, therefore
invcase/invcase.c
#include <linux/module.h>    // all kernel modules
#include <linux/kernel.h>    // KERN_INFO
#include <linux/errno.h>     // EFAULT
#include <linux/device.h>    // device register
#include <linux/fs.h>        // file_operations
#include <linux/types.h>     // size_t
#include <linux/uaccess.h>   // copy_from/to_user

MODULE_LICENSE("GPL");
MODULE_AUTHOR("VQTRONG");
MODULE_DESCRIPTION("Inverse Case");

/* DEVICE NAME */
#define DEVICE_NAME     "invcase"   // The device will appear at /dev/invcase
#define CLASS_NAME      "invcase"
#define DEVICE_DEBUG    "invcase: "

/* Global variable */
static int majorNumber = 0;
static struct class*  invcaseClass  = NULL;
static struct device* invcaseDevice = NULL;

#define MAX_SIZE 1024
static char __buffer[MAX_SIZE] = {0};

/* Function declaration */
static int invcase_init(void);
static void invcase_exit(void);
static ssize_t invcase_receive(
    struct file *filp, char *buf, size_t count, loff_t *f_pos
);
static ssize_t invcase_send(
    struct file *filp, const char *buf, size_t count, loff_t *f_pos
);

/* Device operations */
static struct file_operations __fops = 
{
    .owner  = THIS_MODULE,
    .read   = invcase_receive,
    .write  = invcase_send,
};

static int invcase_init(void){
    // Try to dynamically allocate a major number for the device -- more difficult but worth it
    majorNumber = register_chrdev(0, DEVICE_NAME, &__fops);
    if (majorNumber<0){
        printk(KERN_ERR DEVICE_DEBUG "Failed to register a major number\n");
        return majorNumber;
    }
    printk(KERN_INFO DEVICE_DEBUG "Registered with major number %d\n", majorNumber);

    // Register the device class
    invcaseClass = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(invcaseClass)) // Check for error and clean up if there is
    {
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ERR DEVICE_DEBUG "Failed to register device class\n");
        return PTR_ERR(invcaseClass); // Correct way to return an error on a pointer
    }
    printk(KERN_INFO DEVICE_DEBUG "Device class registered correctly\n");

    // Register the device driver
    invcaseDevice = device_create(invcaseClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
    if (IS_ERR(invcaseDevice)) // Clean up if there is an error
    {
        class_destroy(invcaseClass);
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ERR DEVICE_DEBUG "Failed to create the device\n");
        return PTR_ERR(invcaseDevice);
    }

    // clear buffer
    memset(__buffer, 0, MAX_SIZE);
    printk(KERN_INFO DEVICE_DEBUG "Init!\n");
    return 0; // Zero means OK
}

static void invcase_exit(void){
    device_destroy(invcaseClass, MKDEV(majorNumber, 0)); // remove the device
    class_unregister(invcaseClass); // unregister the device class
    class_destroy(invcaseClass); // remove the device class
    unregister_chrdev(majorNumber, DEVICE_NAME); // unregister the major number
    printk(KERN_INFO DEVICE_DEBUG "Exit\n");
}

static ssize_t invcase_receive(
    struct file *filp, char *buf, size_t count, loff_t *f_pos
) {
    ssize_t remain = MAX_SIZE - *f_pos;
    ssize_t len = count > remain ? remain : count;

    printk(KERN_INFO DEVICE_DEBUG "Read from device, remain=%ld, *f_pos= %lld, count= %ld\n", remain, *f_pos, count);
    if(remain <= 0) return 0;

    if (copy_to_user(buf, __buffer+*f_pos, len)) {
        printk(KERN_ERR DEVICE_DEBUG "Can not copy to user\n");
        return -EFAULT;
    }
    printk(KERN_INFO DEVICE_DEBUG "Read from device: %s\n", __buffer);

    *f_pos += len;
    return len;
}

static ssize_t invcase_send(
    struct file *filp, const char *buf, size_t count, loff_t *f_pos
) {
    int i;
    ssize_t remain = MAX_SIZE - *f_pos;
    ssize_t len = count > remain ? remain : count;

    printk(KERN_INFO DEVICE_DEBUG "Write to device, remain=%ld, *f_pos= %lld, count= %ld\n", remain, *f_pos, count);

    if(*f_pos == 0) memset(__buffer, 0, MAX_SIZE);

    if(remain <= 0) return count; // ignore all requested bytes

    if(copy_from_user(__buffer+*f_pos, buf, len)) {
        printk(KERN_ERR DEVICE_DEBUG "Can not copy from user\n");
        return -EFAULT;
    }
    printk(KERN_INFO DEVICE_DEBUG "Write to device: %s\n", __buffer);

    for(i=*f_pos; i<*f_pos+len; i++) {
        if( __buffer[i] >= 'A' && __buffer[i] <= 'Z') {
            __buffer[i] += 32;
        }
        else if( __buffer[i] >= 'a' && __buffer[i] <= 'z') {
        __buffer[i] -= 32;
        }
    }
    printk(KERN_INFO DEVICE_DEBUG "Convert to: %s\n", __buffer);

    *f_pos += len;
    return len;
}

module_init(invcase_init);
module_exit(invcase_exit);

Kinux Kernel Makefiles

Refer https://docs.kernel.org/kbuild/makefiles.html to understand about the different targets: built-in, modules, library, etc.


Add module to Menuconfig:

invcase/Kconfig
# Invcase Device configuration

menuconfig INVCASE
    tristate "Inverse Characters Case"
    default y
    help
      Say Y to include this module
      Say N will not build this module
      Say M to build this module but not include to kernel yet

Append invcase to list of menu:

Kbuild
+ source "drivers/invcase/Kconfig"

New module in Kernel Menu Config

Add module to Makefile:

invcase/Makefile or invcase/Kbuild
obj-$(CONFIG_INVCASE) += invcase.o

Append invcase to list of modules:

Kbuild or Makefile
+ obj-$(CONFIG_INVCASE)     += invcase/

Rebuild kernel with a fast build option when kernel has been built completely before:

BUILD_CONFIG=goldfish/build.config.goldfish.x86_64 \
LTO=none \
build/build.sh
BUILD_CONFIG=common/build.config.gki.x86_64 \
LTO=none \
FAST_BUILD=1 \
SKIP_MRPROPER=1 \
SKIP_DEFCONFIG=1 \
build/config.sh

Add module to system image

The above example build the target module as a built-in module.

In case you build a loadable module .ko, you have to copy the module into system image, and insert the module at boot:

device/generic/goldfish/vendor.mk
PRODUCT_COPY_FILES += \
+    path/to/invcase.ko:system/lib/modules/invcase.ko
device/generic/goldfish/init.ranchu.rc
+ on boot
+     insmod /system/lib/modules/invcase.ko

Change permission

The module is initialized or loaded by root user. Change the permission if needed:

device/generic/goldfish/init.ranchu.rc
+ on boot
+     chown system system /dev/invcase
+     chmod 0600 /dev/invcase

Rebuild system image to include new module:

Note to change the kernel images and modules which is used for making AOSP system image. Follow the guide Include custom kernel for more details.

m all -j$(nproc)

Run emulator:

emulator -verbose -show-kernel -selinux permissive -writable-system


Now, you can test the invcase device using echo and cat. Check the log in dmesg also.

Test the invcase device with terminal

Vendor Module#

From AOSP 11, Vendor modules are recommended to built separately in common-modules and are built with option EXT_MODULES.

For example: build.config.virtual_device.x86_64build.config.virtual_device which declares:

EXT_MODULES="common-modules/virtual-device"

External modules do not listed in Kernel Config Menu. You have to change them manually in build command or in common-modules/virtual-device/Kbuild file.

Appendix#

A simple device can be implemented and tested in Linux.

Install Kernel Headers
sudo apt install linux-headers-$(uname -r)
Makefile
BINARY := invcase
BUILD := /lib/modules/$(shell uname -r)/build
obj-m := $(BINARY).o

all:
    make -C $(BUILD) M=$(PWD) modules

install:
    echo 'KERNEL=="invcase", SUBSYSTEM=="invcase", MODE="0777"' | sudo tee /etc/udev/rules.d/99-invcase.rules
    sudo insmod $(BINARY).ko

remove:
    sudo rmmod $(BINARY)

clean:
    make -C $(BUILD) M=$(PWD) clean

Comments