上一篇文章介绍了 Linux 驱动编程需要了解的基础知识:
Linux驱动编程必备基础知识
这篇文章来介绍一下,如何构建一个驱动模块。构建一个模块,可以在两个地方完成:
内核树外部
除此之外,还需要编写相应的构建脚本文件 makefile。
接下来,我们逐步进行介绍。
makefile 是用来执行一组操作的特殊文件,其中最重要的操作是程序的编译。专用工具 make 用于解析makefile。
在说明整个make文件之前,先介绍一下 obj-
例如:
obj-y += mymodule.o
告诉 kbuild 在当前目录中有一个名为 mymodule.o 的对象。mymodule.o 将从 mymodule.c 或 mymodule.S 构建。
如果
如果后边跟着是某个目录,例如
obj- += onedir/
kbuild 应该进入 onedir 目录,查找其中所有的 makefile 并处理它们,从而决定应该构建哪些对象。
一份完整的模块构建Makefile 示例:
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
obj-m := helloworld.o
all:
$(MAKE) -C $(KERNELDIR) M=$(shell pwd) modules;
clean:
$(MAKE) -C $(KERNELDIR) M=$(shell pwd) clean;
obj-m := hellowolrd.o
解析,obj-m
列出要构建的模块。对于每一个
KERNELDIR := /lib/modules/$(shell uname -r)/build
解析,KERNELDIR
是预构建的内核源码的位置。构建任何模块都需要预构建内核。 -C
要求 make 在读取 makefile 或执行其他任何操作之前先更改到指定的目录。
M=$(shell pwd)
解析,这与内核构建系统相关。内核 makefile 使用这个变量来定位要构建的外部模块的目录,.c 文件应该被放置在这。
(MAKE) -C $(KERNELDIR ) M=$(shell pwd) modules;
构建驱动模块的规则
在内核树中构建驱动程序,需要把驱动程序的代码文件放在特定的目录中。驱动程序中的每个子目录都有 makefile 和 kconfig。
例如,驱动文件 mychardev.c 为字符驱动程序源码,则应该把他放在内核源码的 drivers/char 目录中。
一个 kconfig 的示例文件如下:
config PACKT_MYCDEV
tristate "Our packtpub special Characterdriver"
default m
help
Say Y here if you want to support the/dev/mycdev device.
The /dev/mycdev device is used to access packtpub.
同时,在这个目录下的 makefile 文件中添加一下语句:
obj-$(CONFIG_PACKT_MYCDEV) += mychardev.o
注意,.o 文件名称必须与 .c 文件名完全一致。
配置完成后,可以分别使用 make 和 make modules 构建内核和模块。
内核源码树中包含的模块安装在 /lib/modules/$(KERNELRELEASE)/kernel/
中。在Linux系统上,它是/lib/modules/$(uname -r)/kernel/
在构建外部模块之前,需要有一个完整的、预编译的内核源代码树。内核源码树版本必须与将加载和使用模块的内核相同。
有两种方法可以获得预构建的内核版本:
自己构建
sudo apt-get update
sudo apt-get install linux-headers-$(uname -r)
这将只安装头文件,而不是整个源代码树。
头文件将被安装在 /usr/src/linux-headers-$(uname -r)
目录下 。
有一个符号链接 /lib/modules/$(uname-r)/build
,指向前面安装的头文件,这应该是在 makefile 中指定为内核目录的路径。
以上内容准备完成之后,就可以进行构建驱动模块。
处理完 makefile 后,只需要切换到源码目录并运行 make 命令,即可开始构建模块。
一个简单模块程序 helloworld.c
#include
#include
#include
/* 模块入口点函数 */
static int helloworld_init(void)
{
pr_info("Hello world!\n");
return 0;
}
/* 模块出口点函数 */
static void helloworld_exit(void)
{
pr_info("End of the world\n");
}
/* 指定函数用途 */
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_AUTHOR("zsky");
MODULE_LICENSE("GPL");
构建脚本文件 Makefile
KERNELDIR ?= /lib/modules/`uname -r`/build
obj-m:= helloworld.o
all :
$(MAKE) -C $(KERNELDIR) M=$(shell pwd) modules;
clean:
$(MAKE) -C $(KERNELDIR) M=$(shell pwd) clean;
rm -f *.ko;
开始构建:
$ make
make -C /lib/modules/`uname -r`/build M=/home/user/learn/drivers/chap2 modules;
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-73-generic'
CC [M] /home/user/learn/drivers/chap2/helloworld.o
MODPOST /home/user/learn/drivers/chap2/Module.symvers
CC [M] /home/user/learn/drivers/chap2/helloworld.mod.o
LD [M] /home/user/learn/drivers/chap2/helloworld.ko
BTF [M] /home/user/learn/drivers/chap2/helloworld.ko
Skipping BTF generation for /home/user/learn/drivers/chap2/helloworld.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-73-generic'
构建完成后,在当先目录下会生成一些文件,其中 helloworld.ko
,为最终生成的内核模块。
交叉编译
上面的例子使用的是本地构建,在 x86 机器上为x86 机器编译。交叉编译怎么实现?
这个过程是在机器 A(称为宿主机)上编译,该代码要运行在机器 B(称为目标机)上;宿主机和目标机具有不同的体系结构。
常见的交叉编译是在 x86 机器上构建的代码要运行在 ARM 架构上。交叉编译内核模块时,构建 makefile 需要指定两个变量:ARCH 和 CROSS_COMPILE,它们分别表示目标体系结构和编译器的前缀名称。
因此,内核模块本地编译和交叉编译之间的差别在于 makefile 构建文件。
另外,还需要一份目标机器正在使用的内核源码,编译模块的时候需要用到。
模块构建完成后,可以通过 insmod
指令进行装载。注意装载和卸载需要 root 访问权限,可以在模块加载指令之前加上 sudo
$ sudo insmod helloworld.ko
指令执行完,看不到任何信息。
加载内核模块,模块的打印信息需要通过 dmsg
指令查看。可以看到入口函数 helloworld_init()
打印的 Hello world!
卸载模块,通过 rmmod
指令
sudo rmmod helloworld
同样出口函数打印的信息,dmesg
指令查看。
另外,用 modinfo
查看模块信息如下:
$ modinfo helloworld.ko
filename: /home/user/learn/drivers/chap2/helloworld.ko
license: GPL
author: zsky
srcversion: 6EFA6AC1502C67E96C09216
depends:
retpoline: Y
name: helloworld
vermagic: 5.15.0-73-generic SMP mod_unload modversions
觉得文章不错,点击“分享”、“赞”、“在看” 呗!