LV06-02-内核模块-02-内核模块的编译

本文主要是内核模块——内核模块编译的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、内核模块怎么编写?

这里我们以一个最简单的字符设备为例,先来了解一下大概的流程,后续再继续详细学习。

1. 模块源文件

我们需要编写一个模块源文件用于测试:

1
2
cd ~/7Linux/imx6ull-kernel
cd drivers/char

可以看到这里有很多的源文件:

image-20241117130956701

我们就在这里新建hello_world_demo.c源文件,并添加以下内容:

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
#include <linux/kernel.h>
#include <linux/init.h> /* module_init module_exit */
#include <linux/module.h> /* MODULE_LICENSE */


// 模块入口函数
int __init hello_world_demo_init(void)
{
printk("hello_world_demo module is running!\n");
return 0;
}

// 模块出口函数
void __exit hello_world_demo_exit(void)
{
printk("hello_world_demo will exit\n");
}

// 将__init定义的函数指定为驱动的入口函数
module_init(hello_world_demo_init);


// 将__exit定义的函数指定为驱动的出口函数
module_exit(hello_world_demo_exit);

/* 模块信息(通过 modinfo hello_world_demo 查看) */
MODULE_LICENSE("GPL"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

2. Kconfig修改

此步骤可以向linux的图形配置界面添加新功能配置选项。

1
2
cd  driver/char/                      # 进入hello_world_demo.c的同级目录
vim Kconfig # 打开相关配置文件

找到如下语句:

1
source "drivers/tty/Kconfig"

在该语句下边添加以下内容:

1
2
3
4
config MY_HELLO
tristate "This is my hello test!"
help
This is a test for kernel new function
image-20241117145426338

然后保存退出即可,我们可以试一下有什么效果,我们回到顶层源码目录下,打开图形配置界面:

1
2
cd ~/7Linux/imx6ull-kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

我们按以下层级找到我们新添加的配置项:

1
2
3
Device Drivers  --->
Character devices --->
< > This is my hello_world_demo module load test! (NEW)

如下图所示:

image-20241117145605191

我们在这里,光标移动到新增加的配置项,按空格键,前面尖括号中的符号就会发生改变,*表示编译进内核,M表示编译成内核模块。这个分别对应两种加载方式,后面我们再讨论。

3. Makefile修改

我们添加了新源码文件,自然要参与编译的,我们打开hello_world_demo.c同级目录下的Makefile:

1
vim drivers/char/Makefile

我么在这里面添加一句:

1
obj-$(CONFIG_MY_HELLO)          += hello_world_demo.o
image-20241117133027783

【注意】这里需要是CONFIG_xxx,这个xxx就是上边修改Kconfig的时候添加的config MY_HELLOMY_HELLO。这样的话后面我们需要在内核的图形配置界面打开这个模块,才能进行编译。

二、模块的编译

模块的编译有两种方式:一种是直接编译进内核,这样就只会生成一个中间文件.o,另一种是编译成单独的内核模块,会生成一个.ko文件。

1. 编译进内核

1.1 配置编译方式

这个我们在kernel的图形配置界面进行配置,我们这里打开linux内核图形配置界面:

1
2
cd ~/7Linux/imx6ull-kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

然后就会打开图形配置界面,我们按照下面的配置项路径找到刚才新增的配置项,选中后按空格键,将新功能前面尖括号的标识改为*,这就表示将新功能编译到内核中:

1
2
3
Device Drivers  --->
Character devices --->
<*> This is my hello_world_demo module load test!

效果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.config - Linux/arm 4.19.71 Kernel Configuration
> Device Drivers > Character devices ─────────────────────────────────────────
┌─────────────────────────── Character devices ───────────────────────────┐
│ Arrow keys navigate the menu. <Enter> selects submenus ---> (or empty │
│ submenus ----). Highlighted letters are hotkeys. Pressing <Y> │
│ includes, <N> excludes, <M> modularizes features. Press <Esc><Esc> to │
│ exit, <?> for Help, </> for Search. Legend: [*] built-in [ ] │
│ ┌────^(-)─────────────────────────────────────────────────────────────┐ │
│ │ [*] Unix98 PTY support │ │
│ │ [ ] Legacy (BSD) PTY support │ │
│ │ [ ] Non-standard serial port support │ │
│ │ < > HSDPA Broadband Wireless Data Card - Globe Trotter │ │
│ │ < > GSM MUX line discipline support (EXPERIMENTAL) │ │
│ │ < > Trace data sink for MIPI P1149.7 cJTAG standard │ │
│ │ [*] Automatically load TTY Line Disciplines │ │
│ │ <*> This is my hello_world_demo module load test! │ │
│ │ [*] /dev/mem virtual device support │ │
│ │ [ ] /dev/kmem virtual device support │ │
│ └────v(+)─────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ <Select> < Exit > < Help > < Save > < Load > │
└─────────────────────────────────────────────────────────────────────────┘

然后我们选择保存,最后退出即可。然后我们可以看一下.config文件的变化:

image-20241117145759071

1.2 编译内核源码

我们不需要清空之前的配置文件,直接重新编译即可,注意这里不要执行这个使用默认配置文件的命令:

1
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alpha_emmc_defconfig

若是执行了,我们前面的配置就会被清空,需要重新在图形界面配置。

1
2
# make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean # 这个清理的命令可以不执行
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16 # 只编译内核

编译完毕后如下:

image-20241117150330450

我们看一下drivers/char目录,可以看到这里只生成了一个.o文件:

image-20241117152548998

1.3 启动测试

因为这个是直接编译进内核源码的,在启动的时候我们不需要做任何操作,不需要像后面独立模块一样需要用一些加载命令去加载,内核就会自动加载这个新功能,这里我们可以直接试一下:

1
cp arch/arm/boot/zImage ~/3tftp/ #将编译出来带有hello_world_demo功能的镜像拷贝到tftp服务器

然后我们启动开发板,之前设置的从tftp下载zImage,所以这里我们直接拷贝到tftp服务器目录下即可,启动后我们可以看到打印日志:

image-20241117153042631

2. 编译成独立模块

新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件.ko。然后可以通过一些命令进行插入或者卸载这个内核模块。

这种编译方式又有两种形式,主要是根据我们的模块源码位置来区分,我们可以在linux内核源码中编写模块的源码然后编译,这种方式显然不利于我们对多个模块源码的管理。内核模块的源码可以在linux内核源码外,通过调用内核源码相关的内容完成对独立模块的编译,第二种显然更利于我们对驱动模块源码的管理。接下来我们就来看一看这两种编译方式怎么实现。

2.1 模块源码在linux内核源码中

2.1.1 配置编译方式

我们还是使用上边的源文件hello_world_demo.c,该文件还是包含在linux内核源码中,这一次我们配置编译方式为另外一种:

修改新功能编译方式

1
2
cd ~/7Linux/imx6ull-kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

然后将新功能前边配置改为M,表示编译成独立的模块:

1
2
3
Device Drivers  --->
Character devices --->
<M> This is my hello_world_demo module load test!

最后效果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.config - Linux/arm 4.19.71 Kernel Configuration
> Device Drivers > Character devices ─────────────────────────────────────────
┌─────────────────────────── Character devices ───────────────────────────┐
│ Arrow keys navigate the menu. <Enter> selects submenus ---> (or empty │
│ submenus ----). Highlighted letters are hotkeys. Pressing <Y> │
│ includes, <N> excludes, <M> modularizes features. Press <Esc><Esc> to │
│ exit, <?> for Help, </> for Search. Legend: [*] built-in [ ] │
│ ┌────^(-)─────────────────────────────────────────────────────────────┐ │
│ │ [ ] Non-standard serial port support │ │
│ │ < > HSDPA Broadband Wireless Data Card - Globe Trotter │ │
│ │ < > GSM MUX line discipline support (EXPERIMENTAL) │ │
│ │ < > Trace data sink for MIPI P1149.7 cJTAG standard │ │
│ │ [*] Automatically load TTY Line Disciplines │ │
│ │ <M> This is my hello_world_demo module load test! │ │
│ │ [*] /dev/mem virtual device support │ │
│ │ [ ] /dev/kmem virtual device support │ │
│ │ Serial drivers ---> │ │
│ │ <*> Serial device bus ---> │ │
│ └────v(+)─────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ <Select> < Exit > < Help > < Save > < Load > │
└─────────────────────────────────────────────────────────────────────────┘

我们来看一下配置文件.config的变化:

image-20241117153159666

2.1.2 编译内核

这里我们需要重新编译内核,主要是去掉之前内核镜像中带的hello_world_demo模块,到后面编译成独立模块的时候,就不需要吧镜像完整的编译一遍了。

1
2
cd ~/7Linux/imx6ull-kernel # 回到顶层源码目录下
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16 # 只编译内核

用重新编译过的这个镜像的话,再在开发板上启动就不会有我们demo中打印的相关信息了。

2.1.3 编译模块

接下来我们编译模块:

1
2
cd ~/7Linux/imx6ull-kernel  # 回到顶层源码目录下
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules -j16 # 编译模块

不出意外的话我们应该可以看到以下打印信息:

image-20241117154215365

会发现这里是在生成各种ko文件。我们自己的模块呢?这里也有:

image-20241117154309446

我们来看一下drivers/char目录:

image-20241117154418513

会发现这里生成了一个ko文件,这就是我们最后要的独立模块。

2.2 模块源码独立在其他目录

这样每次都要使用 linux 源码目录下操作,文件也太多了,而且,这样会把所有的内核模块都编译一遍,还是很麻烦的。我们现在尝试在其他的目录单独创建模块驱动源码文件,然后调用linux源码进行编译,生成我们所需要的.ko内核模块文件。

注意:这种方式就不需要linux的图形界面的各种配置了。

2.2.1 源码编写

  • (1)新建一个目录用于存放我们的模块源码
1
2
mkdir ~/imx6ull-driver-demo/02_module_load/out_of_source # 新建一个目录
cd ~/imx6ull-driver-demo/02_module_load/out_of_source # 进入相应目录

这里随便自定义一个目录就是了,都一样的。

  • (2)编写模块源码文件

这里我直接拷贝之前的文件到这里来:

1
cp ~/7Linux/imx6ull-kernel/drivers/char/hello_world_demo.c .

2.2.2 Makefile文件编写

点击查看 Makefile 文件内容
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
34
35
36
37
38
# 模块名和模块测试APP名称
MODULE_NAME := hello_world_demo
# NFS 共享目录
TFTP_SERVER ?= ~/3tftp
NFS_SERVER ?= ~/4nfs

TFTP_DIR ?= $(TFTP_SERVER)
ROOTFS ?= $(NFS_SERVER)/imx6ull_rootfs

ifeq ($(KERNELRELEASE),)

# 选择可执行文件运行的平台
KERNELDIR ?= ~/7Linux/imx6ull-kernel

PWD := $(shell pwd)
# 编译模块和测试程序
modules:
$(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) modules

.PHONY: clean
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions .cache.mk

help:
@echo "\033[1;32m================================ Help ================================\033[0m"
@echo "Ubuntu may need to add sudo:"
@echo "dmesg # View information printed by the kernel"
@echo "file <module_name>.ko # View \".Ko\" file information"
@echo "make ARCH=arm # arm platform"
@echo "\033[1;32m======================================================================\033[0m"

print:
@echo "KERNELDIR = $(KERNELDIR)"

else
CONFIG_MODULE_SIG = n
obj-m += $(MODULE_NAME).o
endif

2.2.3 编译模块

1
make          # 编译模块

不出意外的话应该会有这些提示信息出现:

image-20241117160148522

然后我们看一下当前目录下都有哪些文件:

image-20241117160252648

发现这里生成了一堆的中间文件,同样生成了我们所需要的ko文件。