Linux 外部内核模块:Kbuild、内核构建目录与符号版本
Linux 外部内核模块:Kbuild、内核构建目录与符号版本
类型:Linux 内核官方文档 Building External Modules 的中文解读。保留原文构建语义和版本条件;下面的
hello_lab模块与 Python 编排是补充学习示例,不是原文完整翻译。没有在博客服务器加载模块或编译内核。
为什么不能把驱动当普通 C 程序编译
内核外部模块是放在内核源码树之外开发的模块,通常生成 .ko 文件。它仍依赖目标内核的配置、头文件、构建参数与导出符号。官方要求通过 Kbuild 构建,以取得内核所需的编译参数,并跟随构建基础设施的变化。[2]
因此,“机器上有 gcc 和 /usr/include/linux”不等于具备模块构建环境。用户空间头文件与目标内核的模块构建资料不能互相替代。
一个最小的验收链条是:确认目标内核 → 找到对应构建目录 → 编译模块 → 检查产物 → 在隔离环境加载验证。编译成功与能正确驱动硬件是不同的检查点。
构建目录必须对应目标内核
原文针对正在运行的内核使用 /lib/modules/<kernel-release>/build。对于发行版内核,需要安装与目标内核对应的开发或头文件包;对于自行构建的内核,需要可用的配置与相关构建产物,并且内核启用了模块支持。[2]
下面的 Python 检查只针对本机运行内核。交叉编译或给另一台设备构建时,不能拿本机 platform.release() 代替目标版本。
from pathlib import Path
import platform
release = platform.release()
build_dir = Path("/lib/modules") / release / "build"
print("运行内核:", release)
print("构建目录:", build_dir)
if not build_dir.exists():
raise SystemExit("缺少对应内核的构建目录,请先准备匹配的开发包")
print("实际路径:", build_dir.resolve())
if not (build_dir / "Makefile").is_file():
raise SystemExit("构建目录中没有 Makefile")
不要为了解决找不到目录的问题,把 build 链接到任意新版源码树。路径存在只能证明文件存在,不能证明 ABI、配置或架构相匹配。
用一个小模块观察构建过程
在独立目录保存 hello_lab.c。它只用于观察模块初始化和退出,不访问实际硬件。内核代码即使很短也应在可恢复的虚拟机或专用实验设备上测试。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_lab_init(void)
{
pr_info("hello_lab: module initialized\n");
return 0;
}
static void __exit hello_lab_exit(void)
{
pr_info("hello_lab: module exited\n");
}
module_init(hello_lab_init);
module_exit(hello_lab_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimal out-of-tree module for a learning lab");
同目录保存 Kbuild,内容只有一行:
obj-m := hello_lab.o
obj-m 告诉 Kbuild 生成可加载模块。Kbuild 将对应 C 文件编译为对象文件,再完成模块链接,得到 hello_lab.ko。文件名与目标名必须一致。[2]
下面用 Python 表达原文的 make -C <kernel-dir> M=<module-dir> 调用关系,不需要自己拼接一套 gcc 参数。
from pathlib import Path
import platform
import subprocess
module_dir = (Path.home() / "kernel-module-lab").resolve()
kernel_dir = Path("/lib/modules") / platform.release() / "build"
for filename in ("Kbuild", "hello_lab.c"):
if not (module_dir / filename).is_file():
raise FileNotFoundError(module_dir / filename)
subprocess.run(
["make", "-C", str(kernel_dir), f"M={module_dir}", "modules"],
check=True,
)
artifact = module_dir / "hello_lab.ko"
if not artifact.is_file():
raise RuntimeError("构建结束但没有预期模块产物")
subprocess.run(["modinfo", str(artifact)], check=True)
-C 指向内核构建目录;M 指向外部模块自身的绝对路径。不要交换这两个目录。modinfo 能帮助检查模块元信息,但不代表模块已加载或工作正常。
多个源文件怎样组成一个模块
原文的多文件例子使用 8123.ko,由 8123_if.c 与 8123_pci.c 组成。这里保留文件命名,避免在解释规则时改变原例的对应关系。[2]
obj-m := 8123.o
8123-y := 8123_if.o 8123_pci.o
第一行定义最终模块,第二行列出组成该模块的对象。8123-y 不是再生成一个独立模块。如果多个模块彼此使用导出符号,原文推荐通过顶层 Kbuild 一起构建;不方便统一时,再考虑 KBUILD_EXTRA_SYMBOLS。[2]
自定义头文件目录也应该交给 Kbuild:
ccflags-y := -I$(src)/include
$(src) 指向当前 Kbuild 文件所在目录。构建过程中工作目录可能位于内核树,所以不能假设相对 include/ 总能指向模块自己的目录。
modules_prepare 并不等于完整准备所有版本信息
官方明确指出:modules_prepare 可准备外部模块构建所需的一部分信息,但即使设置了 CONFIG_MODVERSIONS,它也不会生成 Module.symvers。要正确使用模块版本校验,需要相应完整内核构建产生的符号信息。[2]
Module.symvers 记录导出符号;启用 CONFIG_MODVERSIONS 时,还涉及符号原型的 CRC。加载时内核会比较相应信息,校验不一致可能拒绝模块。[2]
因此,不应把未知符号错误一律归因于“少了头文件”,也不要把另一个不匹配内核的 Module.symvers 随意复制进来。应分别检查符号是否导出、依赖模块是否参与构建以及目标构建资料是否匹配。
安装与加载是后续独立操作
modules_install 是安装构建产物,不是执行模块功能。当前所读取官方文档给出的外部模块默认目录是目标版本下的 updates/,并允许用 INSTALL_MOD_PATH 和 INSTALL_MOD_DIR 调整;发行版或厂商构建可能另有约定。[2]
学习初期可以只构建与查看元信息,不执行系统级安装。需要加载时,先确认有控制台、恢复手段和实验环境,记录内核日志;不要在承担网站或数据库的生产主机上测试未知模块。
本篇刻意没有提供自动加载脚本,以免将“构建练习”误操作为“向正在运行的业务内核注入代码”。
版本边界与嵌入式延伸
原文指出,Linux 6.13 起支持另一种 -f 调用方式及直接包含内核 Makefile 的形式;较旧目标不应照搬。本文示例选用传统 -C 方式,避免把新功能当成所有内核都支持。[2]
给开发板编译时还要明确目标架构、交叉工具链、厂商内核树、配置和内核输出目录。这里没有给出某块板子的通用参数:ARM64 架构相同并不意味着两套厂商内核可以互换模块。
排错顺序
- 找不到
build:核对目标 release 与对应开发包,不先改源码。 - 编译参数或内核接口错误:核对目标源码和驱动所支持的内核版本。
- MODPOST 未定义符号:核对符号导出、依赖模块与
Module.symvers。 - 产物加载被拒绝:读取完整内核错误,继续检查版本、架构与签名策略,不能只看
.ko文件已生成。 - 能加载但硬件没工作:转入总线匹配、probe、资源与硬件协议排查。这已经超出 Kbuild 文档的主题。
原文解决的是“如何正确构建外部模块”,不是“如何保证任意驱动支持任意硬件”。保留这个边界,才能避免把构建成功当作驱动完成。
来源
本文为中文技术解读,未替代 Linux 内核官方文档。