Kernel之MODVERSION

官方说明

关于modversion,内核文档官方解释(Documentation/kbuild/modules.rst):

6 Module Versioning

Module versioning is enabled by the CONFIG_MODVERSIONS tag, and is used
as a simple ABI consistency check. A CRC value of the full prototype
for an exported symbol is created. When a module is loaded/used, the
CRC values contained in the kernel are compared with similar values in
the module; if they are not equal, the kernel refuses to load the
module.

Module.symvers contains a list of all exported symbols from a kernel
build.

6.1 Symbols From the Kernel (vmlinux + modules)

 During a kernel build, a file named Module.symvers will be
 generated. Module.symvers contains all exported symbols from
 the kernel and compiled modules. For each symbol, the
 corresponding CRC value is also stored.

 The syntax of the Module.symvers file is::

 <CRC>       <Symbol>         <Module>                         <Export Type>     <Namespace>

 0xe1cc2a05  usb_stor_suspend drivers/usb/storage/usb-storage  EXPORT_SYMBOL_GPL USB_STORAGE

 The fields are separated by tabs and values may be empty (e.g.
 if no namespace is defined for an exported symbol).

 For a kernel build without CONFIG_MODVERSIONS enabled, the CRC
 would read 0x00000000.

 Module.symvers serves two purposes:

 1) It lists all exported symbols from vmlinux and all modules.
 2) It lists the CRC if CONFIG_MODVERSIONS is enabled.

6.2 Symbols and External Modules

 When building an external module, the build system needs access
 to the symbols from the kernel to check if all external symbols
 are defined. This is done in the MODPOST step. modpost obtains
 the symbols by reading Module.symvers from the kernel source
 tree. If a Module.symvers file is present in the directory
 where the external module is being built, this file will be
 read too. During the MODPOST step, a new Module.symvers file
 will be written containing all exported symbols that were not
 defined in the kernel.

6.3 Symbols From Another External Module

 Sometimes, an external module uses exported symbols from
 another external module. Kbuild needs to have full knowledge of
 all symbols to avoid spitting out warnings about undefined
 symbols. Three solutions exist for this situation.

 NOTE: The method with a top-level kbuild file is recommended
 but may be impractical in certain situations.

 Use a top-level kbuild file
     If you have two modules, foo.ko and bar.ko, where
     foo.ko needs symbols from bar.ko, you can use a
     common top-level kbuild file so both modules are
     compiled in the same build. Consider the following
     directory layout::

         ./foo/ <= contains foo.ko
         ./bar/ <= contains bar.ko

     The top-level kbuild file would then look like::

         #./Kbuild (or ./Makefile):
             obj-m := foo/ bar/

     And executing::

         $ make -C $KDIR M=$PWD

     will then do the expected and compile both modules with
     full knowledge of symbols from either module.

 Use an extra Module.symvers file
     When an external module is built, a Module.symvers file
     is generated containing all exported symbols which are
     not defined in the kernel. To get access to symbols
     from bar.ko, copy the Module.symvers file from the
     compilation of bar.ko to the directory where foo.ko is
     built. During the module build, kbuild will read the
     Module.symvers file in the directory of the external
     module, and when the build is finished, a new
     Module.symvers file is created containing the sum of
     all symbols defined and not part of the kernel.

 Use "make" variable KBUILD_EXTRA_SYMBOLS
     If it is impractical to copy Module.symvers from
     another module, you can assign a space separated list
     of files to KBUILD_EXTRA_SYMBOLS in your build file.
     These files will be loaded by modpost during the
     initialization of its symbol tables.

意义及用途

主要可以进行简单的ABI一致性检查

模块版本控制(MODVERSION)用途:

  1. 它会捕获数据结构可见性发生的更改。如果模块可以更改不透明数据结构(例如不属于
    KMI 的数据结构),则将在相应结构未来发生更改后出现异常。
  2. 它会添加运行时检查,以避免意外加载 KMI 与内核不兼容的模块。(例如,日后不兼
    容的新内核加载当前模块时。)与发生难以调试的后续运行时问题或内核崩溃相比,最
    好使用它。
  3. 就在某些卷积情况下确定 CONFIG_MODVERSIONS 可以捕获的 ABI 差异而言, abidiff
    存在限制。

在Android系统上的应用

应用二进制接口(ABI)监控,使用dump_abi和diff_abi工具
参考Android内核,ABI 监控工具在 AOSP 中开发
(https://android.googlesource.com/kernel/build/+/refs/heads/master/abi/),使用 libabigail(https://sourceware.org/libabigail/) 生成和比较

相关使用

  • Module.symvers文件
    Module.symvers包含了kernel的所有的导出符号的CRC校验值,其是由genksym 命令生成
    可以对比Module.symvers文件是否一致

  • 使用nm命令查看特定符号CRC

    nm <path to vmlinux>/vmlinux | grep __crc_<symbol name>
  • 查看模块KO中的modversions信息:

    modprobe --dump-modversions xxx.KO

    ABI合规性破坏原因及修复

    最常见的ABI合规性破坏原因包括:

  • 添加或删除了函数

  • 更改了数据结构

  • 添加配置选项

修复CRC不一致:

  1. 构建内核时,命令前添加KBUILD_SYMTYPES=1,这将为每个 .o 文件生成一个 .symtypes 文件
  2. 对比正常和不正常下对应的该.symtypes文件差异,并修复其不一致问题

注意: 最好在 diff 工具中启用换行。(例如,对于 vimdff ,使用 :set wrap 。)

相关代码分析

CONFIG_MODVERSIONS打开时,内核会在加载外部模块KO时, 去对比符号的CRC,
主要代码如下(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;
}

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);
}

参考