Skip to main content

Loadable kernel modules 简介

· 9 min read
IceyBlackTea

Loadable kernel modules allow you to add codes to the Linux kernel while it is running.

These modules can do lots of things, especially device drivers, file system drivers and system calls.

主体内容 2021-04-05

LKM

LKM (Loadable Kernel Modules, 可加载内核模块),其最主要的优势在于可以动态的修改部分内核功能而不需要重新编译内核。

LKM 可以在需要时加载,不需要时卸载,这有助于节省系统资源。

但相对的,这也会导致内核偏向“碎片化”,可能导致其他性能问题。

许多操作系统都支持可装载内核模块,不过操作系统不同,LKM 的实现与称呼也可能不同。

如:

  • Linux - loadable kernel module (LKM)
  • FreeBSD - kernel loadable module (kld)
  • macOS - kernel extension (kext)
  • Windows NT - kernel-mode driver
下文未特别指明的情况下都默认指 Linux 操作系统。

Utilities

下面列出一些 LKM 相关的系统工具,也可以使用其他的命令辅助。

UtilitiesDescriptions
lsmodList currently loaded LKMs.
insmodInsert a LKM into the kernel.
rmmodRemove a LKM from the kernel.
depmodDetermine interdependencies between LKMs.
modinfoDisplay contents of .modinfo section in a LKM object file.
modprobeInsert or remove a LKM or set of LKMs intelligently. For example, if you must load A before loading B, Modprobe will automatically load A when you tell it to load B.
kerneldKerneld daemon program
ksymsDisplay symbols that are exported by the kernel for use by new LKMs.

一般最常用的就是 lsmod, insmod, rmmod,分别用于模块的列出安装卸载


Example

说这么多虚的,都不如来一个实际的例子,就让我们简单编写一个 LKM 试试。

C source code

LKM 的代码编写与普通的用户态可执行文件还是有所不同的。

最明显的区别就是 LKM 的源码中不包含 main 函数,

而是包含了 initexit:它们分别是初始化函数与清除函数,并会在模板被加载或卸载时执行。

除此之外,还可以在代码中定义一些模块的相关信息,如:开源协议、作者、模块说明、模块版本、支持的设备等。

hello.c
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
#include <linux/init.h> /* Needed for the macros */

#define LICENSE "GPL"
#define AUTHOR "IceyBlackTea <IceyBlackTea@outlook.com>"
#define DESCRIPTION "A simple module example"

static int __init init_hello(void) {
printk(KERN_INFO "The LKM example loaded: Hello World!\n");

// A non 0 return means init_module failed; module can't be loaded.
return 0;
}

static void __exit cleanup_hello(void) {
printk(KERN_INFO "The LKM example unloaded: Goodbye module!\n");
}

module_init(init_hello);
module_exit(cleanup_hello);

MODULE_LICENSE(LICENSE);
MODULE_AUTHOR(AUTHOR);
MODULE_DESCRIPTION(DESCRIPTION);

从代码的内容来看,我们引用了一些需要的头文件,定义了一些模块相关信息,在模块被加载卸载时输出一些信息。

printk

注意到输出打印使用的是 printk 而不是 printf,因为期望输出于内核日志而不是用户空间中,

而在内核态中 C 标准库包括 printf 是不能使用的。

printk 基于 printf,不过它们在使用时还是有一些区别的。

例如,printk 在输出时可以指定优先级,如代码中的 KERN_INFO

The log levelPrioritiesDescriptions
0KERN_EMERGAn emergency condition; the system is probably dead
1KERN_ALERTA problem that requires immediate attention
2KERN_CRITA critical condition
3KERN_ERRAn error
4KERN_WARNINGA warning
5KERN_NOTICEA normal, but perhaps noteworthy, condition
6KERN_INFOAn informational message
7KERN_DEBUGA debug message, typically superfluous
note

printk 相关在本文中不太重要,请自行查阅。

Kbuild Makefile

编译 LKM 代码与普通用户层代码相比也是稍微复杂一些。

编译 LKM 基于 make modules,例子中进行了扩展。

-C 将当前工作目录转移到你所指定的位置。

M= 当用户需要以某个内核为基础编译一个外部模块的话,程序会自动到你所指定的dir目录中查找模块源码,将其编译,生成 .ko 文件。

Makefile
# obj-m := mymodule.o
obj-m += hello.o

# kernel include dir
KERNELDIR := /lib/modules/$(shell uname -r)/build

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

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

obj-m

obj-m := mod_name.o

编译器会将 mod_name.c 编译得到 mod_name.o,在链接后会得到内核模块文件 mod_name.ko。

如果有多个源文件也可以写成

obj-m := mod_name1.o mod_name2.o ...

note

Linux Kernel Makefiles 的其他相关信息请自行查阅。

Compile

caution

这部分中我略去了自己的路径和内核版本信息,实际是正常输出的。

执行 make

正常情况的输出大致类似这样:

# make
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
make[1]: Entering directory '/usr/src/linux-headers-$(shell uname -r)'
CC [M] $(PWD)/hello.o
Building modules, stage 2.
MODPOST 1 modules
CC $(PWD)/hello.mod.o
LD [M] $(PWD)/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-$(shell uname -r)'

使用 ls 查看当前目录可以发现多了好几个文件。

# ls
hello.c hello.ko hello.mod.c hello.mod.o
hello.o Makefile modules.order Module.symvers

其中最重要的就是 *.ko 文件了

modinfo

可以使用 modinfo 工具查看编译好后的 *.ko 的信息。

输出的信息大致如下:

# modinfo ./hello.ko
filename: $(PWD)/hello.ko
description: A simple module example
author: IceyBlackTea <IceyBlackTea@outlook.com>
license: GPL
srcversion: C4A2BC53933020930189167
depends:
retpoline: Y
name: hello
vermagic: $(shell uname -r) SMP mod_unload

Test the module

要测试我们的 LKM 能否正常使用,最简单的方法就是将其装载与卸载,并检查系统日志中是否正常输出。

insert the module

使用 insmod 工具即可,正常情况不会有输出。

# insmod ./hello.ko

list the module

我们可以使用 lsmod 工具列出已经加载的模块。

可以使用 grep 指令按条件检索过滤下方便观察。

输出大致如下:

# lsmod
Module Size Used by
hello 16384 0
......

可以观察到模块的名称、大小、被其他模块使用的总数和对象等信息。

remove the module

使用 rmmod 工具即可,正常情况不会有输出。

注意卸载模块时不是文件,不需要 .ko 的后缀。

# rmmod hello

check system log

使用 dmesg 可以快速查看系统日志。

可以使用 tail -[number] 指令显示末尾若干条指令方便观察。

输出大致如下:

# dmesg | tail -2
[880287.052304] The LKM example loaded: Hello World!
[880615.125196] The LKM example unloaded: Goodbye module!

至此我们就完成了 LKM 最基本的编写、编译、加载、卸载与检查等多个步骤!


Referrences

主要参考了:

The Linux Kernel Module Programming Guide

Linux Loadable Kernel Module HOWTO

Loadable kernel module - Wikipedia

printk - Wikipedia


最后

简单介绍了如何编写和使用一个 LKM,但是有关它的具体用途却还没有提,看看之后在其他博客中补上吧!

Flag 立起😅!