Skip to content

LV010-内核模块的编译

一、内核模块怎么编写?

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

1. 模块源文件

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

shell
cd ~/7Linux/imx6ull-kernel
cd drivers/char

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

image-20241117130956701

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

c
#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 的图形配置界面添加新功能配置选项。

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

找到如下语句:

shell
source "drivers/tty/Kconfig"

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

shell
config MY_HELLO
	tristate "This is my hello test!"
	help
		This is a test for kernel new function

image-20260117112447069

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

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

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

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

如下图所示:

image-20260117112829370

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

3. Makefile 修改

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

shell
vim drivers/char/Makefile

我么在这里面添加一句:

makefile
obj-$(CONFIG_MY_HELLO)          += hello_world_demo.o

image-20260117113013937

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

二、模块的编译

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

1. 编译进内核

1.1 配置编译方式

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

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

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

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

效果是这样的:

shell
 .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 编译内核源码

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

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

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

shell
# 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 启动测试

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

shell
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 内核源码中,这一次我们配置编译方式为另外一种:

修改新功能编译方式

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

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

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

最后效果是这样的:

shell
 .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 模块,到后面编译成独立模块的时候,就不需要吧镜像完整的编译一遍了。

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

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

2.1.3 编译模块

接下来我们编译模块:

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

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

image-20241117154309446

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

image-20241117154418513

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

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

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

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

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

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

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

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

shell
cp ~/7Linux/imx6ull-kernel/drivers/char/hello_world_demo.c .
2.2.2 Makefile 文件编写
makefile
# 模块名和模块测试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 编译模块
shell
make          # 编译模块

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

image-20241117160148522

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

image-20241117160252648

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