简介

在这篇文章中我介绍了如何编写一个内核模块,从需要安装哪些包,到makefile怎么写.最后是实验结果的展示和对过程中的一些细节的解释

代码地址

https://github.com/liodegwin/kernel/tree/main/module/helloworld

参考

https://linux.cn/article-3251-1.html

https://www.oschina.net/translate/writing-a-simple-linux-kernel-module?print

感谢

Photo by Alex Andrews: https://www.pexels.com/photo/photo-of-fox-sitting-on-ground-2295744/

准备

安装内核模块开发工具包和头文件

fedora安装命令如下

1
2
dnf install kernel-devel
dnf install kernel-headers

其实只需要内核的头文件就能编译出内核模块,也就是第二个命令

安装的结果就是在lib/modules文件夹下多了很多文件

1
2
3
[liode@liodePC:13:helloworld]$ ls /lib/modules
5.14.10-300.fc35.x86_64 5.19.8-200.fc36.x86_64
5.19.12-200.fc36.x86_64 6.0.8-200.fc36.x86_64

每一个对应一个内核版本,我这个系统升级过3次,一共有4个内核版本

对于其中的一个版本,里面的内容如下

1
2
3
4
5
6
7
8
9
[liode@liodePC:14:helloworld]$ ls /lib/modules/5.19.12*
build modules.builtin.alias.bin modules.networking systemtap
config modules.builtin.bin modules.order updates
extra modules.builtin.modinfo modules.softdep vdso
kernel modules.dep modules.symbols vmlinuz
modules.alias modules.dep.bin modules.symbols.bin weak-updates
modules.alias.bin modules.devname source
modules.block modules.drm symvers.gz
modules.builtin modules.modesetting System.map

这里面的build目录里面就是真正的内核头文件了

1
2
3
4
5
6
7
[liode@liodePC:37:build]$ ls
arch/ fs/ Kconfig.redhat Module.symvers System.map
block/ include/ kernel/ net/ tools/
certs/ init/ lib/ samples/ usr/
crypto/ io_uring/ Makefile scripts/ virt/
Documentation/ ipc/ Makefile.rhelver security/ vmlinux.h
drivers/ Kconfig mm/ sound/ vmlinux.id

build里面有这个makefile,编译的时候会用到,十分重要

编写模块

编写源码

在hello.c中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
printk("Hello world\n");
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye world\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("liodeGwin@gmail.com");

在Makefile中

1
2
3
4
5
6
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) modules clean

实验

执行make命令编译出hello.ko

执行sudo insmod hello.ko之后,首先观察打印信息dmesg

1
2
3
[ 5169.258034] hello: loading out-of-tree module taints kernel.
[ 5169.258063] hello: module verification failed: signature and/or required key missing - tainting kernel
[ 5169.258822] Hello world

可以看到在安装好了之后,有打印输出

同时,lsmod看到多了一个hello的模块

1
2
[liode@liodePC:96:helloworld]$ lsmod | grep hello
hello 16384 0

移除模块之后也有打印,执行sudo rmmod hello

1
[22528.691648] Goodbye world

细节

模块的名字是由什么决定的

是makefile中的obj-m参数决定的,和module_init(hello_init)修饰的入口函数名完全没有关系

makefile的写法

总体形式

编译一个模块的make命令是下面这种形式

1
make -C 内核源码目录 M=模块源码目录 modules

我的makefile是这样的

1
2
3
4
5
6
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) modules clean

all这里展开之后是这样

1
make  -C /lib/modules/5.19.12-200.fc36.x86_64/build M=/home/liode/nfs_dir/kernel/helloworld modules

参数介绍

-C

指的是要进入到后面的目录,找到里面的makefie,对于这里,就是内核源码目录里面的makefile

M

其实是makefile里面需要的一个变量,是内核makefile决定的,不是make工具的一个flag,这个变量指定了模块源文件所在的目录

modules

这个其实和M一样,是makefile中定义的变量,指明了要编译一个模块
总之,首先要指定两个目录,一个内核源码目录,一个模块源码目录。而光有目录还不行,还需要告诉make,要将那一个源文件,编译成哪一个ko文件。这就是obj-m参数的功能c

obj-m:=hello.o

hello.o是hello.c源文件默认生成的目标文件的名字,不能是其他的名字

而这句话的意思是说生成一个叫做hello.ko的ko文件,也就是说,模块的名字和源文件的名字是相同的,如果更加详细的指定不同的名字,我还不知道

KDIR:=/lib/modules/$(shell uname -r)/build

这是内核的头文件的目录,不过是我使用命令下载的,暂时还没有检测过,是否和自己手动下载的源码中头文件的部分完全一致

展开之后是这样

/lib/modules/5.19.12-200.fc36.x86_64/build

make命令详细的解释可以参考下

https://blog.csdn.net/shenwansangz/article/details/47041651?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-47041651-blog-85801028.pc_relevant_recovery_v2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-47041651-blog-85801028.pc_relevant_recovery_v2&utm_relevant_index=3