1. 报错现场:先把这两行日志读明白
上周三晚上十一点多,我在 Orange Pi CM5 上折腾一个自研的 IO 扩展板驱动。宿主机是 x86 的 Ubuntu,跑的是板厂给的 arm 交叉编译工具链,交叉编译整个过程顺得让人放松警惕,mydrv.ko生成得干干净净,modinfo看了一眼,字段都齐全。scp 丢到板子上,insmod mydrv.ko,终端冷冰冰甩回来一句:
insmod: ERROR: could not insert module mydrv.ko: Invalid module format再翻dmesg | tail -20,真正的元凶藏在里面:
[ 1234.567890] mydrv: disagrees about version of symbol module_layout [ 1234.568012] mydrv: Unknown symbol module_layout (err -22)这条disagrees about version of symbol module_layout是嵌入式圈子里被问烂了的老朋友。它看起来像"符号版本对不上",很多人第一反应是去改代码、去换 gcc 版本,结果折腾一整天方向完全是错的。它真正想表达的意思只有一句话:你交叉编译这个内核模块时使用的那套内核环境,跟板子上正在跑的内核不是同一套。
这篇东西写给已经能独立写出并编译一个简单字符设备驱动、但一碰到insmod报invalid module format就懵的朋友。不管你是从 x86 转到 ARM 板子,还是在做 Qt5 交叉编译顺带编译配套驱动,只要牵扯到"宿主机编译、目标板加载"这件事,机制都是一样的,弄懂一次就能吃很久。
1.1 insmod 把文件交给内核之后,到底过了哪几道关
很多人对insmod有个误解,以为它是个"安装程序",实际上它朴素得不行:把.ko整个文件读进内存,调用init_module()(或者finit_module)系统调用,把这坨二进制直接丢给内核,剩下的全看内核脸色。真正干活的是内核里的load_module()函数,它会给这个模块过五道关卡,任何一道不过,系统调用返回-ENOEXEC,用户态的 insmod 就统一翻译成人畜无害的 "Invalid module format"。
| 关卡 | 检查内容 | 典型报错 |
|---|---|---|
| 一 | ELF 头部、e_machine架构标识 | ELF header/invalid ELF header |
| 二 | vermagic 字符串比对 | version magic ... should be ... |
| 三 | 符号 CRC 版本比对 | disagrees about version of symbol X |
| 四 | 模块签名校验 | key was rejected by service |
| 五 | 符号解析与重定位 | Unknown symbol X (err -22) |
这里有个容易混乱的点:第三关和第五关的报错经常同时出现,就像我上面 dmesg 里那样,先抱怨 CRC 对不上,紧接着说符号找不到。不是两个问题,是一个问题的两个症状——CRC 比对失败的符号会被标记为"不可用",随后符号解析阶段自然就报 Unknown symbol。所以修的时候只要解决 CRC 那条,下面一条会自己消失。
1.2 module_layout 为什么成了第一个撞墙的符号
module_layout在内核里是个非常特殊的"哨兵符号"。它不是一个真有调用者的函数,它的存在意义纯粹是当ABI 指纹。内核开发者在源码里把它摆在struct module相关声明附近的视野范围内,交给 genksyms 工具去解析类型信息并算出一个 CRC 值挂在这个名字下面。凡是会影响struct module内存布局的配置项——SMP、PREEMPT、MODULE_UNLOAD、MODULE_SIG、各类 TRACEPOINT 开关等等——只要有一个不一样,算出来的 CRC 就可能变。
为什么偏偏是它第一个报错?因为只要内核开启了CONFIG_MODVERSIONS,每一个模块都必然依赖module_layout这个符号,哪怕你的驱动代码里一个字都没提它。它是所有模块的公共依赖,排序上又靠前,所以是第一个被比对的,也是第一个倒下的。
但这里必须补一句诚实的话:vermagic 字符串本身不包含编译器版本。所以网上盛传的"gcc 版本必须和板厂一模一样否则就报这个错"其实是个误导性经验。工具链版本不匹配确实会带来一堆麻烦,但不会通过 vermagic 这条路径表现。真正决定 vermagic 的是内核源码树里的.config和生成出来的include/generated/utsrelease.h。
1.3 顺手区分几个长得很像的报错
同一个insmod报错桶里,其实装着好几种完全不同的问题。分不清它们,就会陷入"换个方法试一下"的随机摸索。这里给个最简判据:
version magic '5.10.160 SMP mod_unload aarch64' should be '5.10.160-rockchip SMP preempt mod_unload modversions aarch64':纯 vermagic 问题,去看.config里的CONFIG_LOCALVERSION和CONFIG_MODVERSIONS。disagrees about version of symbol module_layout:CRC 问题,你的Module.symvers大概率缺失或者来自另一份内核。no symbol version for module_layout:模块里有版本信息但内核侧没有,大概率是内核编译时CONFIG_MODVERSIONS=n。key was rejected by service:这是模块签名体系拦下来的,跟上面三个完全不是一回事,排查方向要换到签名和密钥那边去。
下面几节,我把这几个问题的根挖到底,再给能直接抄的修法。
2. 机制拆解:vermagic 和符号 CRC 是怎么算出来的
2.1 vermagic:模块贴在脑门上的配置指纹
在宿主机的源码目录里敲一句:
modinfo mydrv.ko | grep vermagic你会看到类似这样一串:
vermagic: 5.10.160-rockchip SMP preempt mod_unload modversions aarch64这串东西是由内核源码里的VERMAGIC_STRING宏拼出来的,拆开看:
| 片段 | 来源 | 含义 |
|---|---|---|
5.10.160-rockchip | UTS_RELEASE | 内核版本 +CONFIG_LOCALVERSION后缀 |
SMP | CONFIG_SMP=y | 对称多处理 |
preempt | CONFIG_PREEMPT=y | 抢占式内核 |
mod_unload | CONFIG_MODULE_UNLOAD=y | 允许卸载模块 |
modversions | CONFIG_MODVERSIONS=y | 开启符号 CRC 校验 |
aarch64 | MODULE_ARCH_VERMAGIC | 架构标识,arm64 上基本就是这个 |
这里有两个实战要点。
第一,5.10.160-rockchip里的-rockchip后缀来自.config的CONFIG_LOCALVERSION="-rockchip",或者来自内核源码里的localversion*文件,或者来自CONFIG_LOCALVERSION_AUTO=y时 git 仓库自动追加的描述。板厂发布的内核全都带这个后缀,你自己从 kernel.org 拉的干净源码编译出来是裸的5.10.160,vermagic 直接对不上。
第二,vermagic 字符串是在编译外部模块时由内核源码树现场生成的,不是从.ko里读出来的。也就是说,你的模块 vermagic 长什么样,完全取决于你make -C指向的那棵内核树。这就解释了为什么有人换了个-C路径就突然好了。
2.2 CONFIG_MODVERSIONS 与 genksyms:符号级 CRC 的诞生
vermagic只能保证"大版本、大配置"对得上,它挡不住这种情况:内核版本完全一样,但板厂在源码里给某个结构体加了个字段。这种情况下 vermagic 一个字符都不差,模块插进去却会读到错位的字段,轻则数据乱码,重则内核直接崩。
CONFIG_MODVERSIONS就是为了挡住这个。内核编译时,genksyms工具会解析每个EXPORT_SYMBOL导出的符号声明,把它的类型签名(函数参数、返回值、相关结构体布局)算成一个 32 位 CRC32 值。这个值跟符号名一起被记录下来。
到了外部模块编译时,modpost工具会读取内核树里那份记录,把模块用到的每个外部符号及其 CRC写进.ko的一个名为__versions的段里。这个段的结构在内核头文件里定义得非常直白:
struct modversion_info { unsigned long crc; /* 64 位平台上占 8 字节,高 4 字节为 0 */ char name[MODULE_NAME_LEN]; /* 64 位平台上为 56 字节 */ };所以在 64 位平台上,每一条版本记录正好 64 字节,name字段是左对齐的字符串,后面补零。记住这个 64 字节,后面手工救砖的时候要用。
2.3 Module.symvers:连接内核树和外部模块的那份清单
内核编译完成后,源码树根目录会多出一个文件:
$ head -3 Module.symvers 0x1234abcd module_layout vmlinux EXPORT_SYMBOL 0x8f3c21de printk vmlinux EXPORT_SYMBOL 0x00000000 some_unexported_thing vmlinux EXPORT_SYMBOL_GPL格式是制表符分隔的四列:CRC 值、符号名、所属模块、导出类型。这就是那份关键清单。你的外部模块要编得能被目标板接受,就必须拿着一份和板子内核完全匹配的Module.symvers去编译。
如果编译外部模块时modpost找不到这份清单,它会打印一条不那么起眼、但极其致命的警告:
WARNING: Symbol version dump ./Module.symvers is missing; modules will have no dependencies and modversions.很多人编译时扫一眼就过去了,觉得 WARNING 不是 ERROR 就没事。结果.ko里的__versions段是空的,或者每条 CRC 都是 0。加载时内核一看:我这边module_layout的 CRC 是0x1234abcd,你那边写的是 0,对不上,于是就有了那句经典报错。
提示:
make modules_prepare这个目标不会生成Module.symvers。这不是 bug,是设计如此——它跳过 vmlinux 和导出符号的编译,自然也就算不出符号签名。这一点是绝大多数人第一次部署交叉编译环境时掉进去的坑。
2.4 加载时的两次比对,报错文本各不相同
内核侧的 CRC 值存在 vmlinux 的__kcrctab段里,模块侧的版本信息在__versions段里,加载时由check_version()逐一比对。比对的逻辑可以简化成三步:
- 模块里根本没有
__versions段(编译时CONFIG_MODVERSIONS就是关的)——内核直接放行,不做版本检查。 - 模块里有
__versions段,但里面找不到这个符号的条目——打印no symbol version for X,走失败分支放行与否要看具体内核版本的小差别。 - 找到了条目,但 CRC 值不相等——打印
disagrees about version of symbol X,直接判死。
这也解释了为什么"内核关掉 MODVERSIONS"能解决一部分人的问题:走了第 1 条路径,检查直接被绕过。代价是所有版本兼容性保护都没了,后续一旦换了不兼容的内核,模块可能以更隐蔽的方式炸掉。急用可以,长期不建议。
3. 交叉编译内核模块,环境该怎么搭
3.1 内核源码树的三条获取路径与取舍
这份环境的核心是"一棵跟板子内核同源的内核树"。获取路径大致三条,差别巨大:
| 来源 | 含Module.symvers | 含完整.config | 推荐度 |
|---|---|---|---|
| 板厂 SDK 里的完整内核源码 | 完整编译一次后就有 | 有 | 强烈推荐 |
发行版linux-headers-$(uname -r)包 | 有 | 有 | x86 同架构下很方便 |
| 从 kernel.org 拉的对应版本原版源码 | 需要自己编,且配置未必一致 | 需要自己配 | 只能作为最后手段 |
只带include/和scripts/的裁剪树 | 没有 | 没有 | 别用这个 |
第二条路径在 x86 服务器上很常见,apt install linux-headers-$(uname -r)一条命令搞定,但它是给同架构用的,交叉编译场景下基本用不上。嵌入式板子直接走第一条:找板厂要 SDK,里面通常有一份完整内核源码,还可能附带了已经生成好的Module.symvers。
如果拿不到 SDK,退一步的做法是找到和板厂内核完全一致的上游 tag,然后用板子导出的.config覆盖源码里的.config,自己完整编译一遍。这个方案能不能成,取决于板厂是否在源码里打了改结构体布局的补丁——打了,CRC 就永远对不上,只能去要原厂源码。
3.2 从板子上取出运行内核的真实身份
坐到板子串口前面,敲这几条:
uname -r # 5.10.160-rockchip cat /proc/version # Linux version 5.10.160-rockchip (builder@ci) (aarch64-linux-gnu-gcc ...) ... zcat /proc/config.gz > /tmp/board.config 2>/dev/null || \ cp /boot/config-$(uname -r) /tmp/board.config ls -l /lib/modules/$(uname -r)/ # 看有没有 build / source 软链接,指向哪里zcat /proc/config.gz这条命令能不能用,取决于板子内核有没有开CONFIG_IKCONFIG_PROC。这个选项本身不影响模块 ABI,但在调试阶段价值极高,如果你在给板厂提需求,一定要让他们把CONFIG_IKCONFIG_PROC=y打开并顺手放一份Module.symvers进 SDK。这一个改动能省掉无数开发者的通宵。
拿到board.config之后,回到宿主机,把它和宿主机内核树里的.config做一次对比。重点看这几个会改变struct module布局的项:
diff <(grep -E 'CONFIG_(SMP|PREEMPT|MODULE_UNLOAD|MODVERSIONS|MODULE_SIG|KALLSYMS|TRACEPOINTS?)[= ]' /tmp/board.config | sort) \ <(grep -E 'CONFIG_(SMP|PREEMPT|MODULE_UNLOAD|MODVERSIONS|MODULE_SIG|KALLSYMS|TRACEPOINTS?)[= ]' .config | sort)有差异项,就说明 CRC 一定对不上。
3.3 内核树准备:modules_prepare 能做什么、不能做什么
假设内核源码在/opt/kernel/linux-5.10.160,工具链前缀是aarch64-linux-gnu-。
标准的准备动作:
cd /opt/kernel/linux-5.10.160 cp /tmp/board.config .config make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules_prepare -j$(nproc)olddefconfig的作用是把.config里因为源码差异而缺失的新选项,用默认值补齐,避免后面编译时被反复询问。这一步很多人省略,结果在modules_prepare阶段被问一堆问题,随手回车的结果就是配置和板子出现了差异。
modules_prepare干的事情是:生成include/generated/autoconf.h、utsrelease.h、compile.h等一堆头文件,让外部模块编译时能拿到内核的配置和版本信息。它不编译 vmlinux,所以__kcrctab符号表不存在,Module.symvers也就出不来。
想要Module.symvers,只有一条路——完整编一次内核:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image modules -j$(nproc)这一遍在普通开发机上大概二三十分钟,慢是慢,但只需要做一次。编完之后,源码树根目录就有那份珍贵的Module.symvers了。编完千万别手贱去make clean或者make mrproper,那会把它删掉,然后你就得再等半小时。我在这个坑里蹲过不止一次。
3.4 外部模块的 Makefile 与编译命令逐参数说明
驱动目录里那份 Makefile 保持极简:
obj-m += mydrv.o mydrv-objs := mydrv_main.o mydrv_ioctl.o如果只用一个源文件,obj-m += mydrv.o一行就够了。
编译命令:
make -C /opt/kernel/linux-5.10.160 \ M=$(pwd) \ ARCH=arm64 \ CROSS_COMPILE=aarch64-linux-gnu- \ LOCALVERSION=-rockchip \ modules逐参数拆一下,每一个都有它存在的理由:
-C切到内核源码目录,借用它的顶层 Makefile。这是整套机制的入口,-C后面跟了错的树,后面全白搭。M=$(pwd)告诉 Kbuild 真正的模块源码在哪里。Kbuild 会为这个目录单独跑一遍modpost。ARCH=arm64决定编译哪些架构相关的代码,也影响MODULE_ARCH_VERMAGIC,写错就变成给错误架构编模块。CROSS_COMPILE=aarch64-linux-gnu-指定工具链前缀,注意结尾那个横杠不能少。LOCALVERSION=-rockchip强制覆盖本地版本后缀。这个参数只在.config里CONFIG_LOCALVERSION_AUTO为n时才会生效,CONFIG_LOCALVERSION_AUTO=y时 Kbuild 会走另一条分支。
如果一次编译多个互相依赖的外部模块,还要加上:
KBUILD_EXTRA_SYMBOLS=/path/to/other_module/Module.symvers这个变量用于把另一个外部模块导出符号的 CRC 也纳入本次编译的解析范围,不然模块之间会互相报未定义符号。
编译成功之后,用宿主机上的modinfo再复查一遍关键字段:
modinfo mydrv.ko | grep -E 'vermagic|name|depends|license'没有modinfo命令的话,退而求其次:
strings mydrv.ko | grep '^vermagic=' readelf -p .modinfo mydrv.ko | grep vermagic三选一,效果一样。只要这一步的 vermagic 和板子uname -r对不上,就别往下走了,直接回头改环境。
4. 四个真实场景的排查与修复
4.1 场景一:Module.symvers 缺失(最常见)
这是我踩得最多的坑,也是社区里占比最高的原因。
现象:编译时出现过那条Symbol version dump ... is missing的警告,被忽略了。insmod时报disagrees about version of symbol module_layout。
验证:在宿主机上看看模块里到底有没有版本信息。
readelf -S mydrv.ko | grep versions # 空输出 → 根本没有 __versions 段 readelf -x __versions mydrv.ko # 有段但全是 0,或者没有 module_layout 这条再看内核树里:
grep module_layout /opt/kernel/linux-5.10.160/Module.symvers # 没输出 → 内核树没算出来,或者你指向的树不对修复:完整编一次内核,把Module.symvers生成出来,然后重新编译外部模块。这一步没有捷径。
如果你实在不想等那半小时编译,还有一个"抠门版"的救法——从板子上把 CRC 值抠出来,手写进Module.symvers。板子内核如果开了 MODVERSIONS,/proc/kallsyms里通常能看到这些符号:
# 在板子上执行 cat /proc/kallsyms | grep __crc_module_layout # ffffffc010123456 R __crc_module_layout把最后一列地址对应的实际 CRC 值取出来(有些内核导出的是符号地址不是值,需要再从 vmlinux 里读,视内核配置而定)。拿到值之后,在宿主机的Module.symvers里手工补一行:
echo -e "0x1234abcd\tmodule_layout\tvmlinux\tEXPORT_SYMBOL" >> Module.symvers然后重新make外部模块,modpost会读这一行并把正确的 CRC 写进__versions。这个方法只适合只差一两个符号的临时救急,符号一多就不现实了,老老实实编译内核才是正道。
4.2 场景二:.config 差异改变了 struct module 布局
现象:vermagic两边一模一样,Module.symvers也是从这棵树上生成的,但module_layout的 CRC 就是对不上。
根因:CRC 是对类型签名的哈希,只要定义符号时依赖到的某个结构体布局变了,哈希值就变。struct module里有一大堆#ifdef CONFIG_XXX包起来的字段,所以 SMP、PREEMPT、MODULE_SIG、MODULE_UNLOAD 这几个开关任意一个不一致,都会导致它的 CRC 变化。
注意一个反直觉的点:这些配置开关的差异有时候会同时反映在 vermagic 上(因为 vermagic 里带了 SMP/preempt 这些关键字),有时候又不会。比如MODULE_SIG的存在与否不体现在 vermagic 里,但会实打实改变struct module的布局。所以会出现"vermagic 一致但 CRC 不一致"这种让人抓狂的情况。
验证:直接对比配置。
grep -E 'CONFIG_(SMP|PREEMPT|MODULE_UNLOAD|MODVERSIONS|MODULE_SIG|MODULE_SIG_FORCE|KALLSYMS|KALLSYMS_ALL)=' \ /opt/kernel/linux-5.10.160/.config和板子的/tmp/board.config逐项比对。有差异就改过来,重跑olddefconfig和modules_prepare(改过.config之后一定要重跑,否则include/config/kernel.release还是旧值)。
修复:把板子的.config覆盖过来,重新完整编译一次内核,再用新的Module.symvers编外部模块。
注意:改完
.config之后千万不要只跑make M=... modules。内核的一些生成文件是会缓存的,必须重新走一遍olddefconfig+modules_prepare,让include/generated/下的头文件同步刷新。
4.3 场景三:localversion 后缀导致的 vermagic 不匹配
现象:报的是version magic '5.10.160 SMP preempt mod_unload modversions aarch64' should be '5.10.160-rockchip ...',也就是 CRC 那条根本没轮到,vermagic 就先把人拦下了。
根因:板子的内核版本带-rockchip后缀,你本地编出来的是裸的5.10.160。后缀的来源有三个可能:
.config里的CONFIG_LOCALVERSION="-rockchip";- 内核源码根目录里的
localversion-rockchip这类文件; CONFIG_LOCALVERSION_AUTO=y时,scripts/setlocalversion从 git 仓库自动追加描述,甚至在仓库有未提交改动时追加-dirty。
第三条最容易中招。你从板厂拿到一份 git 仓库形式的内核源码,改了.config没提交,编出来的版本号就变成5.10.160-rockchip-dirty,加个后缀就是天壤之别。
修复:
# 方式一:直接改 .config sed -i 's/^CONFIG_LOCALVERSION=.*/CONFIG_LOCALVERSION="-rockchip"/' .config sed -i 's/^CONFIG_LOCALVERSION_AUTO=.*/CONFIG_LOCALVERSION_AUTO=n/' .config # 方式二:编译外部模块时用命令行覆盖 make -C /opt/kernel/linux-5.10.160 M=$(pwd) ARCH=arm64 \ CROSS_COMPILE=aarch64-linux-gnu- LOCALVERSION=-rockchip modules方式二更轻量,但前提是.config里CONFIG_LOCALVERSION_AUTO=n。否则命令行传的LOCALVERSION会被 Kbuild 忽略。这一点在 Kbuild 的顶层 Makefile 里写得很清楚,踩过一次就记住了。
改完之后再modinfo确认一遍,看到 vermagic 和uname -r完全一致,再往板子上传。
4.4 场景四:模块签名相关的 key was rejected
这个跟前面三个完全不是一回事,单独拿出来说是为了让你别在错误的方向上死磕。
现象:
insmod: ERROR: could not insert module yt6801.ko: key was rejected by servicedmesg里可能还会跟一条和签名校验相关的信息,但不会出现disagrees about version of symbol module_layout。
根因:内核编译时开了CONFIG_MODULE_SIG_FORCE=y,或者系统通过其他方式强制校验模块签名,然后你的模块要么没签名,要么签名用的密钥不在内核信任的密钥环里。
修复思路:这条路有几种走法,哪一种合适取决于你的开发阶段。给模块签名需要拿到对应的私钥和签名工具,流程在scripts/sign-file里;开发阶段如果想先跑通,可以考虑暂时关掉强制签名相关配置重编内核。两种都属于"知道是这个问题了,具体选哪条看你的项目阶段",不要跟 CRC 问题混在一起排查。
5. 排查清单、速查表与实操心得
5.1 五分钟定位流程
我把整个排查过程压缩成一条路径,照着走基本五分钟能定位到具体哪一类:
# 第 1 步:板上抓完整报错 dmesg | tail -20 # 第 2 步:看模块自己的身份证 modinfo mydrv.ko | grep vermagic uname -r # 在板子上 # 第 3 步:看有没有版本段、里面写了什么 readelf -S mydrv.ko | grep __versions readelf -x __versions mydrv.ko # 第 4 步:看内核树里的清单 ls -l /opt/kernel/linux-5.10.160/Module.symvers grep module_layout /opt/kernel/linux-5.10.160/Module.symvers # 第 5 步:配置对比 diff <(grep -E 'CONFIG_(SMP|PREEMPT|MODULE_UNLOAD|MODVERSIONS|MODULE_SIG|LOCALVERSION)' /tmp/board.config | sort) \ <(grep -E 'CONFIG_(SMP|PREEMPT|MODULE_UNLOAD|MODVERSIONS|MODULE_SIG|LOCALVERSION)' .config | sort)第 2 步对不上 → 场景三。第 3、4 步对不上 → 场景一。第 2 步对上但第 5 步有差异 → 场景二。全都没问题还有报错 → 往签名方向看。
5.2 报错与动作对照表
| 现象 | 根因 | 动作 |
|---|---|---|
Symbol version dump ... is missing警告 | 内核树没完整编译 | 完整make ... Image modules一次 |
disagrees about version of symbol module_layout | Module.symvers不匹配或缺失 | 用目标板同源内核树的Module.symvers重编 |
| vermagic 字符串不一致 | LOCALVERSION/ 配置标志不同 | 对齐.config,必要时命令行覆盖LOCALVERSION= |
| vermagic 一致但 CRC 不同 | .config中 SIM 类开关差异 | 用板子config.gz覆盖后重编内核 |
no symbol version for X | 内核侧MODVERSIONS未开 | 确认.config的CONFIG_MODVERSIONS |
key was rejected by service | 模块签名校验 | 走签名流程或调整内核签名配置 |
ELF header相关 | 架构或位数写错 | 检查ARCH、CROSS_COMPILE和文件位数 |
5.3 我踩过的坑和几条习惯
第一条习惯:内核源码树单独放一块本地 SSD 目录,绝不放在 NFS 或者网络盘上。内核编译过程中对文件系统的元数据操作极其密集,网络盘会让整编时间从半小时变成两小时,还容易中途出错。而且这棵树的路径一定不要带空格或者中文,Kbuild 在某些子目录里处理路径并不健壮。
第二条习惯:保留一棵"干净树",专门用来编外部模块,绝不在这棵树里执行make clean或者make mrproper。我的做法是把这棵树整个复制一份到/opt/kernel/work/,原始那棵linux-5.10.160只用来参考。复制一次占几十 G 磁盘,但比每次重新编译半小时划算得多。
第三条习惯:每次上板子之前先在宿主机把 vermagic 念一遍。modinfo mydrv.ko | grep vermagic,两秒钟的事情,能省掉一次 scp、一次 insmod、一次看 dmesg 的完整循环。这个习惯养成之后,我在这个报错上花的时间直接降了一个数量级。
第四条习惯:向板厂要 SDK 的时候,一次性把Module.symvers和CONFIG_IKCONFIG_PROC=y一起提。很多板厂的 SDK 只给源码不给Module.symvers,也不开IKCONFIG_PROC,导致每个做驱动的开发者都要自己完整编一遍内核。这活儿一个人做一次没问题,十个人做十次就是纯粹的浪费。
最后再分享一个小技巧。如果你手上同时有多个板子型号,内核源码树长得还都差不多,很容易混。我的做法是在每棵树的根目录下放一个MY_BOARD_INFO文本文件,里面记着这块板子的uname -r输出、CONFIG_LOCALVERSION、Module.symvers的生成日期和对应的 SDK 版本号。编外部模块之前先cat一眼,几秒钟确认自己在对着哪块板子编。这个土办法听起来很傻,但它帮我挡掉过至少三次"给 A 板编的模块插到 B 板上"的低级错误,而那种错误排查起来比现在这个 CRC 问题还要费劲,因为你会一直以为自己环境是对的。