内核模块-hello.ko

本文讲述了一个最简单的内核模块HelloWorld编写、构建、载入的全过程。需要的环境如下:

内核版本 linux-5.5.6
交叉编译器 arm-linux-gnueabi-
根文件系统 busybox-1.31.1
虚拟机环境 qemu-system-arm

以上相关环境的搭建过程详见前文《用Busybox制作Linux最小根文件系统

内核模块的意义

Linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。这些代码(包括相关的子例程、数据、函数入口和函数出口)被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块中,或简称为模块。

支持模块的好处是基本的内核镜像可以尽可能地小,因为可选的功能和驱动程序可以利用模块形式来提供。模块允许我们方便地删除和重新载入内核代码,也方便了调试工作。而且当热插拔新设备时,可通过命令载入新的驱动程序。

Hello, World

一个最简单的Hello, World内核模块代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* hello.c
* Hello World 内核模块
*/

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

/*
* hello_init - 初始化函数,当模块装载时调用。
* 成功返回零,否则返回非零值。
*/
static int hello_init(void)
{
printk(KERN_ALERT "I hear a charmed life.\n");
return 0;
}

/*
* hello_exit - 退出函数,当模块卸载时被调用。
*/
static void hello_exit(void)
{
printk(KERN_ALERT "Out, out, brief candle!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("dhd");
MODULE_DESCRIPTION("A Hello, World Module");

模块初始化

hello_init函数是模块的入口,通过module_init()宏调用注册到系统中,在内核装载时被调用。初始化函数必须符合下面的形式

1
int my_init(void);

通常情况下,初始化函数不会导出,可标记为static.

init函数会返回一个int型数值,成功返回零,否则,返回非零值。

模块退出

hello_exit函数是模块的出口,通过module_exit()宏调用注册到系统中,在模块从内存中卸载时被内核调用。退出函数必须符合下面的形式

1
void my_exit(void);

exit函数也可以标记为static

退出函数可能会在返回前负责清理资源,以保证硬件处于一致状态,或者做其他的一些操作。

记录信息的宏

MODULE_LICENSE() 宏用于指定模块的版权。如果载入非GPL模块到系统内存,则会在内核中设置被污染标识。
MODULE_AUTHOR() 宏指定代码作者。
MODULE_DESCRIPTION() 宏是对模块的简要描述。

构建模块

Linux内核模块的编译有两种方式:

  1. 放在内核代码树中
  2. 放在内核代码树外

这里我选择了放在内核代码树外进行编译。下面先说明一下我们的内核代码和模块代码所在的目录

  • 内核代码目录: ~/kernel/linux-5.5.6
  • 模块代码目录: ~/kernel/my_kernel_obj/helloworld
  • 模块代码文件: ~/kernel/my_kernel_obj/helloworld/hello.c

在模块代码目录下新建一个Makefile文件,内容如下:

1
2
3
4
5
6
obj-m := hello.o
KERNEL_DIR := ~/kernel/linux-5.5.6/
PWD := $(shell pwd)

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

obj-m := hello.o 这一行是告诉编译器将hello.c编译为内核模块。

KERNEL_DIR 是内核代码的顶层目录。

PWD 是当前目录,表示将结果输出在当前目录(模块代码目录)下。

直接 make 编译。

ps: 由于我已经通过修改内核顶层Makefile文件指定了交叉编译选项,此处make时无须重复指定。

完成后,模块代码目录下内容如下:

1
2
3
dhd@dhd-pc:~/kernel/my_kernel_obj/helloworld$ ls
hello.c hello.mod hello.mod.o Makefile Module.symvers
hello.ko hello.mod.c hello.o modules.order

hello.ko 就是所需的内核模块。

载入模块

载入内核模块的命令是 insmod, 删除内核模块的命令是 rmmod

把刚刚生成的hello.ko拷贝到根文件系统目录下,并重新生成根文件系统镜像,最后用先前写好的qemu启动脚本启动。

1
2
3
4
5
cp hello.ko ~/qemu/rootfs/lib/
cd ~/qemu/rootfs/
find . | cpio -o -H newc | gzip > ../rootfs.img
cd ~/qemu/
./arm_start.sh

进入系统后,使用命令载入和删除内核模块进行测试。如下

ps:第一次 insmod 不成功是因为在系统初始化脚本中加了 insmod /lib/hello.ko

result