编写一个 Linux 内核模块

编写一个 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产生的依赖关系,加载依赖的的其他模块。

三、程序架构

内核模块程序的基本结构包括了以下几个部分:

  1. 头文件;
  2. 内核模块加载/卸载函数;
  3. 内核模块的参数;
  4. 内核模块导出符号;
  5. 内核模块的许可证;
  6. 内核模块的其他信息,如作者,模块的描述信息,模块的别名等;

四、编写一个内核模块

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 关键字的作用如下:

  1. static 修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期;
  2. static 的修饰全局变量只能在本文件中访问,不能在其它文件中访问;
  3. 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

Ubuntu下的第一个内核模块编译

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
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

解琛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值