初学 Linux 内核或者第一次编译使用内核模块时经常会遇到类似这样的错误:
insmod: ERROR: could not insert module kvm.ko: Invalid module format
这个报错通常由于当前插入kvm.ko
的version magic
版本信息与正在运行的 kernel 的version magic
版本不一致导致的。
内核校验模块的流程
我们从问题出发,看看内核是如何校验模块的。搜索了内核源码,找到了在函数check_version
中抛出了disagrees about version of symbol
错误信息,我们根据源码来回溯一下整个过程。
// kernel/module.c
static int check_version(const struct load_info *info,
const char *symname,
struct module *mod,
const s32 *crc)
{
Elf_Shdr *sechdrs = info->sechdrs;
unsigned int versindex = info->index.vers;
unsigned int i, num_versions;
struct modversion_info *versions;
/* Exporting module didn't supply crcs? OK, we're already tainted. */
if (!crc)
return 1;
/* No versions at all? modprobe --force does this. */
if (versindex == 0)
return try_to_force_load(mod, symname) == 0;
versions = (void *) sechdrs[versindex].sh_addr;
num_versions = sechdrs[versindex].sh_size
/ sizeof(struct modversion_info);
for (i = 0; i < num_versions; i++) {
u32 crcval;
if (strcmp(versions[i].name, symname) != 0)
continue;
if (IS_ENABLED(CONFIG_MODULE_REL_CRCS))
crcval = resolve_rel_crc(crc);
else
crcval = *crc;
if (versions[i].crc == crcval)
return 1;
pr_debug("Found checksum %X vs module %lX\n",
crcval, versions[i].crc);
goto bad_version;
}
/* Broken toolchain. Warn once, then let it go.. */
pr_warn_once("%s: no symbol version for %s\n", info->name, symname);
return 1;
bad_version:
pr_warn("%s: disagrees about version of symbol %s\n",
info->name, symname);
return 0;
}
参数说明:
- info: 包含正在加载信息的结构体。
- symname: 需要查找对比的符号的名称。
- mod: 表示模块的结构体。
- crc: 当前正在运行的内核的模块符号的 CRC(Cyclic Redundancy Check)值
- 如果 CRC 为空,不检查直接 PASS
- 如果模块中没有_versions 小节,表示模块没有开启 CRC
- 如果开启了 CONFIG_MODULE_FORCE_LOAD,则强制加载,内核标记为 tainted,直接 PASS
- 如果没有开启 CONFIG_MODULE_FORCE_LOAD,则报错
- 如果模块中有_versions 小节,没有找到 module_layout 符号,报 warning,直接 PASS
- 如果模块中有_versions 小节,找到 module_layout 符号
- 对比_versions 小节中的 CRC 和参数中的 CRC,如果一致,PASS
- 如果不一致,报错
插播一句,CRC是什么?在 Linux 内核中,模块符号 CRC(Cyclic Redundancy Check)是一种校验值,用于确保模块中的符号(函数、变量等)在加载时与内核中的符号一致。当模块被构建时,针对每个符号都会计算一个 CRC 值,然后将这些 CRC 值保存在模块的符号版本表中。简单理解就是如果要保持CRC不变,需要满足两个条件:
- 语法保持不变 遵守这个条件,说明如果模块在新内核下重新编译,那应该没有任何语法问题。 即导出符号的类型名没有变化,如果是函数,则要求参数和返回值类型没有任何变化;如果这些类型是结构体的话,结构体的成员名也没有有任何变化。
- 语义保持不变 这要求符号的类型不能有变化,如果类型本身是结构体(struct),则它成员的类型不能有变化,成员在结构体内的位置不能有变化,以及成员本身不能增删。 如果想要深入了解如何计算CRC可以参考这篇博客:Linux内核模块符号CRC检查机制-CSDN博客
分析代码我们可以知道,内核会通过遍历正在加载的模块的版本信息的数组versions
,从中查找与给定符号名称匹配的版本信息。如果找到匹配的版本信息,则计算 CRC 值,与参数中的 CRC 值进行比较。(通过后续分析我们知道参数的 CRC 就是正在运行的内核的 module_layout 符号的 CRC)如果匹配,表示版本一致,返回 1。如果不匹配,打印调试信息,并跳转到 bad_version
,输出警告信息。
这里有两个疑问,
versions
内容是怎么链接到模块的 elf 文件中的?
我们找到模块的mod.c
文件,打开可以发现以下内容:
MODULE_INFO(vermagic, VERMAGIC_STRING);
MODULE_INFO(name, KBUILD_MODNAME);
__visible struct module __this_module
__section(.gnu.linkonce.this_module) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
#ifdef CONFIG_RETPOLINE
MODULE_INFO(retpoline, "Y");
#endif
static const struct modversion_info ____versions[]
__used __section(__versions) = {
{ 0x3e549f1d, "module_layout" },
{ 0x5138e6ba, "param_ops_int" },
{ 0x183b57f9, "phy_ethtool_nway_reset" },
{ 0x6e5363eb, "eth_validate_addr" },
{ 0x4df55e5b, "usb_deregister" },
{ 0x8e48f69b, "usb_register_driver" },
}
这个文件是在编译过程中调用了scripts/modpost
脚本生成的,它的功能是在里面增加了 2 个__section
,.gnu.linkonce.this_module
和__versions
。__versions 小节的内容就是一些字符串和值组成的数组,check_version
就是解析这个小节去做验证。
- CRC 值哪来的?
我们继续向上跟踪,找到函数check_modstruct_version
,其中find_symbol
会在内核符号表中查找给定符号名称的符号信息,接着调用check_version
函数,传入符号名称、模块结构体和 CRC 值,进行版本匹配。
// kernel/module.c
static inline int check_modstruct_version(const struct load_info *info,
struct module *mod)
{
const s32 *crc;
/*
* Since this should be found in kernel (which can't be removed), no
* locking is necessary -- use preempt_disable() to placate lockdep.
*/
preempt_disable();
if (!find_symbol("module_layout", NULL, &crc, true, false)) {
preempt_enable();
BUG();
}
preempt_enable();
return check_version(info, "module_layout", mod, crc);
}
// 获取当前运行内核 module_layout 函数的 crc 值
find_symbol("module_layout", NULL, &crc, true, false)
each_symbol_section(find_exported_symbol_in_section, &fsa)
// 遍历内核三个导出符号表段__start___ksymtab,__start___ksymtab_gpl 和__start___ksymtab_gpl_future,为每段调用 find_symbol_in_section
each_symbol_in_section(arr, ARRAY_SIZE(arr), NULL, fn, data)
// 遍历内核每个已加载模块的三个导出符号表段 mod->syms,mod->gpl_syms,mod->gpl_future_syms,为每段调用 find_symbol_in_section
each_symbol_in_section(arr, ARRAY_SIZE(arr), mod, fn, data)
// 对导出符号表进行二分查找,按照字符串排序,符号表的地址按照地址排序
find_exported_symbol_in_section(const struct symsearch *syms,struct module *owner,void *data)
Linux 对可装载模块采取了两层验证:除了上述的模块 CRC 值校验外还有 vermagic
的检查。模块 vermagic(即 Version Magic String)保存了模块编译时的内核版本以及 SMP 等配置信息,当模块 vermagic 与主机信息不相符时也无法加载模块。
在内核中load_module
函数调用check_modstruct_version
函数完成 CRC 校验后,就会继续调用layout_and_allocate --> check_modinfo
完成 vermagic 校验。
// kernel/module.c
static int check_modinfo(struct module *mod, struct load_info *info, int flags)
{
const char *modmagic = get_modinfo(info, "vermagic");
int err;
if (flags & MODULE_INIT_IGNORE_VERMAGIC)
modmagic = NULL;
/* This is allowed: modprobe --force will invalidate it. */
if (!modmagic) {
err = try_to_force_load(mod, "bad vermagic");
if (err)
return err;
} else if (!same_magic(modmagic, vermagic, info->index.vers)) {
pr_err("%s: version magic '%s' should be '%s'\n",
info->name, modmagic, vermagic);
return -ENOEXEC;
}
...
return 0;
}
get_modinfo
会获取内核中的 vermagic 信息,模块 vermagic 信息则被保存在了 ELF 的 .modinfo
小节中。这里我们说的 vermagic 就是下文提到的拓展版本信息,它就是系统配置信息组成的一个字符串。
如何解决模块校验错误
Linux 对可装载模块采取了两层验证,我们需要分别从 CRC 和 vermagic 两个方面来解决模块校验错误。首先从简单的 vermagic 校验开始。我们需要保证运行的内核版本与模块编译时的内核版本一致,这样才能保证 vermagic 校验通过。首先了解如何查看内核版本以及模块版本信息,然后修改内核模块版本信息。
解决 vermagic 校验错误
如何查看内核版本以及模块版本信息
uname
参数功能:
- -s, 输出 kernel 名称;
- -n, 输出主机名;
- -r, 输出 kernel 发行版本号;
- -v, 输出操作系统版本;
- -m, 输出主机的硬件架构名称;
- -p, 输出处理器类型;
- -i, 输出硬件平台;
- -o, 输出操作系统名称
- -a, 输出所有信息
# 输出kernel发行版本号
uname -r
6.4.0-10.1.0.20.oe2309.riscv64
# 输出所有信息
uname -a
Linux openeuler 6.4.0-10.1.0.20.oe2309.riscv64 #1 SMP Sat Oct 7 06:19:28 UTC 2023 riscv64 riscv64 riscv64 GNU/Linux
modinfo 可以查看模块信息,包括模块vermagic信息。
modinfo kvm.ko
filename: /root/build-kernel/kernel/./arch/riscv/kvm/kvm.ko
license: GPL
author: Qumranet
srcversion: 5DA13DC0E55100B5FE1D56A
depends:
intree: Y
name: kvm
vermagic: 6.4.0 SMP mod_unload modversions riscv
parm: halt_poll_ns:uint
parm: halt_poll_ns_grow:uint
parm: halt_poll_ns_grow_start:uint
parm: halt_poll_ns_shrink:uint
其中vermagic
就是version magic
版本信息,可以看到当前kvm.ko
的version magic
版本信息为6.4.0
。与前文的uname -r
输出的 kernel 发行版本号6.4.0-10.1.0.20.oe2309.riscv64
不一致。所以会报错。
修改内核模块版本信息
- 修改基础版本信息
打开内核源代码根目录下的 Makefile 文件。你会找到一个包含内核版本信息的地方,类似于:
# SPDX-License-Identifier: GPL-2.0
VERSION = 6
PATCHLEVEL = 4
SUBLEVEL = 0
EXTRAVERSION =
表示内核版基础本号为6.4.0
。
- 修改拓展版本信息
kernel 引入了一些配置来增强版本信息,在内核源码的"include/linux/vermagic.h"
下我们可以看到模块的健全版本信息,如下默认配置的有:
/* Simply sanity version stamp for modules. */
#ifdef CONFIG_SMP
#define MODULE_VERMAGIC_SMP "SMP "
#else
#define MODULE_VERMAGIC_SMP ""
#endif
#ifdef CONFIG_PREEMPT_BUILD
#define MODULE_VERMAGIC_PREEMPT "preempt "
#elif defined(CONFIG_PREEMPT_RT)
#define MODULE_VERMAGIC_PREEMPT "preempt_rt "
#else
#define MODULE_VERMAGIC_PREEMPT ""
#endif
#ifdef CONFIG_MODULE_UNLOAD
#define MODULE_VERMAGIC_MODULE_UNLOAD "mod_unload "
#else
#define MODULE_VERMAGIC_MODULE_UNLOAD ""
#endif
#ifdef CONFIG_MODVERSIONS
#define MODULE_VERMAGIC_MODVERSIONS "modversions "
#else
#define MODULE_VERMAGIC_MODVERSIONS ""
#endif
#ifdef RANDSTRUCT
#include <generated/randstruct_hash.h>
#define MODULE_RANDSTRUCT "RANDSTRUCT_" RANDSTRUCT_HASHED_SEED
#else
#define MODULE_RANDSTRUCT
#endif
#define VERMAGIC_STRING \
UTS_RELEASE " " \
MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT \
MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS \
MODULE_ARCH_VERMAGIC \
MODULE_RANDSTRUCT
其中,"UTS_RELEASE"
的配置在内核源码的"include/generated/utsrelease.h"
下可以看到,utsrelease.h
的内容是由Makefile
和``.config的内容来生成的,当成功编译kernel以后,
utsrelease.h`得到更新,
#defineUTS_RELEASE "6.4.0"
那么前文"6.4.0-d46299ae"中的"-d46299ae"是如何得来的呢?其实这个是开发者使用了 Git 来管理代码。当你修改或者提交代码以后,每次编译内核后,在"UTS_RELEASE"后面就会看到一串哈希值,例如"6.4.0-d46299ae",其中"-d46299ae"这个就是 Git 版本控制而产生的哈希值。发行版本号只是各个厂商用于区别自己发布的不同时期的 kernel 版本或者产品版本而产生的编号,完全由各厂商自己定义。
解决 CRC 校验错误
我们可以通过一些手段修改新模块的 module_layout 的 CRC 与内核CRC相同,再插入。/boot
目录下通常有个在symvers-<kernel_version>.gz
文件通常包含了内核模块的符号版本信息。这个文件是由 Linux 内核构建时生成的,用于记录在该内核版本下构建的模块的符号信息,包括函数和变量的名称、版本号等。里面就保存了内核的 module_layout 的 CRC 值。我们可以使用gzip -d
命令解压这个文件,找到 module_layout 的 CRC 值,记录下来。
方法一:使用 16 进制编辑器修改模块文件,将 module_layout 的值修改为相同的值,再插入。 在 Linux 中,可以使用
hexdump
和xxd
等工具查看二进制文件的内容,并尝试编辑。下面是一种使用xxd
查看和修改二进制文件的方法:使用
xxd
将二进制文件转换为十六进制文本:xxd /usr/src/linux-6.4.0-10.1.0.20.oe2309.riscv64/arch/riscv/kvm/kvm.ko > kvm_hex.txt
这将创建一个名为
kvm_hex.txt
的文本文件,其中包含kvm.ko
的十六进制表示。使用文本编辑器(如
nano
或vim
)打开kvm_hex.txt
,找到并编辑module_layout
的值。请确保你了解所做更改的含义,并且只修改你确信的内容。保存并关闭文本编辑器。
使用
xxd
将修改后的十六进制文本转换回二进制文件:xxd -r kvm_hex.txt > /usr/src/linux-6.4.0-10.1.0.20.oe2309.riscv64/arch/riscv/kvm/kvm.ko
这将覆盖原始的
kvm.ko
文件。
方法二:修改 Module.symvers
# 清理编译结果。不要使用 make distclean,这会删除.config 文件以及 Module.symvers 文件 make clean # 修改 Module.symvers 文件 sed -i '/module_layout/ s/0x[0-9a-f][0-9a-f]*/0xdf88831e/' Module.symvers # 重新编译模块 make M=./arch/riscv/kvm/ -j33 # 检查模块的 module_layout modprobe --dump-modversions /usr/src/linux-6.4.0-10.1.0.20.oe2309.riscv64/arch/riscv/kvm/kvm.ko | grep module_layout 0xdf88831e
如何生成 .mod.c 文件
如果是自己写的模块,可以根据下面的命令编译模块,就可以得到.mod.c
文件,它是源文件到.mod.o
文件的一个中间文件。
make -C /root/build-kernel/kernel M=/driver_study/ modules -j22
如果是用内核自己的模块,执行make modules
命令,也可以得到.mod.c
文件。但是一般执行make -j22
命令就不会生成这些文件,我们可以找一个启用的内核模块,例如kvm.ko
,执行下面的命令,就可以得到.mod.c
文件。不能随便找个模块,必须在 config 中开启的模块,否则会报错undefined!
。
make M=./arch/riscv/kvm modules -j22