Linux 驱动开发之内核模块分析2(基于Linux6.6)---内核模块编译 Makefile分析
一、模块的编译
在 Linux 驱动开发中,驱动编译主要分为静态编译和动态编译两种方式。这两种编译方式在内核模块的加载方式、依赖关系、更新机制等方面有所不同,适用于不同的场景。
1. 静态编译(Static Compilation)
静态编译指的是将驱动代码直接编译到内核映像中。这样,驱动在内核启动时就已经被包含,不需要单独的模块加载或卸载过程。
特点:
- 编译过程:驱动的源代码被直接编译到内核映像中,在内核构建时就被静态链接到内核中。
- 无需模块加载:一旦内核启动,驱动程序即加载并运行,不需要用户手动加载或卸载模块。
- 内核与驱动绑定:驱动代码和内核紧密结合,更新驱动需要重新编译内核。
- 不支持热插拔:无法在系统运行时动态加载或卸载驱动。
使用场景:
- 嵌入式设备:对于资源有限、需要较高稳定性的嵌入式系统,通常会选择静态编译,以减少内存使用和启动时间。
- 高安全性要求:在某些安全性要求较高的环境中,静态编译可以避免动态加载模块带来的潜在安全风险。
编译步骤:
- 在内核的
.config
文件中启用相应的驱动选项,通常是CONFIG_<driver_name>=y
。 - 重新编译内核并安装。
- 驱动将作为内核的一部分加载。
例如,假设我们有一个网卡驱动 eth_driver
,我们在 .config
文件中启用它:
CONFIG_NET_VENDOR_XYZ=y
CONFIG_ETH_DRIVER=y
然后重新编译内核:
make && make install
此时,eth_driver
驱动就会被静态编译进内核。
2. 动态编译(Dynamic Compilation)
动态编译(通常指的是模块化编译)指的是将驱动代码编译为独立的内核模块,这些模块可以在运行时动态加载和卸载。
特点:
- 编译过程:驱动程序被编译为一个或多个内核模块(
.ko
文件),并在需要时动态加载到内核中。 - 动态加载与卸载:模块可以在系统运行时通过
insmod
(加载)和rmmod
(卸载)命令进行加载和卸载。也可以使用modprobe
管理模块依赖。 - 节省内存:仅加载需要的驱动模块,未使用的驱动不会占用系统资源。
- 支持热插拔:在运行时可以根据需要动态加载驱动程序,例如插入一个新硬件设备时,自动加载相应的驱动。
使用场景:
- 硬件支持:对于需要支持多种硬件设备的系统,使用动态编译可以根据实际硬件情况加载对应的驱动。
- 模块更新:开发过程中,如果需要更新驱动程序,只需重新编译模块并加载,而不需要重新编译整个内核。
- 驱动调试:对于驱动开发人员,动态编译和加载模块便于调试和测试,不需要每次修改后都重新编译内核。
编译步骤:
- 在内核的
.config
文件中启用相应的驱动选项,通常是CONFIG_<driver_name>=m
。 - 使用
make modules
编译内核模块。 - 使用
insmod
或modprobe
加载模块。
例如,假设我们有一个网络驱动 eth_driver
,我们在 .config
文件中启用它作为模块:
CONFIG_NET_VENDOR_XYZ=m
CONFIG_ETH_DRIVER=m
然后编译内核模块:
make modules
生成的内核模块(eth_driver.ko
)可以通过以下命令加载:
sudo insmod eth_driver.ko
或者通过 modprobe
加载,并自动解析模块依赖:
sudo modprobe eth_driver
3. 静态编译 vs 动态编译
特性 | 静态编译 | 动态编译 |
---|---|---|
编译方式 | 驱动被直接编译进内核映像 | 驱动编译为独立的模块(.ko ) |
加载方式 | 驱动随内核一起加载 | 驱动可以动态加载和卸载 |
内存占用 | 总是占用内存,不能按需加载 | 仅加载需要的模块,占用较少内存 |
更新驱动 | 更新驱动需要重新编译整个内核 | 可以单独编译并加载更新模块 |
热插拔支持 | 不支持 | 支持热插拔,支持在运行时加载模块 |
使用场景 | 嵌入式系统、高安全性需求 | 大型系统、多硬件支持、驱动开发 |
二、具体编译过程分析
对于一个普通的linux设备驱动模块,以下是一个经典的makefile代码,使用下面这个makefile可以完成大部分驱动的编译,使用时只需要修改一下要编译生成的驱动名称即可。只需修改obj-m的值。
ifneq ($(KERNELRELEASE),) obj-m:=hello.o else KDIR := /lib/modules/$(shell uname -r)/build PWD:=$(shell pwd) all: make -C $(KDIR) M=$(PWD) modules clean: rm -f *.ko *.o *.symvers *.cmd *.cmd.o endif |
2.1、makefile 中的变量
先说明以下makefile中一些变量意义:
(1)KERNELRELEASE 在linux内核源代码中的顶层makefile中有定义
(2)shell pwd 取得当前工作路径
(3)shell uname -r 取得当前内核的版本号
(4)KDIR 当前内核的源代码目录。
关于linux源码的目录有两个,分别为
"/lib/modules/$(shell uname -r)/build"
"/usr/src/linux-header-$(shell uname -r)/"
但如果编译过内核就会知道,usr目录下那个源代码一般是我们自己下载后解压的,而lib目录下的则是在编译时自动copy过去的,两者的文件结构完全一样,因此有时也将内核源码目录设置成/usr/src/linux-header-$(shell uname -r)/。关于内核源码目录可以根据自己的存放位置进行修改。
(5)make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
这就是编译模块了:
a -- 首先改变目录到-C选项指定的位置(即内核源代码目录),其中保存有内核的顶层makefile;
b -- M=选项让该makefile在构造modules目标之前返回到模块源代码目录;然后,modueles目标指向obj-m变量中设定的模块;在上面的例子中,我们将该变量设置成了hello.o。
2.2、make 的的执行步骤
a -- 第一次进来的时候,宏“KERNELRELEASE”未定义,因此进入 else;
b -- 记录内核路径,记录当前路径;
由于make 后面没有目标,所以make会在Makefile中的第一个不是以.开头的目标作为默认的目标执行。默认执行all这个规则
c -- make -C $(KDIR) M=$(PWD) modules
-C 进入到内核的目录执行Makefile ,在执行的时候KERNELRELEASE就会被赋值,M=$(PWD)表示返回当前目录,再次执行makefile,modules 编译成模块的意思
所以这里实际运行的是
make -C /lib/modules/2.6.13-study/build M=/home/fs/code/1/module/hello/ modules
d -- 再次执行该makefile,KERNELRELEASE就有值了,就会执行obj-m:=hello.o
obj-m:表示把hello.o 和其他的目标文件链接成hello.ko模块文件,编译的时候还要先把hello.c编译成hello.o文件
可以看出make在这里一共调用了3次
1)-- make
2)-- linux内核源码树的顶层makedile调用,产生。o文件
3)-- linux内核源码树makefile调用,把.o文件链接成ko文件
2.3、编译多文件
若有多个源文件,则采用如下方法:
obj-m := hello.o
hello-objs := file1.o file2.o file3.o
三、内部编译简单说明
在 Linux 内核开发中,Makefile 用于管理和自动化编译过程。内核源码树的根目录和各个子目录都包含 Makefile,用于控制源代码的编译过程。在这里,我将通过一个简单的实例来介绍如何编写和应用一个 Linux 内核模块的 Makefile。
1. 简单的内核模块结构
假设我们要编写一个简单的 Linux 内核模块 hello.c
,这个模块会在加载时打印一条消息,在卸载时打印另一条消息。目录结构如下:
/home/user/kernel_module/
├── Makefile
└── hello.c
其中 hello.c
是内核模块源代码,Makefile
用于编译该模块。
2. 编写内核模块源代码 (hello.c
)
首先,我们创建一个简单的内核模块,代码如下:
// hello.c
#include <linux/module.h> // 所需的头文件
#include <linux/kernel.h>
#include <linux/init.h>
// 模块初始化函数
static int __init hello_init(void) {
printk(KERN_INFO "Hello, world!\n");
return 0;
}
// 模块退出函数
static void __exit hello_exit(void) {
printk(KERN_INFO "Goodbye, world!\n");
}
// 指定模块的初始化和退出函数
module_init(hello_init);
module_exit(hello_exit);
// 模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World Kernel Module");
这个模块包含两个基本的函数:
hello_init()
:模块加载时执行。hello_exit()
:模块卸载时执行。
3. 编写 Makefile
接下来,我们为这个模块编写一个简单的 Makefile。Makefile 的作用是告诉 make
工具如何编译源文件以及生成最终的内核模块。
# Makefile
# 内核源代码路径。这里假设你在开发模块时的环境下,内核源码已被安装。
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
# 当前目录
PWD := $(shell pwd)
# 模块的源文件
obj-m := hello.o
# 默认目标
all:
make -C $(KERNEL_DIR) M=$(PWD) modules
# 清理目标
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
Makefile 解释
-
KERNEL_DIR
:指定内核源代码的路径。使用$(shell uname -r)
获取当前运行内核的版本,并从该版本的路径中获取build
目录。$(KERNEL_DIR)
会指向你的内核源码目录,通常在/lib/modules/$(kernel_version)/build
位置。
-
obj-m
:这是一个标准的 Makefile 变量,用于指定内核模块对象文件。obj-m := hello.o
表示我们要编译hello.o
对象文件,最终会生成一个名为hello.ko
的内核模块。 -
all
目标:这是默认的目标,make
命令执行时会自动调用它。这个目标会调用内核的make
系统来编译模块。make -C $(KERNEL_DIR) M=$(PWD) modules
命令的作用是:-C $(KERNEL_DIR)
让make
切换到内核源码目录。M=$(PWD)
告诉make
当前目录是内核模块的目录,确保它知道在哪里找到hello.c
文件。modules
是编译模块的目标。
-
clean
目标:当你想要清理编译过程中的临时文件时,执行make clean
。这个目标会调用make -C $(KERNEL_DIR) M=$(PWD) clean
来清理模块相关的文件。
4. 编译模块
在终端中进入 kernel_module
目录,执行以下命令来编译内核模块:
make
这个命令会使用 Makefile
中的规则,将 hello.c
编译成 hello.ko
内核模块文件。
如果编译成功,你会看到类似以下的输出:
make -C /lib/modules/5.4.0-42-generic/build M=/home/user/kernel_module modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-42-generic'
CC [M] /home/user/kernel_module/hello.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/user/kernel_module/hello.mod.o
LD [M] /home/user/kernel_module/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-42-generic'
生成的 hello.ko
就是你的内核模块文件。
5. 加载和卸载模块
你可以通过以下命令来加载和卸载模块:
-
加载模块:
-
sudo insmod hello.ko
加载后,内核会打印
Hello, world!
消息。 -
卸载模块:
-
sudo rmmod hello
卸载后,内核会打印
Goodbye, world!
消息。 -
查看内核日志: 可以使用
dmesg
命令查看内核日志输出,确认模块的加载和卸载信息:
-
dmesg | tail
6. 清理构建文件
如果你想要清理编译生成的文件,可以运行:
make clean
这将删除所有编译生成的临时文件,包括 hello.o
和 hello.ko
。