编写一个 Linux 内核模块
作者:解琛
时间:2020 年 8 月 16 日
一、实验环境
jerome@jerome:~$ uname -a
Linux jerome 5.4.0-42-generic #46~18.04.1-Ubuntu SMP Fri Jul 10 07:21:24 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
jerome@jerome:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.4 LTS
Release: 18.04
Codename: bionic
二、Linux 内核模块相关命令
命令 | 作用 |
---|---|
lsmod | 用于显示所有已载入系统的内核模块。 |
insmod | 用于加载内核模块,通常可载入的模块一般是设备驱动程序 |
rmmod | 用于卸载不需要的模块。 |
modinfo | 用于显示内核模块的相关信息。 |
depmod | 用于分析检测内核模块之间的依赖关系。 |
modprobe | 同样用于加载内核模块,与insmod不同,modprobe会根据depmod产生的依赖关系,加载依赖的的其他模块。 |
三、程序架构
内核模块程序的基本结构包括了以下几个部分:
- 头文件;
- 内核模块加载/卸载函数;
- 内核模块的参数;
- 内核模块导出符号;
- 内核模块的许可证;
- 内核模块的其他信息,如作者,模块的描述信息,模块的别名等;
四、编写一个内核模块
4.1 头文件
#include <linux/init.h>
#include <linux/module.h>
4.1.1 init.h
位于 lib/modules/5.4.0-42-generic/build/include/linux/init.h。
/* These are for everybody (although not all archs will actually
discard it in modules) */
#define __init __section(.init.text) __cold notrace
#define __initdata __section(.init.data)
#define __initconst __constsection(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module).
* There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module.
* the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
init.h 头文件主要包含了内核模块的加载、卸载函数的声明,还有一些宏定义,因此,只要涉及内核模块的编程,就需要加上该头文件。
4.1.2 module.h
位于 lib/modules/5.4.0-42-generic/build/include/linux/module.h。
/* Generic info of form tag = "info" */
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
/* For userspace: you can also call me...
*/
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
/*
* Author(s), use "Name <email>" or just "Name", for multiple
* authors use multiple MODULE_AUTHOR() statements/lines.
*/
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
MODULE_LICENSE 是指定该内核模块的许可证,是必须要有的,编写内核模块时需要添加该头文件。
4.2 模块加载
static int __init hello_init(void)
{
if (debug_on)
printk("[ DEBUG ] debug info output.\n");
printk("Hello World Jerome's Module Init.\n");
return 0;
}
module_init(hello_init);
hello_init 函数类型为 int,该函数返回 0,表示模块初始化成功,并会在 /sys/module 下新建一个以模块名为名的目录。返回非0值,表示模块初始化失败。
static 关键字的作用如下:
- static 修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期;
- static 的修饰全局变量只能在本文件中访问,不能在其它文件中访问;
- static 修饰的函数只能在本文件中调用,不能被其他文件调用;
内核模块的代码,实际上是内核代码的一部分,假如内核模块定义的函数和内核源代码中的某个函数重复了,编译器就会报错,导致编译失败,因此我们给内核模块的代码加上 static 修饰符就可以避免这种错误的发生。
/* 定义在 init.h 中; */
#define __init __section(.init.text) __cold notrace
#define __initdata __section(.init.data)
Linux 内核的栈资源有限,应该动态分配。
__init 用于修 饰函数,__initdata 用于修饰变量。
带有 __init 的修饰符,表示将该函数放到可执行文件的 __init 节区中,该节区的内容只能用于模块的初始化阶段,初始化阶段执行完毕之后,这部分的内容则会被释放掉。
module_init 用于通知内核初始化模块的时候,要使用哪个函数进行初始化。它会将函数地址加入到相应的节区 section 中。因此,开机的时候就可以自动加载该模块了。
4.3 模块卸载
static void __exit hello_exit(void)
{
printk("Hello World Jerome's Module Exit.\n");
}
module_exit(hello_exit);
_exit 主要是用于释放初始化阶段分配的内存,分配的设备号等,是初始化过程的逆过程,该函数的返回值是 void 类型。
#define __exit __section(.exit.text) __exitused __cold notrace
#define __exitdata __section(.exit.data)
__exit 用于修饰函数,__exitdata 用于修饰变量。__exit,表示将该函数放在可执行文件的 __exit 节区,当执行完模块卸载阶段之后,就会自动释放该区域的空间。
宏定义 module_exit 用于告诉内核,当卸载模块时,需要调用 hello_exit 函数。
4.4 参数
bool debug_on = 0;
module_param(debug_on, bool, S_IRUSR);
在调试内核模块的时候,可以使用 module_param 函数来定义一个变量,控制调试信息的输出。
/* 位于内核源码 /linux/moduleparam.h; */
#define module_param(name, type, perm) module_param_named(name, name, type, perm)
如果定义了一个模块参数,则会在 /sys/module/模块名/parameters 下会存在以模块参数为名的文件。
可以通过指令 insmod debug_on=1 来输出调试信息。参数 perm 表示的是该文件的权限。
文件权限名称 | 说明 |
---|---|
S_IRUSR | 用户拥有读权限 |
S_IWUSR | 用户拥有写权限 |
S_IRGRP | 当前用户组的其他用户拥有读权限 |
S_IWGRP | 当前用户组的其他用户拥有写权限 |
S_IROTH | 其他用户拥有读权限 |
S_IWOTH | 其他用户拥有写权限 |
4.5 导出符号
符号指的就是函数和变量。
当模块被装入内核后,它所导出的符号都会记录在内核符号表中。在使用命令 insmod 加载模块后,模块就被连接到了内核,因此可以访问内核的共用符号。
#define EXPORT_SYMBOL(sym) __EXPORT_SYMBOL(sym, "")
EXPORT_SYMBOL 宏用于向内核导出符号,这样的话,其他模块也可以使用这个已经被导出的符号了。类似一般写程序时使用的 extern。
4.6 许可证
Linux 是一款免费的操作系统,采用了 GPL 协议,允许用户可以任意修改其源代码。
GPL 协议的主要内容是软件产品中即使使用了某个 GPL 协议产品提供的库,衍生出一个新产品,该软件产品都必须采用 GPL 协议,即必须是开源和免费使用的,可见 GPL 协议具有传染性。
在 Linux 内核版本 2.4.10 之后,模块必须通过 MODULE_LICENSE 宏声明此模块的许可证,否则在加载此模块时,会提示内核被污染。
MODULE_LICENSE("GPL");
内核模块许可证有 “GPL”,“GPL v2”,“GPL and additional rights”,“Dual SD/GPL”,“Dual MPL/GPL”,“Proprietar”。
4.7 作者
MODULE_AUTHOR("xiechen");
使用 modinfo 中打印出的模块信息中 author 信息便是来自于宏定义 MODULE_AUTHOR。
该宏定义用于声明该模块的作者,可有可无。
4.8 模块描述信息
MODULE_DESCRIPTION("xiechen's hello world module.");
模块信息中 description 信息则来自宏 MODULE_DESCRIPTION,该宏用于描述该模块的功能作用。
4.9 模块别名
MODULE_ALIAS("hello_module");
模块信息中 alias 信息来自于宏定义 MODULE_ALIAS。该宏定义用于给内核 模块起别名。
在使用该模块的别名时,需要将该模块复制到 /lib/modules/ 目录下,使用命令 depmod 更新模块的依赖关系,否则的话,Linux 内核不知道这个模块还有另一个名字。
4.10 实验源码
#include <linux/init.h>
#include <linux/module.h>
bool debug_on = 0;
module_param(debug_on, bool, S_IRUSR);
static int __init hello_init(void)
{
if (debug_on)
printk("[ DEBUG ] debug info output\n");
printk("Jerome's Hello World Module Init\n");
return 0;
}
module_init(hello_init);
static void __exit hello_exit(void)
{
printk("Jerome's Hello World Module Exit\n");
}
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xiechen");
MODULE_DESCRIPTION("xiehcne's hello world module");
MODULE_ALIAS("hello_module");
五、Makefile
obj-m := hello.o
KERNELBUILD :=/lib/modules/$(shell uname -r)/build # 编译内核模块需要的Makefile的路径,Ubuntu下是 /lib/modules/5.4.0-42-generic-generic/build;
default:
make -C $(KERNELBUILD) M=$(shell pwd) modules # 编译内核模块。-C 将工作目录转到 KERNELBUILD,调用该目录下的 Makefile,并向这个 Makefile 传递参数 M 的值是 $(shell pwd) modules;
clean:
rm -rf *.o *.ko *.mod.c .*.cmd *.markers *.order *.symvers .tmp_versions
六、编译模块
文件结构如下。
jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ tree
.
├── hello.c
└── Makefile
0 directories, 2 files
对程序进行编译。
jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ sudo make
[sudo] jerome 的密码:
make -C /lib/modules/5.4.0-42-generic/build M=/home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块 modules # 编译内核模块。-C 将工作目录转到 KERNELBUILD,调用该目录下的 Makefile,并向这个 Makefile 传递参数 M 的值是 /home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块 modules;
make[1]: 进入目录“/usr/src/linux-headers-5.4.0-42-generic”
CC [M] /home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块/hello.o
Building modules, stage 2.
MODPOST 1 modules
CC [M] /home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块/hello.mod.o
LD [M] /home/jerome/文档/2.训练场/1.linux内核/0.编写一个内核模块/hello.ko
make[1]: 离开目录“/usr/src/linux-headers-5.4.0-42-generic”
jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ tree
.
├── hello.c
├── hello.ko
├── hello.mod
├── hello.mod.c
├── hello.mod.o
├── hello.o
├── Makefile
├── modules.order
└── Module.symvers
0 directories, 9 files
这时,在hello.c 所在文件夹就会有 hello.ko ,这个就是编译好的内核模块。
七、加载内核模块
sudo insmod ./hello.ko
使用 dmesg 查看产生的内核信息。
jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ dmesg
[ 6992.056350] Jerome's Hello World Module Init
八、卸载内核模块
sudo rmmod hello
使用 dmesg 查看内核的打印信息。
jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ dmesg
[ 7222.074148] Jerome's Hello World Module Exit
九、终端输出调试信息
加载内核模块时通过传入参数来选择输出调试信息。
sudo insmod hello.ko debug_on=1
使用 dmesg 查看内核打印出来的信息。
jerome@jerome:~/文档/2.训练场/1.linux内核/0.编写一个内核模块$ dmesg
[ 7371.072662] [ DEBUG ] debug info output
[ 7371.072664] Jerome's Hello World Module Init