ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内核模块交叉编译Invalid module format报错排查

Linux内核模块交叉编译Invalid module format报错排查 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_layoutCRC 问题你的Module.symvers大概率缺失或者来自另一份内核。no symbol version for module_layout模块里有版本信息但内核侧没有大概率是内核编译时CONFIG_MODVERSIONSn。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-rockchipUTS_RELEASE内核版本 CONFIG_LOCALVERSION后缀SMPCONFIG_SMPy对称多处理preemptCONFIG_PREEMPTy抢占式内核mod_unloadCONFIG_MODULE_UNLOADy允许卸载模块modversionsCONFIG_MODVERSIONSy开启符号 CRC 校验aarch64MODULE_ARCH_VERMAGIC架构标识arm64 上基本就是这个这里有两个实战要点。第一5.10.160-rockchip里的-rockchip后缀来自.config的CONFIG_LOCALVERSION-rockchip或者来自内核源码里的localversion*文件或者来自CONFIG_LOCALVERSION_AUTOy时 git 仓库自动追加的描述。板厂发布的内核全都带这个后缀你自己从 kernel.org 拉的干净源码编译出来是裸的5.10.160vermagic 直接对不上。第二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 (builderci) (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_PROCy打开并顺手放一份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 ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- olddefconfig make ARCHarm64 CROSS_COMPILEaarch64-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 ARCHarm64 CROSS_COMPILEaarch64-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) \ ARCHarm64 \ CROSS_COMPILEaarch64-linux-gnu- \ LOCALVERSION-rockchip \ modules逐参数拆一下每一个都有它存在的理由-C切到内核源码目录借用它的顶层 Makefile。这是整套机制的入口-C后面跟了错的树后面全白搭。M$(pwd)告诉 Kbuild 真正的模块源码在哪里。Kbuild 会为这个目录单独跑一遍modpost。ARCHarm64决定编译哪些架构相关的代码也影响MODULE_ARCH_VERMAGIC写错就变成给错误架构编模块。CROSS_COMPILEaarch64-linux-gnu-指定工具链前缀注意结尾那个横杠不能少。LOCALVERSION-rockchip强制覆盖本地版本后缀。这个参数只在.config里CONFIG_LOCALVERSION_AUTO为n时才会生效CONFIG_LOCALVERSION_AUTOy时 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。内核的一些生成文件是会缓存的必须重新走一遍olddefconfigmodules_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_AUTOy时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_AUTOn/ .config # 方式二编译外部模块时用命令行覆盖 make -C /opt/kernel/linux-5.10.160 M$(pwd) ARCHarm64 \ CROSS_COMPILEaarch64-linux-gnu- LOCALVERSION-rockchip modules方式二更轻量但前提是.config里CONFIG_LOCALVERSION_AUTOn。否则命令行传的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_FORCEy或者系统通过其他方式强制校验模块签名然后你的模块要么没签名要么签名用的密钥不在内核信任的密钥环里。修复思路这条路有几种走法哪一种合适取决于你的开发阶段。给模块签名需要拿到对应的私钥和签名工具流程在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_layoutModule.symvers不匹配或缺失用目标板同源内核树的Module.symvers重编vermagic 字符串不一致LOCALVERSION/ 配置标志不同对齐.config必要时命令行覆盖LOCALVERSIONvermagic 一致但 CRC 不同.config中 SIM 类开关差异用板子config.gz覆盖后重编内核no symbol version for X内核侧MODVERSIONS未开确认.config的CONFIG_MODVERSIONSkey 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_PROCy一起提。很多板厂的 SDK 只给源码不给Module.symvers也不开IKCONFIG_PROC导致每个做驱动的开发者都要自己完整编一遍内核。这活儿一个人做一次没问题十个人做十次就是纯粹的浪费。最后再分享一个小技巧。如果你手上同时有多个板子型号内核源码树长得还都差不多很容易混。我的做法是在每棵树的根目录下放一个MY_BOARD_INFO文本文件里面记着这块板子的uname -r输出、CONFIG_LOCALVERSION、Module.symvers的生成日期和对应的 SDK 版本号。编外部模块之前先cat一眼几秒钟确认自己在对着哪块板子编。这个土办法听起来很傻但它帮我挡掉过至少三次给 A 板编的模块插到 B 板上的低级错误而那种错误排查起来比现在这个 CRC 问题还要费劲因为你会一直以为自己环境是对的。
返回列表