Kbuild 与模块

本页目录

生成 .ko 后,加载仍可能失败;签名有效,也不保证 ABI 匹配。沿配置、构建信息、加载检查和资源退出分别核对,才能解释模块是否真正适配。

查阅目标阅读位置
功能选择与产物组成Kconfig、Kbuild
可复现构建与目标匹配外部模块工程、构建信息
运行与信任边界加载、导出符号、签名
配置、构建、加载是三个不同的检查边界 Kconfig 与 .config 选择功能,满足配置依赖 1 Kbuild 与 modpost 编译链接,检查模块符号等信息 2 内核模块加载器 检查目标兼容、符号、签名并调用初始化 3 编译出 .ko 不代表能在任意内核加载;加载成功也不等于驱动行为正确。

Kconfig:依赖不是文本菜单装饰

bool 表示 y/n,tristate 还允许 m,前提包括模块支持等配置。depends on 约束选项的可用性;select 反向强制另一个符号,可能绕过后者本来的依赖检查,因此不能把 select 当作通用依赖声明。Kconfig

.config 是选择结果,改变源码或内核版本后可用 olddefconfig 等更新,但默认值变化应 review。defconfig 与架构有关,不保证适合当前机器所有硬件。

依赖为 m 时,消费者为什么不能保持 y

对普通 tristate 依赖,可用 n < m < y 理解上界。若 CONSUMER 直接调用 PROVIDER 的功能,且依赖写为 depends on PROVIDER,PROVIDER=m 时,CONSUMER 最高只能到 m;不能让内建代码依赖尚待运行时加载的模块。

在独立实验目录保存以下 Kconfig。它同时包含一个故意写错的 select 关系,用于观察反向强制如何突破依赖。

mainmenu "Dependency example"
config MODULES
    bool "Enable modules"
    modules
    default y
config PROVIDER
    tristate "Provider"
config CONSUMER
    tristate "Consumer"
    depends on PROVIDER
config HARDWARE
    bool "Required hardware support"
config DRIVER
    tristate "Driver"
    depends on HARDWARE
config FORCE_DRIVER
    bool "Force driver"
    select DRIVER
seed 中的请求求值后的关键结果原因
PROVIDER=m,CONSUMER=yCONSUMER=m正向依赖把上界压到 m
PROVIDER=n,CONSUMER=yCONSUMER=n提供者关闭,消费者不可启用
PROVIDER=y,CONSUMER=yCONSUMER=y依赖允许内建
HARDWARE=n,FORCE_DRIVER=yDRIVER=y,并报告 unmet dependenciesselect 强制下界,没有替 DRIVER 满足依赖

例如将以下内容保存为 seed.config,使用已构建内核源码树里的 scripts/kconfig/conf 求值;输出目录和 KCONFIG_CONFIG 都应留在这个实验目录,避免改动正在使用的内核配置。

CONFIG_MODULES=y
CONFIG_PROVIDER=m
CONFIG_CONSUMER=y
KCONF=/path/to/kernel/scripts/kconfig/conf
KCONFIG_CONFIG=out.config "$KCONF" --defconfig=seed.config Kconfig

第四行不是推荐配置,而是在展示错误关系为什么危险。修复时应让配置表达真实依赖,或把适合由多个调用者选择的无依赖内部 helper 单独建模。把 select 换成 imply 也不能替代对内建/模块链接关系的设计。Kconfig 依赖与三值表达式

直接改变依赖与所请求的 tristate 值。下面把 n、m、y 视为有序上限,显示 requested 与依赖上界取较小值后的构建结果;它不把依赖视为自动选中的菜单。

正在呈现知识画面
请求 y,为什么最后只能是 m 或 n?

depends on 为符号建立上界。DRIVER 依赖 BUS && NET 时,不能以内建 y 引用只以模块 m 提供的依赖。select 有不同语义,不能替代普通依赖。

Kbuild:对象集合与复合模块

obj-$(CONFIG_MYDRV) += mydrv.o
mydrv-y := main.o helper.o

当配置为 y 时加入内建对象路径,为 m 时构建模块,未启用时不加入对应集合。mydrv-y 指定组成该复合对象的文件;不要同时把 mydrv.o 当作与同名 mydrv.c 无关系的两个不同目标。

配置为 y 并不自动保证最终能链接,配置为 m 也不保证依赖方能以内建方式调用它。Kconfig 关系与链接依赖要共同设计。

一个完整外部模块工程

目录包含下面三个文件。此示例仅记录加载与卸载,不接管设备。

hello.c:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>

static int greeting_id = 42;
module_param(greeting_id, int, 0444);
MODULE_PARM_DESC(greeting_id, "Identifier printed on initialization");

