内核Module.symvers文件揭秘

很多朋友在编译Linux内核模块时,经常遇到下面的错误:

WRANING:symbol version dump "Module.symvers" is missing
no rule to make target 'scripts/module.lds'

通过提示信息可以看到:编译出错的大概原因是缺少一个叫做Module.symvers的文件。解决方法也很简单,在linux内核源码根目录下,使用make modules或者make all命令,编译一下独立的模块,生成out-of-tree模块依赖的完整符号表:Module.symvers文件就可以了。

那么这个文件是干嘛用的呢?这还得从Linux内核的编译说起。

在一个普通的C语言项目中,函数默认是一个强符号,在一个a.c中定义的函数,在b.c中声明之后就可以直接调用。而Linux内核则不支持这种调用方式:在a.c中定义的函数func,你要使用EXPORT_SYMBOL导出,然后在b.c中才能正常调用。如果你在a.c定义的func没有使用EXPORT_SYMBOL导出,b.c是无法调用的。a.c导出的符号func保存在各自目录下的Module.symvers文件中,内核编译时再将这些符号收集汇总,供其他模块调用。同样的道理,如果一个模块在编译时,需要导出符号供其他人使用,也会生成该模块对应的Module.symvers文件,保存在该模块的当前目录下。

  CC [M]  drivers/char/hello.o
  MODPOST Module.symvers
  CC [M]  drivers/char/hello.mod.o
  LD [M]  drivers/char/hello.ko

接下来,我们就以Linux-5.10.4内核编译为例,来给大家分下一下Module.symvers这个文件的作用。

当我们使用make或make modules编译内核时,一般都会执行到Makefile中的modules目标,在这个目标的规则中,会执行scripts/Makefile.modpost文件:

modules: $(if $(KBUILD_BUILTIN),vmlinux)  modules_check  modules_prepare
        $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost

在modules规则的命令行中,执行scripts/Makefile.modpost时并没有指定目标,所以会执行scripts/Makefile.modpost的默认目标:

PHONY := __modpost
__modpost:
__modpost: $(output-symdump)
        $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modfinal

在__modpost的规则中,命令行最后会执行scripts/Makefile.modfinal文件,因为没有指定目标,所以会执行默认目标:

PHONY := __modfinal
__modfinal:
__modfinal: $(modules)
# find all .ko modules listed in modules.order
modules := $(sort $(shell cat $(MODORDER)))

modules展开后就是各个子目录下的modules.order文件,每个modules.order文件中存放的是.ko文件的路径名:

# cat modules.order 
drivers/char/hello.ko

每个.ko文件对应的规则,在scripts/Makefile.modfinal中定义:

$(modules): %.ko :  %.o %.mod.o scripts/module.lds FORCE
  +$(call if_changed,ld_ko_o)
cmd_ld_ko_o =                                                     \
$(LD) -r $(KBUILD_LDFLAGS) $(KBUILD_LDFLAGS_MODULE) $(LDFLAGS_MODULE) \
    -T scripts/module.lds -o $@ $(filter %.o, $^);    \
     $(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)

Linux内核中模块的编译一般分为以下步骤

步骤01:
  • 将每个源文件编译为对应的.o目标文件
  • 将每个单独的目标文件链接成模块文件module.o
  • 生成对应的module.mod文件,该文件保存链接到模块的所有原始 .o目标文件
  • 生成modules.order文件,里面保存的是所有的KO文件
步骤02:
  • 从modules.order中查找所有的KO模块
  • 使用modpost,为每个KO模块创建module.mod.c文件
  • 创建Module.symvers文件,保存模块中通过export导出的符号及其CRC值
步骤03:主要生成和模块相关的信息
  • 版本魔幻数
  • 模块信息、License、version、alias
步骤04:
  • 外部模块的版本验证
  • 通过module.symvers文件,检测模块编译需要的内核符号是否存在

以hello.ko为例,这个模块就是由hello.o、hello.mod.o链接而成,生成最终的hello.ko文件。核心变量:KBUILD_EXTMOD用来指定模块的位置,如果是再Linux内核源码外编译独立的模块,由命令行的变量M指定。如果是源码内编译模块,KBUILD_EXTMOD变量为空。模块在编译过程中,如果依赖一个外部模块,需要使用KBUILD_EXTMOD来指定外部模块的符号文件Module.symvers,否则也会产生编译错误。

最后做个小结:Linux内核中,一个模块的编译如果需要调用内核提供的各种接口,需要依赖内核编译时生成的Module.symvers文件,在这个文件中保存了大量通过EXPORT_SYMBOL导出的全局符号。同理,如果一个源码外单独编译的模块也需要导出一些函数接口给内核其他模块使用,也会生成一个独立的Module.symvers文件,其他模块在编译时,需要显示指定这个Module.symvers文件的位置,然后才能调用你这个模块实现的函数接口。

明白了这个道理之后,再去看看上面的编译错误,就非常简单了:如果你在Linux内核源码外编译一个独立的模块,需要你先编译一遍内核,生成对应的Module.symvers文件,独立的模块需要依赖这个文件才能正常编译,否则就会报错,出现文章开头所示的编译错误信息。

本文根据Linux内核驱动开发实战教程:《Linux内核编程》第02期:Kbuild子系统改编,更多内核驱动开发实战教程,请参考:Linux内核编程 第02期:Kbuild子系统

驱动开发核心理论,Linux内核开发入门实战视频教程:《Linux内核编程》,具有一线芯片原厂开发经验的驱动工程师录制,详情点击:王利涛老师个人淘宝店:Linux内核编程