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
Table of Content
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
Kernel Module#
Implement a Kernel module invcase
that inverses the characters’ case, e.g. AbC
→ aBc
:
Change Overview:
-
kernel
-
common/drivers
orgoldfish/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
orKbuild
obj-$(CONFIG_INVCASE) += invcase.o invcase-y := invcase.o
-
-
-
Kconfig
+= source "drivers/invcase/Kconfig"
-
Makefile
orKbuild
+ obj-$(CONFIG_INVCASE) += invcase/
-
Module implementation:
invcase_init
: register a character device at/dev/invcase
invcase_exit
: remove the registered device-
invcase_receive
: akaread
, 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
- Use
-
invcase_send
: akawrite
, 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
- Use
#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 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:
+ source "drivers/invcase/Kconfig"
Add module to Makefile:
obj-$(CONFIG_INVCASE) += invcase.o
Append invcase
to list of modules:
+ 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:
PRODUCT_COPY_FILES += \
+ path/to/invcase.ko:system/lib/modules/invcase.ko
+ on boot
+ insmod /system/lib/modules/invcase.ko
Change permission
The module is initialized or loaded by root user. Change the permission if needed:
+ 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.
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_64
→ build.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.
sudo apt install linux-headers-$(uname -r)
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