Linux内核模块校验机制

初学 Linux 内核或者第一次编译使用内核模块时经常会遇到类似这样的错误:

insmod: ERROR: could not insert module kvm.ko: Invalid module format 

这个报错通常由于当前插入kvm.koversion 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)值
  1. 如果 CRC 为空,不检查直接 PASS
  2. 如果模块中没有_versions 小节,表示模块没有开启 CRC
    1. 如果开启了 CONFIG_MODULE_FORCE_LOAD,则强制加载,内核标记为 tainted,直接 PASS
    2. 如果没有开启 CONFIG_MODULE_FORCE_LOAD,则报错
  3. 如果模块中有_versions 小节,没有找到 module_layout 符号,报 warning,直接 PASS
  4. 如果模块中有_versions 小节,找到 module_layout 符号
    1. 对比_versions 小节中的 CRC 和参数中的 CRC,如果一致,PASS
    2. 如果不一致,报错

插播一句,CRC是什么?在 Linux 内核中,模块符号 CRC(Cyclic Redundancy Check)是一种校验值,用于确保模块中的符号(函数、变量等)在加载时与内核中的符号一致。当模块被构建时,针对每个符号都会计算一个 CRC 值,然后将这些 CRC 值保存在模块的符号版本表中。简单理解就是如果要保持CRC不变,需要满足两个条件:

  1. 语法保持不变
    遵守这个条件,说明如果模块在新内核下重新编译,那应该没有任何语法问题。 即导出符号的类型名没有变化,如果是函数,则要求参数和返回值类型没有任何变化;如果这些类型是结构体的话,结构体的成员名也没有有任何变化。
  2. 语义保持不变
    这要求符号的类型不能有变化,如果类型本身是结构体(struct),则它成员的类型不能有变化,成员在结构体内的位置不能有变化,以及成员本身不能增删。
    如果想要深入了解如何计算CRC可以参考这篇博客:Linux内核模块符号CRC检查机制-CSDN博客

分析代码我们可以知道,内核会通过遍历正在加载的模块的版本信息的数组versions,从中查找与给定符号名称匹配的版本信息。如果找到匹配的版本信息,则计算 CRC 值,与参数中的 CRC 值进行比较。(通过后续分析我们知道参数的 CRC 就是正在运行的内核的 module_layout 符号的 CRC)如果匹配,表示版本一致,返回 1。如果不匹配,打印调试信息,并跳转到 bad_version,输出警告信息。

这里有两个疑问,

  1. 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就是解析这个小节去做验证。

  1. 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.koversion magic版本信息为6.4.0。与前文的uname -r输出的 kernel 发行版本号6.4.0-10.1.0.20.oe2309.riscv64不一致。所以会报错。

修改内核模块版本信息

  1. 修改基础版本信息

打开内核源代码根目录下的 Makefile 文件。你会找到一个包含内核版本信息的地方,类似于:

# SPDX-License-Identifier: GPL-2.0
VERSION = 6
PATCHLEVEL = 4
SUBLEVEL = 0
EXTRAVERSION =

表示内核版基础本号为6.4.0

  1. 修改拓展版本信息

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 中,可以使用 hexdumpxxd 等工具查看二进制文件的内容,并尝试编辑。下面是一种使用 xxd 查看和修改二进制文件的方法:

    1. 使用 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 的十六进制表示。

    2. 使用文本编辑器(如 nanovim)打开 kvm_hex.txt,找到并编辑 module_layout 的值。请确保你了解所做更改的含义,并且只修改你确信的内容。

    3. 保存并关闭文本编辑器。

    4. 使用 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