生成 .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=y CONSUMER=m 正向依赖把上界压到 m
PROVIDER=n,CONSUMER=y CONSUMER=n 提供者关闭,消费者不可启用
PROVIDER=y,CONSUMER=y CONSUMER=y 依赖允许内建
HARDWARE=n,FORCE_DRIVER=y DRIVER=y,并报告 unmet dependencies select 强制下界,没有替 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 有不同语义,不能替代普通依赖。
{"id":"kconfig-tristate-bounds","title":"请求 y,为什么最后只能是 m 或 n?","summary":"depends on 为符号建立上界。DRIVER 依赖 BUS && NET 时,不能以内建 y 引用只以模块 m 提供的依赖。select 有不同语义,不能替代普通依赖。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"modules\" checked><span data-i18n=\"modules\"></span></label><label for=\"bus\" class=\"caption\" data-i18n=\"bus\"></label><select id=\"bus\"><option value=\"0\">n</option><option value=\"1\" selected>m</option><option value=\"2\">y</option></select><label for=\"net\" class=\"caption\" data-i18n=\"net\"></label><select id=\"net\"><option value=\"0\">n</option><option value=\"1\">m</option><option value=\"2\" selected>y</option></select><label for=\"requested\" class=\"caption\" data-i18n=\"requested\"></label><select id=\"requested\"><option value=\"0\">n</option><option value=\"1\">m</option><option value=\"2\" selected>y</option></select><div class=\"tristates\"><div><span data-i18n=\"bus\"></span><div class=\"level\"><i id=\"bar-bus\"></i></div><strong id=\"bus-value\"></strong></div><div><span data-i18n=\"net\"></span><div class=\"level\"><i id=\"bar-net\"></i></div><strong id=\"net-value\"></strong></div><div><span data-i18n=\"requested\"></span><div class=\"level\"><i id=\"bar-requested\"></i></div><strong id=\"requested-value\"></strong></div><div><span data-i18n=\"bound\"></span><div class=\"level\"><i id=\"bar-bound\"></i></div><strong id=\"bound-value\"></strong></div><div><span data-i18n=\"effective\"></span><div class=\"level\"><i id=\"bar-effective\"></i></div><strong id=\"effective-value\"></strong></div></div><div class=\"formula\" id=\"formula\"></div><div class=\"card\"><header data-i18n=\"artifact\"></header><div id=\"artifact\" class=\"readout\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.caption{display:block;margin:12px 0 5px}.tristates{margin:20px 0}.tristates>div{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr) 20px;gap:8px;align-items:center;min-height:4.5em;font-size:12px}.level{height:18px;background:var(--surface);border-inline:1px solid var(--rule)}.level i{display:block;height:100%;background:var(--accent);transition:width .2s}#bar-bound{background:var(--second)}.readout{min-height:3.2em}.formula{min-height:4.8em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function draw(){const b=+$('#bus').value,n=+$('#net').value,r=+$('#requested').value,bound=Math.min(b,n),effective=Math.min(r,bound);for(const[id,v]of [['bus',b],['net',n],['requested',r],['bound',bound],['effective',effective]]){set(id+'-value',['n','m','y'][v]);$('#bar-'+id).style.width=v*50+'%';}set('formula','min('+[r,b,n].map(v=>['n','m','y'][v]).join(', ')+') = '+['n','m','y'][effective]);set('artifact',t(['none','module','builtin'][effective]));set('explanation',t('note'));}$('#modules').onchange=()=>{for(const id of ['bus','net','requested']){const e=$('#'+id);e.querySelector('[value=\"1\"]').disabled=!$('#modules').checked;if(!$('#modules').checked&&e.value==='1')e.value='0';}draw();};for(const id of ['bus','net','requested'])$('#'+id).onchange=draw;draw();","audio":false,"strings":{"artifact":"对应构建产物","bound":"依赖上界 BUS && NET","builtin":"obj-y → 内建链接","bus":"BUS","effective":"DRIVER 实际值","heading":"请求 y,为什么最后只能是 m 或 n?","intro":"分别选择 BUS、NET 和 DRIVER 的期望值。依赖表达式使用 n<m<y 的三值逻辑,观察 DRIVER 的上界和构建产物。","module":"obj-m → driver.ko","modules":"启用 MODULES","net":"NET","none":"不进入对象集合","note":"把 BUS 设为 m、NET 设为 y、期望 DRIVER=y。依赖上界只有 m,因此 DRIVER 不会按这个请求变成内建。","requested":"DRIVER 期望值","reset":"重置","scope":"三个无 default/select/imply 的可见 tristate 符号。仅计算 effective=min(requested,BUS,NET),不执行完整 Kconfig。关闭 MODULES 时本界面将 m 选择显式重置为 n 并禁用 m;这项 UI 选择不是所有真实配置迁移的通用规则。","scopeLabel":"模型范围与约定"}}
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 可能使其失效;应把签名安排在正确的产物处理阶段。
进一步测试策略见 调试与测试 。