static int __init hello_init(void)
{
    pr_info("kb_hello: id=%d\n", greeting_id);
    return 0;
}
static void __exit hello_exit(void)
{
    pr_info("kb_hello: removed\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Knowledge base build example");

Kbuild:

obj-m := kb_hello.o
kb_hello-y := hello.o

Makefile(recipe 行以 Tab 开头):

KDIR ?= /lib/modules/$(shell uname -r)/build
MODULE_DIR := $(CURDIR)

.PHONY: all clean
all:
	$(MAKE) -C $(KDIR) M=$(MODULE_DIR) modules
clean:
	$(MAKE) -C $(KDIR) M=$(MODULE_DIR) clean

在该目录运行 make 得到 kb_hello.ko,也可通过 make KDIR=/path/to/prepared/kernel 指定目标构建目录。使用 $(MAKE) 保留递归构建参数;Kbuild 单独列对象,避免普通包装目标与内核规则混淆。外部模块

头文件还不够:必须匹配目标构建信息

外部模块需要目标内核的配置、生成头文件和必要构建产物。modules_prepare 不生成启用 MODVERSIONS 所需的完整 Module.symvers;需要对应构建提供它,不能以为准备头文件就包含全部符号版本信息。

modpost 处理导出符号和模块元信息,未解析符号、命名空间和版本问题应在此及加载阶段分别排查。主线 Linux 不承诺跨内核版本稳定的内部模块 ABI。稳定 API 说明

两个外部模块:符号已知,不代表已链接进消费者

以下第二个工程只演示模块之间的依赖,放到与 hello 工程不同的目录。provider 导出一个函数,consumer 在初始化时调用它。先统一构建两个模块,便于 modpost 同时看到它们的导出信息。

kb_api.h:

#ifndef KB_API_H
#define KB_API_H
unsigned int kb_step(unsigned int value);
#endif

kb_provider.c:

#include <linux/module.h>
#include "kb_api.h"

unsigned int kb_step(unsigned int value)
{
    return value + 1U;
}
EXPORT_SYMBOL_GPL(kb_step);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Knowledge base symbol provider");

kb_consumer.c:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include "kb_api.h"

static int __init kb_consumer_init(void)
{
    return kb_step(41U) == 42U ? 0 : -EINVAL;
}
static void __exit kb_consumer_exit(void) {}
module_init(kb_consumer_init);
module_exit(kb_consumer_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Knowledge base symbol consumer");

Kbuild:

obj-m := kb_provider.o kb_consumer.o

可以复用前面 Makefile 的包装规则,指定准备好的 KDIR 构建。完成后,不加载模块也能检查:

modinfo -F depends ./kb_consumer.ko
nm -u ./kb_consumer.ko

depends 应包含 kb_provider,未定义符号列表中仍能看到 kb_step。这不矛盾:consumer 的 .ko 没有把 provider 的实现静态复制进去,符号留待内核加载时解析;modpost 知道它来自哪个模块,并生成依赖元信息。

声明、导出记录与运行时实现提供不同证据 C 编译 头文件声明 kb_step modpost 导出表说明符号提供者 运行时加载 解析已加载 provider 符号 consumer.ko 中的未定义引用可以保留,依赖信息另行记录 KBUILD_EXTRA_SYMBOLS 提供构建信息;不会把 provider 的实现嵌入 consumer。

如果把两个模块拆成独立构建目录,仅给 consumer 一份声明头文件,C 编译仍可能成功,但 modpost 缺少 kb_step 的导出记录会拒绝继续。先构建 provider,再向 consumer 构建传入 KBUILD_EXTRA_SYMBOLS=/absolute/path/to/provider/Module.symvers,可以补上构建阶段所需的符号信息;两个工程必须针对兼容的目标内核构建。

这个参数没有安装或加载 provider,也不能把头文件不一致、过期 symvers 或 ABI 不匹配变成正确依赖。运行时仍需要 provider 存在并先被解析;模块索引安装正确时,modprobe 才能按依赖查找和加载。若只使用 insmod 指定 consumer 文件,不会因为构建时给过 symvers 就自动找到 provider。外部模块的符号与 Module.symvers

加载工具与生命周期

工具依赖处理
insmod按指定文件加载,不自动寻找全部依赖
modprobe依据模块索引与配置处理依赖、别名
modprobe -r可尝试移除不再使用的依赖,不保证全都卸载

选工具之前先区分“加载这个文件”与“按已安装模块关系加载”。

模块加载包含重定位、符号解析、检查和 init 调用;init 失败必须清理已经取得的资源。退出路径要停止新请求、取消工作、等待引用和回调结束,不能仅写一句 pr_info 就当作真实驱动完整卸载逻辑。

可以先用 modinfo ./kb_hello.ko 检查名称、参数和 vermagic。安装与加载会影响系统,通常应在匹配内核的测试 VM 中进行。make install 的启动项和 initramfs 行为依赖架构及发行版钩子,不是 Kbuild 保证一次完成的所有步骤。

导出符号不是永久稳定的公共 API

EXPORT_SYMBOL 把符号暴露给适用模块,EXPORT_SYMBOL_GPL 对模块许可声明施加限制;符号命名空间还可能要求相应导入。导出意味着可以解析,不代表任意上下文调用都安全,也不承诺未来版本不变。

模块签名:验证、自动签名与强制策略分开

配置或策略负责哪一件事
CONFIG_MODULE_SIG提供签名验证机制
CONFIG_MODULE_SIG_ALL涉及构建安装中的自动签名
CONFIG_MODULE_SIG_FORCE 或强制策略决定是否必须有可接受签名

一个 SIG=y 不等于“自动签好所有模块并强制拒绝全部未匹配模块”。签名文档

运行时策略、可信密钥和 Secure Boot 集成受内核与发行版配置影响。签名验证成功也不保证模块与当前内核 ABI 匹配或行为正确。签名附加在模块内容之后,签名后再修改或 strip 可能使其失效;应把签名安排在正确的产物处理阶段。

进一步测试策略见 调试与测试。