【Linux内核模块】模块的编译:从代码到可加载模块的 “变身术“
一、内核模块编译的特殊性为什么不能直接用 gcc普通 C 程序编译很简单gcc hello.c -o hello就行但内核模块可不行。这就像做面包和做蛋糕的区别 —— 虽然都是面粉做的但烤箱温度、配料比例完全不同。1.1 内核模块的 特殊身份内核模块不是独立程序而是要嵌入到内核中的 插件意味着必须使用与内核完全一致的编译选项比如字节序、对齐方式不能依赖标准 C 库glibc只能用内核提供的函数如 printk必须与内核版本严格匹配不同内核版本的头文件差异很大1.2 举个直观的例子如果你用系统默认的 gcc 编译模块会得到类似这样的错误代码语言javascriptAI代码解释error: printk undeclared (first use in this function)这不是因为 printk 不存在而是因为没有正确引入内核头文件和编译选项。1.3 内核模块编译的三大要素二、编译前的准备搭建 工作台在开始编译前需要准备好必要的工具和环境就像做蛋糕前要准备好烤箱和原料。2.1 安装内核开发包最关键的是要安装与当前内核版本匹配的内核源码或开发包Ubuntu/Debian系统代码语言javascriptAI代码解释# 查看当前内核版本 uname -r # 安装对应版本的内核开发包 sudo apt-get install linux-headers-$(uname -r)CentOS/RHEL 系统代码语言javascriptAI代码解释sudo yum install kernel-devel-$(uname -r)这些包会安装编译模块所需的头文件和配置文件通常放在/lib/modules/$(uname -r)/build目录下。2.2 准备测试代码创建一个简单的测试模块hello.c代码语言javascriptAI代码解释#include linux/init.h #include linux/module.h static int __init hello_init(void) { printk(KERN_INFO Hello, 内核模块世界\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO Goodbye, 内核模块世界\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(一个简单的测试模块);这个模块加载时会打印一句欢迎语卸载时打印告别语。三、核心工具Makefile 的写法内核模块的编译完全依赖 Makefile这是整个过程的 指挥中心。一个最简单的内核模块 Makefile 只有几行但每一行都有特殊含义。3.1 最基础的 Makefile创建文件Makefile注意首字母大写代码语言javascriptAI代码解释# 声明要编译的模块 obj-m hello.o # 内核源码树路径自动获取当前系统内核 KERNELDIR ? /lib/modules/$(shell uname -r)/build # 当前目录路径 PWD : $(shell pwd) # 默认编译目标 default: # 进入内核源码树执行编译 $(MAKE) -C $(KERNELDIR) M$(PWD) modules # 清理编译生成的文件 clean: $(MAKE) -C $(KERNELDIR) M$(PWD) cleanobj-m hello.o告诉内核构建系统要编译一个名为hello.ko的模块.o文件会被链接成.ko模块KERNELDIR指定内核源码树的位置/lib/modules/$(uname -r)/build是标准位置PWD记录当前目录路径让内核构建系统知道模块代码在哪里(MAKE)−C(KERNELDIR) M(PWD) modules这是核心命令意思是 -C (KERNELDIR)进入内核源码目录M(PWD)告诉内核构建系统模块源码在当前目录modules执行内核模块编译目标3.2 编译模块在终端执行make命令代码语言javascriptAI代码解释make如果一切顺利会看到类似这样的输出代码语言javascriptAI代码解释make -C /lib/modules/5.4.0-100-generic/build M/home/user/kernel-modules modules make[1]: Entering directory /usr/src/linux-headers-5.4.0-100-generic CC [M] /home/user/kernel-modules/hello.o Building modules, stage 2. MODPOST 1 modules CC [M] /home/user/kernel-modules/hello.mod.o LD [M] /home/user/kernel-modules/hello.ko make[1]: Leaving directory /usr/src/linux-headers-5.4.0-100-generic编译成功后目录下会生成几个文件hello.ko最终可加载的内核模块最重要的文件hello.o编译产生的目标文件hello.mod.c模块依赖信息自动生成hello.mod.o模块依赖目标文件modules.order和Module.symvers模块顺序和符号信息四、多文件模块编译如何编译多个源代码文件当模块代码复杂时通常会分成多个文件。比如我们有main.c和helper.c两个文件该如何编译呢4.1 多文件模块的 Makefile只需将多个文件合并成一个模块名代码语言javascriptAI代码解释# 将main.o和helper.o合并成一个demo.ko模块 obj-m demo.o demo-objs : main.o helper.o # 指定组成demo.ko的目标文件 KERNELDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean关键是demo-objs : main.o helper.o它告诉构建系统demo.ko由main.o和helper.o链接而成。4.2 多文件模块的代码结构main.c主文件代码语言javascriptAI代码解释#include linux/init.h #include linux/module.h #include helper.h // 包含辅助函数头文件 static int __init demo_init(void) { printk(KERN_INFO 主模块初始化\n); helper_function(); // 调用辅助函数 return 0; } static void __exit demo_exit(void) { printk(KERN_INFO 主模块退出\n); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL);helper.c辅助文件代码语言javascriptAI代码解释#include linux/kernel.h #include helper.h void helper_function(void) { printk(KERN_INFO 这是辅助函数\n); }helper.h头文件代码语言javascriptAI代码解释#ifndef _HELPER_H_ #define _HELPER_H_ void helper_function(void); #endif执行make后会生成demo.ko模块包含了两个文件的功能。五、编译选项的高级配置自定义编译规则有时需要添加额外的编译选项比如警告级别、宏定义等这可以通过EXTRA_CFLAGS实现。5.1 添加编译警告选项让编译器更严格地检查代码代码语言javascriptAI代码解释obj-m hello.o EXTRA_CFLAGS -Wall -Wextra # 开启更多警告 KERNELDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean-Wall开启基本警告-Wextra开启额外警告帮助发现潜在问题。5.2 定义宏常量在编译时传递宏定义控制代码条件编译代码语言javascriptAI代码解释obj-m debug_module.o EXTRA_CFLAGS -DDEBUG1 # 定义DEBUG宏值为1 # 或者根据条件定义 ifdef DEBUG EXTRA_CFLAGS -DDEBUG1 endif在代码中可以这样使用代码语言javascriptAI代码解释#ifdef DEBUG printk(KERN_DEBUG 调试信息变量x的值为%d\n, x); #endif编译时如果定义了DEBUG1就会输出调试信息。5.3 包含额外头文件目录如果模块需要引用其他目录的头文件代码语言javascriptAI代码解释obj-m mymodule.o EXTRA_CFLAGS -I$(PWD)/include # 包含当前目录下的include子目录 KERNELDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M$(PWD) modules六、交叉编译为其他平台编译模块有时需要为嵌入式设备编译模块比如 ARM 架构这就需要交叉编译。1. 安装交叉编译工具链以 ARM 架构为例在 Ubuntu 上安装代码语言javascriptAI代码解释sudo apt-get install gcc-arm-linux-gnueabihf2. 交叉编译的 Makefile代码语言javascriptAI代码解释obj-m hello_arm.o # 目标架构的内核源码路径 KERNELDIR : /path/to/arm-linux-kernel # 交叉编译器前缀 CROSS_COMPILE : arm-linux-gnueabihf- # 当前目录 PWD : $(shell pwd) default: $(MAKE) -C $(KERNELDIR) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) ARCHarm CROSS_COMPILE$(CROSS_COMPILE) M$(PWD) cleanARCHarm指定目标架构为 ARMCROSS_COMPILEarm-linux-gnueabihf-指定交叉编译器前缀执行make后会生成能在 ARM 设备上运行的hello_arm.ko模块。七、常见编译错误及解决方法即使最简单的模块也可能遇到编译错误。这里列举几个最常见的问题及解决办法。7.1 没有规则可制作目标 错误代码语言javascriptAI代码解释make: *** 没有规则可制作目标“modules”。 停止。原因内核源码树路径错误或未安装内核开发包。解决检查KERNELDIR是否指向正确的内核源码目录确保已安装linux-headers-$(uname -r)包7.2 隐式声明函数 错误代码语言javascriptAI代码解释error: implicit declaration of function ‘printk’ [-Werrorimplicit-function-declaration]原因忘记包含必要的头文件。解决在代码开头添加#include linux/kernel.hprintk 的声明在这个头文件中。