参考书籍:《Linux设备驱动程序(第3版)》
次实例演示如何构建一个简单的内核驱动文件,然后加载此内核驱动文件到Linux内核中,最后从内核卸载此驱动。
源码 hello.c
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
printk(KERN_ALERT "Hello, world\n"); // 用空格分割参数?
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n"); // 用空格分割参数?
}
module_init(hello_init);
module_exit(hello_exit);
构建文件 Makefile
Makefile文件中各行含义解析见【附录A】
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~core .depend .*.cmd *.ko *.mod.c .tmp_versions
.PHONY: modules modules_install clean
else
obj-m := hello.o
endif
构建方法
首先介绍我目录结构:
$ tree
.
├── hello.c
└── Makefile
构建
$ make
make -C /lib/modules/5.4.0-100-generic/build M=/home/dog/Dev/Linux/driver/hello modules
make[1]: 进入目录“/usr/src/linux-headers-5.4.0-100-generic”
CC [M] /home/dog/Dev/Linux/driver/hello/hello.o
Building modules, stage 2.
MODPOST 1 modules
CC [M] /home/dog/Dev/Linux/driver/hello/hello.mod.o
LD [M] /home/dog/Dev/Linux/driver/hello/hello.ko
make[1]: 离开目录“/usr/src/linux-headers-5.4.0-100-generic”
构建后的目录结构(不可见文件省略)
$ tree
.
├── hello.c
├── hello.ko
├── hello.mod
├── hello.mod.c
├── hello.mod.o
├── hello.o
├── Makefile
├── modules.order
└── Module.symvers
测试驱动文件
$ sudo insmod hello.ko #需要root权限;加载驱动文件到内核
$ sudo rmmod hello #从内核卸载驱动模块
查看相关操作的系统日志
【附录A】Makefile解析
可以阅读《Linux设备驱动程序(第3版)》第28页“编译和装载”,对此类型的Makefile有详细说明。
以下为我个人在实践中总结而来:
先上Makefile文件
从 make 操作开始分析:
$ make
"--------->Entery" # 第1次进入
make -C /lib/modules/5.4.0-100-generic/build M=/home/dog/Dev/Linux/driver/hello modules
make[1]: 进入目录“/usr/src/linux-headers-5.4.0-100-generic”
"--------->Entery" # 第2次进入
CC [M] /home/dog/Dev/Linux/driver/hello/hello.o
"--------->Entery" # 第3次进入
Building modules, stage 2.
MODPOST 1 modules
CC [M] /home/dog/Dev/Linux/driver/hello/hello.mod.o
LD [M] /home/dog/Dev/Linux/driver/hello/hello.ko
make[1]: 离开目录“/usr/src/linux-headers-5.4.0-100-generic”
第1次进入:执行第3~8行(第8行仅执行到$(MAKE) -C $(KERNELDIR))
第2次进入:执行第18~19行
第3次进入:执行第8行的“M=$(PWD) modules”部分
如上分析自认为不太正确,但大概是这样的流程,且进入Makefile3次是确定无疑。请自行看“$make”的输出信息也能明白是怎样的流程。
ifeq ($(KERNELRELEASE),)
驱动编译要用到kernel的Makefile文件——也就是源码树的编译系统(kbuild构建体系)。开发者的Makefile会被多次执行,因此需要判断是从哪个入口跳转(是从kernel的Makefile跳转,还是直接make开发者的Makefile)过来执行。因此需要逻辑判断以区分。
$(KERNELRELEASE)被定义在$(KERNELDIR)变量表示目录下的某个Makefile中。在开发者目录下执行“make”命令时,$(KERNELDIR)下的Makefile尚未执行,因此ifeq ($(KERNELRELEASE),)判断为真,即执行
KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd)
第一行:获取当前系统kernel的build目录
为什么要在目录中使用$(shell uname -r)变量来组合目录?因为一般Linux系统下会有多个kernel目录,我们只选取当前系统正在使用的版本(即uname -r输出的那个),如Ubuntu18.04下有如下kernel目录
第二行:获取make开始执行的目录
-C $(KERNELDIR)
切换到$(KERNELDIR)目录下执行(即make执行此目录下的Makefile文件,此处为内核构建体系的Makefile文件)
M=$(PWD)
返回到开发者构建目录下执行(即make执行M=$(PWD)后面的命令modules)。
M是kbuild体系中的makefile脚本中定义的一个变量
# Use make M=dir to specify directory of external module to build # Old syntax make ... SUBDIRS=$PWD is still supported # Setting the environment variable KBUILD_EXTMOD take precedence ifdef SUBDIRS KBUILD_EXTMOD ?= $(SUBDIRS) endif ifdef M //如果没有定义或赋值M,此处M未定义(undefined) ifeq ("$(origin M)", "command line") //如果定义了,此句用来判断M是否从命令行来 KBUILD_EXTMOD := $(M) endif endif
else
obj-m := hello.oobj-m是kbuild构建系统需要生成的目标文件。kbuild构建系统会根据obj-m中对应目标文件的文件名找到对应的.c文件来生成对应的.o文件。
如果hello.o由多个源文件生成(比如file1.c,file2.c),则需要使用“module-objs”,例如:
obj-m := hello.o
module-objs := file1.o file2.o
.PHONY: modules modules_install clean
目标的具体意思是如果在Makefile的工作目录中有名如:modules,modules_install,clean等文件时命令会出错。
参考:
make -C $(LINUX_KERNEL_PATH) M=$(PWD) modules中的M选项-qinzhong-ChinaUnix博客
编译内核模块的Makefile中的($(KERNELRELEASE)_在水_的博客-CSDN博客