Chinaunix首页 | 论坛 | 博客
  • 博客访问: 122114
  • 博文数量: 26
  • 博客积分: 15
  • 博客等级: 民兵
  • 技术积分: 15
  • 用 户 组: 普通用户
  • 注册时间: 2010-03-24 21:45
文章分类
文章存档

2019年(1)

2018年(6)

2017年(17)

2016年(2)

我的朋友

分类: LINUX

2018-03-13 10:53:56

原文地址:内核Kbuild 学习 作者:emmoblin

首先,如果mixed-targets取值为1,则表明是混合目标的情况,构建系统要处理框架中的C部分。

我们取出其中代码如下:

  1. # ===========================================================================
  2. # We're called with mixed targets (*config and build targets).
  3. # Handle them one by one.

  4. %:: FORCE
  5. $(Q)$(MAKE) -C $(srctree) KBUILD_SRC= $@

从代码中可以看出,这里使用了一个双冒号的模式匹配规则。

百分号代表任何目标都使用这个规则,其中$(srctree)为内核代码树所在目录,KBUILD_SRC定义为空。

所以如果make命令为:make s3c2410_defconfig all,那么构建系统就会分别执行下面两条命令:

make -C $(srctree) KBUILD_SRC= s3c2410_defconfig
make -C $(srctree) KBUILD_SRC= all

这其实和简单的用手动的输入两条连续命令(make s3c2410_defconfig 和 make all)是一样效果的。


编译host programara

make -f scripts/Makefile.build obj=scripts/basic 命令由于没有指定目标,

所以会在 script/Makefile.build 中处理默认目标__build,如下:

  1. __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
  2.          $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
  3.          $(subdir-ym) $(always)
  4.         echo 'KBUILD_MODULES := $(KBUILD_MODULES)' >> modules_builtin.cmd
  5.         echo 'KBUILD_BUILTIN := $(KBUILD_BUILTIN)' >> modules_builtin.cmd
  6.         @:

同时,别忘记在scripts/Makefile.build中会包含进 scripts/basic 目录下的 Kbuild/Makefile,
所以该make命令的实际效果是去编译出 scripts/basic 目录下的三个 host program,
也就是 fixdep docproc和hash。

接着执行 $(Q)$(MAKE) $(build)=scripts/kconfig $@ 。
如果我们的make 命令是 "make s3c2410_defconfig" 的话,这个时候执行的就是:

make -f scripts/Makefile.build obj=scripts/kconfig s3c2410_defconfig

又牵涉到文件 scripts/Makefile.build 了,我们先搜索一下这个文件里面有没有 s3c2410_defconfig 或 类似于 %config 之类的目标。没有,怎么办,该不会是弄错了吧?呵呵,你忘记了么?

文件 scripts/Makefile.build 会包含obj变量所指代目录内的 Makefile的,在这里就是 script/kconfig/Makefile。打开这个文件,果然能找到相关的规则:

Linux内核构建系统中defconfig系列目标的处理
在这里,s3c2410_defconfig 需要依赖于同目录下的conf程序。这其实就是Linux内核进行Kconfig操作的主程序之一了,类似的还有mconf,qconf和gconf等。 他们其实都是host program。关于它们是如何被编译出来的,还请参见 scripts/kconfig/Makefile 文件,主要是借助于bison,flex和gperf三个工具来生成c源程序文件,之后再编译出来的。

GNU Make 是这样一个大致的读取Makefile的流程:首先它读入主Makefile,在读的过程中,如果碰到 "include" 或 "-include",它就会包含对应的文件。如果对应的文件不存在,则暂时跳过做包含的地方,继续读入。待所有makefie都读完后。GNU Make会考虑将每个makefile作为目标,在全局范围内查找是否有能生成这些目标的规则,如果发现有一个makefile可以被一条规则生成,那么 GNU Make就会先生成这个makefile。生成后,GNU Make又会从零开始读入主Makefile以及所有被包含的makefile,然后再检查是否有makefile可以被remade….这样一次又一 次,直到所有的makefile都不需要再次生成了,它才处理依赖规则链。它之这样做,是为了保证所有 makefile 都是 update-to-date 的。

有两方面的用户需要关注所记载的配置结果。一个自然是内核构建系统,它需要根据配置结果产生具有指定功能的内核映像;另外一个就是大部分代码为C语言代码 的Linux内核本身,它也需要用户的配置结果,主要用来预处理C代码。前者使用配置结果,并不是直接通过 .config 文件来的,而是将其转换成两个文件:include/config/auto.conf 和 include/config/auto.conf.cmd。后者也没办法直接通过 .config 文件来使用配置结果,它需要将其转换成C语言头文件的形式使用,在这里就是文件 include/linux/autoconf.h

再次回到处理 auto.conf 的那条规则上来,我们看到它的命令 "$(Q)$(MAKE) -f $(srctree)/Makefile silentoldconfig",这个命令最终会导致GNU Make执行文件 scripts/kconfig/Makefile 中针对目标 silentoldconfig 的命令:

$(obj)/conf -s arch/arm/Kconfig

conf配置程序在前面已经有所提及,其对应的代码都在目录 scripts/kconfig/ 中。conf 的主函数main即定义在 conf.c 文件中。其实,目标silentoldconfig 和 目标oldconfig类似,只不过它多了生成auto.conf、auto.conf.cmd以及autoconf.h等三个文件的任务。这是怎么做到 的?答案就在conf.c文件中 main 函数最后的一段代码:

  1. int main(int ac, char **av)
  2. {
  3.         int opt;
  4.         const char *name;
  5.         struct stat tmpstat;
  6.  
  7.         .....
  8.         .....
  9.         
  10.         if (sync_kconfig) {
  11.                 /* silentoldconfig is used during the build so we shall update autoconf.
  12.                  * All other commands are only used to generate a config.
  13.                  */
  14.                 if (conf_get_changed() && conf_write(NULL)) {
  15.                         fprintf(stderr, _("\n*** Error during writing of the kernel configuration.\n\n"));
  16.                         exit(1);
  17.                 }
  18.                 if (conf_write_autoconf()) {
  19.                         fprintf(stderr, _("\n*** Error during update of the kernel configuration.\n\n"));
  20.                         return 1;
  21.                 }
  22.         } else {
  23.                 if (conf_write(NULL)) {
  24.                         fprintf(stderr, _("\n*** Error during writing of the kernel configuration.\n\n"));
  25.                         exit(1);
  26.                 }
  27.         }
  28.         return 0;
  29. }

前面我们已经介绍过,当用 conf 处理 silentoldconfig 时,变量sync_kconfig会被设置为1。实际上,也只有处理此目标时,它才会被设置成1,其他的目标都不会。对于oldconfig、 menuconfig等目标来说,conf程序最后会直接调用函数 conf_write 将配置结果写到配置文件 .config 中去。
而对于 silentoldconfig 目标来说,conf 程序除了调用 conf_write 来写 .config 文件外,它还会调用 conf_write_autoconf 函数来完成 auto.conf、auto.conf.cmd 和 autoconf.h 三个文件的生成。

if_changed讲解

  1. # Execute command if command has changed or prerequisite(s) are updated.
  2. #
  3. if_changed = $(if $(strip $(any-prereq) $(arg-check)), \
  4.         @set -e; \
  5.         $(echo-cmd) $(cmd_$(1)); \
  6.         echo 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd)
  7.  
  8. # Execute the command and also postprocess generated .d dependencies file.
  9. if_changed_dep = $(if $(strip $(any-prereq) $(arg-check) ), \
  10.         @set -e; \
  11.         $(echo-cmd) $(cmd_$(1)); \
  12.         scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).tmp;\
  13.         rm -f $(depfile); \
  14.         mv -f $(dot-target).tmp $(dot-target).cmd)
  15.  
  16. # Usage: $(call if_changed_rule,foo)
  17. # Will check if $(cmd_foo) or any of the prerequisites changed,
  18. # and if so will execute $(rule_foo).
  19. if_changed_rule = $(if $(strip $(any-prereq) $(arg-check) ), \
  20.         @set -e; \
  21.         $(rule_$(1)))
最简单的就是 if_changed,当发现规则的依赖有被更新了、或者编译该规则对应目标的命令行发生改变了,
它就先用$(echo-cmd) 回显出新的命令$(cmd_$(1)),接着执行命令$(cmd_$(1)),
最后再将该命令写到一个叫做$(dot-target).cmd 的临时文件中去,
以方便下一次检查命令行是否有变的时候用。
变量 dot-target 定义成 .targetname 的形式,如下:

###
# Name of target with a '.' as filename prefix. foo/bar.o => foo/.bar.o
dot-target = $(dir $@).$(notdir $@)

那如何去检查规则的依赖文件被更新了,以及检查编译该规则对应目标的命令行发生改变了的呢?

答案就是下面的两个定义:

  1. # Find any prerequisites that is newer than target or that does not exist.
  2. # PHONY targets skipped in both cases.
  3. any-prereq = $(filter-out $(PHONY),$?) $(filter-out $(PHONY) $(wildcard $^),$^)

  4. ifneq ($(KBUILD_NOCMDDEP),1)
  5. # Check if both arguments has same arguments. Result is empty string if equal.
  6. # User may override this check using make KBUILD_NOCMDDEP=1
  7. arg-check = $(strip $(filter-out $(cmd_$(1)), $(cmd_$@)) \
  8.                     $(filter-out $(cmd_$@), $(cmd_$(1))) )
  9. endif

在 any-prereq 变量定义中,$(filter-out $(PHONY),$?) 指代的是那些比目标还新的依赖文件,而 $(filter-out $(PHONY) $(wildcard $^),$^) 指的是那些当前还不存在的依赖文件。另外注意 arg-check 变量定义中比较新老命令的方式。假设我们现在有下面这样一条规则调用了函数 if_changed:

使用 if_changed 的例子

那么上面比较的是变量 cmd_link_target 所指代的新命令和变量 cmd_target 所指代的老命令。而这个老命令就是被 if_changed 写入文件 .target.cmd 的。可以想见,内核构建系统必定在某个地方将这些包含老命令的 .*.cmd 读入进来。没错,读入代码可以找到在顶层Makefile中:

  1. # read all saved command lines
  2.  
  3. targets := $(wildcard $(sort $(targets)))
  4. cmd_files := $(wildcard .*.cmd $(foreach f,$(targets),$(dir $(f)).$(notdir $(f)).cmd))
  5.  
  6. ifneq ($(cmd_files),)
  7.   $(cmd_files): ; # Do not try to update included dependency files
  8.   include $(cmd_files)
  9. endif

注意了,上面只包含了处理那些列在变量 targets 中的目标的老命令。所以如果你想让构建系统也帮你比较新老命令,并若发现其中有区别就帮你处理的话,你需要将你的目标也列入 targets 变量中。另外,因为构建系统中目录 scripts 下的很多Makfile和顶层Makefile是独立运行的,所以在目录 scripts 下面,像在 Makefile.build、Makefile.headersinst、Makefile.modpost以及Makefile.fwinst等文件 中,你也可以找到类似的读入代码。

if_changed_dep 函数和 if_changed 差不多,所不同的是它用 fixdep 工具程序处理了依赖文件 *.d ,并将依赖信息也一并写入到文件 .targetname.cmd 文件中去。可以说依赖处理是整个内核构建系统中最难理解的部分,我们后面会花一点专门的篇幅来讨论它。if_changed_rule 其实也和 if_changed 差不多,只不过它直接调用了命令 rule_$(1),而不是 cmd_$(1) 所指代的命令。if_changed_XXX 系列在内核构建系统中用的比较多,还请注意掌握。


vmlinux.dirs的处理
  1. vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
  2.                      $(core-y) $(core-m) $(drivers-y) $(drivers-m) \
  3.                      $(net-y) $(net-m) $(libs-y) $(libs-m)))

  1. PHONY += $(vmlinux-dirs)
  2. $(vmlinux-dirs): prepare scripts
  3.         $(Q)$(MAKE) $(build)=$@
在看这条规则的命令之前,我们先看看它有两个依赖:prepare 和 scripts。
下面是顶层Makefile中prepare 相关的代码:
  1. # Things we need to do before we recursively start building the kernel
  2. # or the modules are listed in "prepare".
  3. # A multi level approach is used. prepareN is processed before prepareN-1.
  4. # archprepare is used in arch Makefiles and when processed asm symlink,
  5. # version.h and scripts_basic is processed / created.
  6.  
  7. # Listed in dependency order
  8. PHONY += prepare archprepare prepare0 prepare1 prepare2 prepare3
  9.  
  10. # prepare3 is used to check if we are building in a separate output directory,
  11. # and if so do:
  12. # 1) Check that make has not been executed in the kernel src $(srctree)
  13. # 2) Create the include2 directory, used for the second asm symlink
  14. prepare3: include/config/kernel.release
  15. ifneq ($(KBUILD_SRC),)
  16.         @$(kecho) ' Using $(srctree) as source for kernel'
  17.         $(Q)if [ -f $(srctree)/.config -o -d $(srctree)/include/config ]; then \
  18.                 echo " $(srctree) is not clean, please run 'make mrproper'";\
  19.                 echo " in the '$(srctree)' directory.";\
  20.                 /bin/false; \
  21.         fi;
  22.         $(Q)if [ ! -d include2 ]; then \
  23.             mkdir -p include2; \
  24.             ln -fsn $(srctree)/include/asm-$(SRCARCH) include2/asm; \
  25.         fi
  26. endif
  27.  
  28. # prepare2 creates a makefile if using a separate output directory
  29. prepare2: prepare3 outputmakefile
  30.  
  31. prepare1: prepare2 include/linux/version.h include/linux/utsrelease.h \
  32.                    include/asm include/config/auto.conf
  33.         $(cmd_crmodverdir)
  34.  
  35. archprepare: prepare1 scripts_basic
  36.  
  37. prepare0: archprepare FORCE
  38.         $(Q)$(MAKE) $(build)=.
  39.         $(Q)$(MAKE) $(build)=. missing-syscalls
  40.  
  41. # All the preparing..
  42. prepare: prepare0

构建系统处理 prepare 及相关目标的目的是为了后面真正进入各子目录编译内核或模块做准备。

在这些目标的处理中,最重要的莫过于对 prepare0 目标的处理了。注意目标 prepare0 对应规则的命令,它们会调用 scripts/Makefile.build,并且在其中包含顶层目录中的 Kbuild 文件,其功能分别是由 arch/arm/kerel/asm-offset.c 文件生成 include/asm-arm/asm-offset.h 文件以及使用 scripts/checksyscalls.sh 来检查是否还有未实现的系统调用(检查时,以i386所实现的系统调用为比较依据)。

至于 $(vmlinux-dires) 所依赖的另外一个目标 scripts

它就是为了在真正进入各子目录编译内核或者模块之前,在目录 scripts 中准备好若干工具

其规则如下:

  1. # Additional helpers built in scripts/
  2. # Carefully list dependencies so we do not try to build scripts twice
  3. # in parallel
  4. PHONY += scripts
  5. scripts: scripts_basic include/config/auto.conf
  6.         $(Q)$(MAKE) $(build)=$(@)

$(Q)$(MAKE) $(build)=$@如何执行的

 "$(Q)$(MAKE) $(build)=$@" 其实就是调用 scripts/Makefile.Build 文件,

依次在其中包含变量 $(vmlinux-dirs) 所对应各目录的 Kbuild/Makefile

最终在各目录中编译出不同的对象文件来(一系列的.o文件和.a文件)

这到底是如何实现的?我们先看命令 "$(Q)$(MAKE) $(build)=$@",简化出来就是:

make -f scripts/Makefile.build obj=$@

$@代表所有目标集合。

执行Makefile.build文件,并传递参数obj=scripts/basic.
 
在Makefile.build的第5行有:
 
src := $(obj)
 
这就把传递进来的值赋给了src,并包换obj目录中的Kbuild/Makefile。

  1. kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
  2. kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
  3. include $(kbuild-file)

由于上面这个命令中并没有指定要处理什么具体的目标,所以此make命令实际上是在处理 scripts/Makefile.build 中的默认目标:__build,我们列出具体的规则:

  1. __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
  2.          $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
  3.          $(subdir-ym) $(always)
  4.         echo 'KBUILD_MODULES := $(KBUILD_MODULES)' >> modules_builtin.cmd
  5.         echo 'KBUILD_BUILTIN := $(KBUILD_BUILTIN)' >> modules_builtin.cmd
  6.         @:

这这条规则没有什么命令,但是却有很多依赖。
正是这些依赖指明了内核构建系统进入到各个子目录(由vmlinux-dirs中列出)后所要编译处理的各种目标。
如果要处理基本内核,那变量KBUILD_BUILTIN被设置为1;
如果要编译内部模块,那变量KBUILD_MODULES被设置为1。

规则的命令是一个冒号命令”:”,冒号(:)命令是bash的内建命令,通常把它看作true命令。
bash的help解释(help :)为:
No effect; the command does nothing. A zero exit code is returned.
(没有效果,该命令是空操作,退出状态总是0)。

再来看看builtin-target和lib-target的定义:
  1. ifneq ($(strip $(lib-y) $(lib-m) $(lib-n) $(lib-)),)
  2. lib-target := $(obj)/lib.a
  3. endif

  4. ifneq ($(strip $(obj-y) $(obj-m) $(obj-n) $(obj-) $(lib-target)),)
  5. builtin-target := $(obj)/built-in.o
  6. endif
来看lib-target的处理:
  1. ifdef lib-target
  2. quiet_cmd_link_l_target = AR $@
  3. cmd_link_l_target = rm -f $@; $(AR) rcs $@ $(lib-y)

  4. $(lib-target): $(lib-y) FORCE
  5.     $(call if_changed,link_l_target)

  6. targets += $(lib-target)
  7. endif
上面的规则表明构建系统会使用归档工具将变量 lib-y 中列出的所有对象文件归档成lib.a文件。
而在整个 Linux内核代码树中,只有两类(个)目录下的 Kbuild/Makefile 包含有对 lib-y的定义,
一个是内河代码树下的lib/目录,另外一个arch/$(ARCH)/lib/目录。

再来看builtin-target的处理:
  1. # Do section mismatch analysis for each module/built-in.o
    ifdef CONFIG_DEBUG_SECTION_MISMATCH
      cmd_secanalysis = ; scripts/mod/modpost $@
    endif

  2. ifdef builtin-target
  3. quiet_cmd_link_o_target = LD $@
  4. # If the list of objects to link is empty, just create an empty built-in.o
  5. cmd_link_o_target = $(if $(strip $(obj-y)),\
  6.          $(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) \
  7.          $(cmd_secanalysis),\
  8.          rm -f $@; $(AR) rcs $@)

  9. $(builtin-target): $(obj-y) FORCE
  10.     $(call if_changed,link_o_target)

  11. targets += $(builtin-target)
  12. endif # builtin-target
所以会编译出每个目录下的lib.a,build-in.o和obj-m。


生成vmlinux

内核构建系统之所以要在链接 vmlinux 之前,去链接出vmlinux.o。

其原因并不是要将 vmlinux.o 链接进 vmlinux,而是要在链接 vmlinux.o 的过程中做完两个动作:

a) elf section 是否 mis-match 的检查;

b) 生成内核导出符号文件 Module.symvers(Symbol version dump文件);

Modules.symvers 文件,其中包含内核及内部模块所导出的各种符号及其相关CRC校验值。

对于 vmlinux.o 目标,在 顶层 Makefile 中,有这样的规则定义:

  1. modpost-init := $(filter-out init/built-in.o, $(vmlinux-init))
  2. vmlinux.o: $(modpost-init) $(vmlinux-main) FORCE
  3.         $(call if_changed_rule,vmlinux-modpost)

该规则命令中的 if_changed_rule,我们前面已经介绍过。这里构建系统要调用 rule_vmlinux-modpost 变量所定义的命令。变量 rule_vmlinux-modpost 定义在顶层Makefile文件中:

  1. # Do modpost on a prelinked vmlinux. The finally linked vmlinux has
  2. # relevant sections renamed as per the linker script.
  3. quiet_cmd_vmlinux-modpost = LD $@
  4.       cmd_vmlinux-modpost = $(LD) $(LDFLAGS) -r -o $@ \
  5.          $(vmlinux-init) --start-group $(vmlinux-main) --end-group \
  6.          $(filter-out $(vmlinux-init) $(vmlinux-main) FORCE ,$^)
  7. define rule_vmlinux-modpost
  8.         :
  9.         +$(call cmd,vmlinux-modpost)
  10.         $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost $@
  11.         $(Q)echo 'cmd_$@ := $(cmd_vmlinux-modpost)' > $(dot-target).cmd
  12. endef

变量 rule_vmlinux-modpost 中定义的命令中,一开始就是调用 cmd_vmlinux-modpost 变量所定义的命令来链接出 vmlinux.o 目标。紧接着,构建系统就用 scripts/Makefile.modpost来调用 make。最后,它将构建 vmlinux.o 目标的命令保存到 .vmlinux.o.cmd 文件中。

注意看第二条make命令中的目标为 $@,也就是说要make vmlinux.o

我们看 Makefile.modpost 中对 vmlinux.o 的定义:

  1. quiet_cmd_kernel-mod = MODPOST $@
  2.       cmd_kernel-mod = $(modpost) $@
  3.  
  4. vmlinux.o: FORCE
  5.         @rm -fr $(kernelmarkersfile)
  6.         $(call cmd,kernel-mod)

而 modpost 变量被定义成这样:

  1. modpost = scripts/mod/modpost \
  2.  $(if $(CONFIG_MODVERSIONS),-m) \
  3.  $(if $(CONFIG_MODULE_SRCVERSION_ALL),-a,) \
  4.  $(if $(KBUILD_EXTMOD),-i,-o) $(kernelsymfile) \
  5.  $(if $(KBUILD_EXTMOD),-I $(modulesymfile)) \
  6.  $(if $(KBUILD_EXTRA_SYMBOLS), $(patsubst %, -e %,$(KBUILD_EXTRA_SYMBOLS))) \
  7.  $(if $(KBUILD_EXTMOD),-o $(modulesymfile)) \
  8.  $(if $(CONFIG_DEBUG_SECTION_MISMATCH),,-S) \
  9.  $(if $(CONFIG_MARKERS),-K $(kernelmarkersfile)) \
  10.  $(if $(CONFIG_MARKERS),-M $(markersfile)) \
  11.  $(if $(KBUILD_EXTMOD)$(KBUILD_MODPOST_WARN),-w) \
  12.  $(if $(cross_build),-c)

可以看出,其中包含有很多条件的判断。由于编译 vmlinux.o 时, CONFIG_MODVERSIONS,CONFIG_MODULE_SRCVERSION_ALL,KBUILD_EXTMOD,

KBUILD_EXTRA_SYMBOLS,CONFIG_MARKERS,

CONFIG_DEBUG_SECTION_MISMATCH 等等都没定义,

而我们做的是交叉编译,也就是 cross_build 则被赋值1。

所以在 scripts/Makefile.modpost 中,处理 vmlinux.o 的命令就是:

  1. scripts/mod/modpost -o /home/yihect/linux-2.6.31/Module.symvers -S -c vmlinux.o

(modpost的作用)


这个命令做的就是用工具 modpost 来解析 vmlinux.o 对象文件

并将基本内核导出的所有符号都记录到文件 Module.symvers 中去

当然,这个命令附带完成的,还有前面所说到的检查 sections 是否 mis match 的工作

你此时打开该文件后看到的符号CRC值都是0x00000000,那是因为你在配置的时并没有设置

CONFIG_MODVERSIONS。

一旦设置过这个配置选项,就意味着打开了内核的 Module versioning功能。

Module versioning 功能应用在我们使用模块的场合。

它会以每个导出符号的C原型声明作为输入,计算出对应的CRC校验值,保存在文件 Module.symvers 中。

如此一来,内核在后面要加载使用模块的时候,会两相比较模块中的CRC值和保存下来的CRC值,

如果发现不相等,内核就拒绝加载这个模块。

在编译完 vmlinux.o 后,在链接 vmlinux 之前,构建系统还要处理目标 $(kallsyms.o)。

kallsyms

在2.6版的内核中,为了更方便的调试内核代码,开发者考虑将内核代码中所有函数以及所有非栈变量的地址抽取出来,形成是一个简单的数据块(data blob),并将此链接进 vmlinux 中去。如此,在需要的时候,内核就可以将符号地址信息以及符号名称都显示出来,方便开发者对内核代码的调试。完成这一地址抽取+数据快组织封装功能的相关 子系统就称之为 kallsyms。

抽取函数和非堆栈变量的地址对内核构建系统来说比较简单,只需要使用 nm 工具即可,我们后面会看到。内核使用这个工具的输出作为输入,来调用主机工具程序 scripts/kallsyms,从而生成一个汇编 程序文件。在这个汇编程序文件的数据段中,定义有若干个标号(你可以将其理解成用来存储数据的C数组或数据结构)。在这些标号下面就存储有前面取到的函数 /变量地址和对应的名称。所以,很自然的,前面所谓的 data blog 其实就是这个汇编文件编译后的对象文件。构建系统将其链接到 vmlinux 基本内核中。

上面是一般的原理。在内核构建系统中,真正得到正确的汇编文件是一个两遍的过程。

第一遍得到的名为 ./.tmp_kallsyms1.S,

第二遍为 ./.tmp_kallsyms2.S。

两个文件格式完全一样,不同的时其中包含的函数/变量地址和名称等等

我们先来看看这个汇编文件的格式,大致是这样的(我这里重在列出那些标号,具体数据都省略掉):

tmp_kallsyms 汇编文件的内容

从上面的汇编代码中可以看出,有六个汇编标号(也是全局变量)的定义,分别是 kallsyms_addresses,kallsyms_num_syms,kallsyms_names,

kallsyms_markers,kallsyms_token_table 和 kallsyms_token_index。

这些变量的具体用法,我们这里先不予关心,因为已经超出本文的讨论范围。

你目前只需要知道Linux内核通过 它们来保存函数/变量地址和对应名称之间的mapping即可。

因为在这里定义的时候,它们都是全局的。

所以在C代码里面,只需要做一下 extern 声明就可以直接引用它们,

这些 extern 声明放在文件kernel/kallsyms.c 中,具体代码如下:

  1. /*
  2.  * These will be re-linked against their real values
  3.  * during the second link stage.
  4.  */
  5. extern const unsigned long kallsyms_addresses[] __attribute__((weak));
  6. extern const u8 kallsyms_names[] __attribute__((weak));
  7.  
  8. /*
  9.  * Tell the compiler that the count isn't in the small data section if the arch
  10.  * has one (eg: FRV).
  11.  */
  12. extern const unsigned long kallsyms_num_syms
  13. __attribute__((weak, section(".rodata")));
  14.  
  15. extern const u8 kallsyms_token_table[] __attribute__((weak));
  16. extern const u16 kallsyms_token_index[] __attribute__((weak));
  17.  
  18. extern const unsigned long kallsyms_markers[] __attribute__((weak))

这里需要引起注意的是,上面声明中都使用了 __attribute__((weak))。

我们还是来看看构建系统是如何处理kallsyms 的,我们先看看变量 kallsyms.o 的定义

  1. ifdef CONFIG_KALLSYMS_EXTRA_PASS
  2. last_kallsyms := 3
  3. else
  4. last_kallsyms := 2
  5. endif
  6.  
  7. kallsyms.o := .tmp_kallsyms$(last_kallsyms).o

默认情况下,CONFIG_KALLSYMS_EXTRA_PASS 是不会被配置的,因此 last_kallsyms 被默认设为 2,

给它赋值为3只是为了更方便调试kallsyms系统代码来的。所以kallsyms.o变量指代的就是.tmp_kallsyms2.o

Module symvers 文件的内容

由这个依赖链表出发,可以很明显的看出是一个两遍的过程。

只不过前后两遍的过程顺序是逆着依赖关系来的,右边为第一遍,左边为第二遍。

每一遍都是以 先生成一个内核映像(.tmp_vmlinux*)出发,

用nm/kallsyms来生成汇编程序文件(.tmp_kallsyms*.S),并最终以编 译此汇编文件产生对象文件为结束。

好了,知道这些后,我们再列出顶层 Makefile 中的代码来就比较好懂了:

  1. # Update vmlinux version before link
  2. # Use + in front of this rule to silent warning about make -j1
  3. # First command is ':' to allow us to use + in front of this rule
  4. cmd_ksym_ld = $(cmd_vmlinux__)
  5. define rule_ksym_ld
  6.         :
  7.         +$(call cmd,vmlinux_version)
  8.         $(call cmd,vmlinux__)
  9.         $(Q)echo 'cmd_$@ := $(cmd_vmlinux__)' > $(@D)/.$(@F).cmd
  10. endef
  11.  
  12. # Generate .S file with all kernel symbols
  13. quiet_cmd_kallsyms = KSYM $@
  14.       cmd_kallsyms = $(NM) -n $ $@
  15.  
  16. .tmp_kallsyms1.o .tmp_kallsyms2.o .tmp_kallsyms3.o: %.o: %.S scripts FORCE
  17.         $(call if_changed_dep,as_o_S)
  18.  
  19. .tmp_kallsyms%.S: .tmp_vmlinux% $(KALLSYMS)
  20.         $(call cmd,kallsyms)
  21.  
  22. # .tmp_vmlinux1 must be complete except kallsyms, so update vmlinux version
  23. .tmp_vmlinux1: $(vmlinux-lds) $(vmlinux-all) FORCE
  24.         $(call if_changed_rule,ksym_ld)
  25.  
  26. .tmp_vmlinux2: $(vmlinux-lds) $(vmlinux-all) .tmp_kallsyms1.o FORCE
  27.         $(call if_changed,vmlinux__)
  28.  
  29. .tmp_vmlinux3: $(vmlinux-lds) $(vmlinux-all) .tmp_kallsyms2.o FORCE
  30.         $(call if_changed,vmlinux__)

注意,处理目标 .tmp_vmlinux1 的命令并非直接调用 cmd_vmlinux__ ,而是调用了 rule_ksym_ld。

在 rule_ksym_ld中,它先用 cmd_vmlinux_version 去更新基本内核的链接次数

也就是 init/version.o 中的版本号,具体代码为:

  1. # Generate new vmlinux version
  2. quiet_cmd_vmlinux_version = GEN .version
  3.       cmd_vmlinux_version = set -e; \
  4.         if [ ! -r .version ]; then \
  5.           rm -f .version; \
  6.           echo 1 >.version; \
  7.         else \
  8.           mv .version .old_version; \
  9.           expr 0$$(cat .old_version) + 1 >.version; \
  10.         fi; \
  11.         $(MAKE) $(build)=init

在第一遍生成的内核基本映像 .tmp_vmlinux1 中,实际上已经有对上面提到的六个kallsyms_*变量的的引用,

只不过那是weak链接,意味着在链接时这些变量即使没有没有定义也没有关 系。

.tmp_vmlinux1 生成后,就可以生成 .tmp_kallsyms1.S 了,所用的命令为:cmd_kallsyms

假设定义了CONFIG_KALLSYMS_ALL,所以简化一下,生成 .tmp_kallsyms1.S 的命令就是:

arm-linux-nm -n .tmp_vmlinux1 | scripts/kallsyms --all-symbols > .tmp_kallsyms1.S

生成 .tmp_kallsyms1.S 后,内核中所有函数和非堆栈变量的地址及名称也都已经保存在汇编程序中了

(也就是上面汇编程序中省略掉的部分)。

将这个汇编文件编译成对象文件后,勾建系 统就着手进行第二个阶段,开始链接第二个基本内核映像

.tmp_vmlinux2 了。和 .tmp_vmlinux1 不同的是,.tmp_vmlinux2 将 .tmp_kallsyms1.o 也链接进去了

注意,链接成功 .tmp_vmlinux2 后,其中包含的部分函数和部分非堆栈变量的地址就发生了变化。为什么?

很简单,在一个排好的队伍中间插进去几个人,那后面原有那些人的序号就会因增加不同 数目而发生改变。

这个时候,这些新地址与记录在 .tm_kallsyms1.o 中的对应地址就不一样。那么以哪个为准?

自然是这些新地址,别忘了,它们是因为链接进 kallsyms 而发生改变的,我们就是要链接在一起的效果。

既然发生了地址改变,我们就必须想办法重新生成一次汇编程序 .tmp_kallsyms2.S。

这个汇编程序和前面的那个 .tmp_kallsyms1.S 相比。在文件尺寸上没有差别,所不同的只是部分地址罢了。

所以结论就是, 这个对象文件 .tmp_kallsyms2.o 就是我们最后要得到的 data blog,即 $(kallsyms.o) 目标。

回到处理vmlinux的规则上面来。至此,目标 vmlinux 的所有依赖都处理完毕了,

接下来构建系统就会执行该规则内如下的命令:

  1. ifdef CONFIG_HEADERS_CHECK
  2.         $(Q)$(MAKE) -f $(srctree)/Makefile headers_check
  3. endif
  4. ifdef CONFIG_SAMPLES
  5.         $(Q)$(MAKE) $(build)=samples
  6. endif
  7. ifdef CONFIG_BUILD_DOCSRC
  8.         $(Q)$(MAKE) $(build)=Documentation
  9. endif
  10.         $(call vmlinux-modpost)
  11.         $(call if_changed_rule,vmlinux__)
  12.         $(Q)rm -f .old_version

最重要的就是 $(call if_changed_rule,vmlinux__) 一句了。这之前的 $(call vmlinux-modpost),

$(call vmlinux-modpost) 是条多余的没用的命令。

命令 $(call if_changed_rule,vmlinux__) 会调用 rule_vmlinux__ 变量所定义的命令,在顶层 Makefile 中找到 rule_vmlinux__ 的定义:

  1. # Generate System.map
  2. quiet_cmd_sysmap = SYSMAP
  3.       cmd_sysmap = $(CONFIG_SHELL) $(srctree)/scripts/mksysmap
  4.  
  5. # Link of vmlinux
  6. # If CONFIG_KALLSYMS is set .version is already updated
  7. # Generate System.map and verify that the content is consistent
  8. # Use + in front of the vmlinux_version rule to silent warning with make -j2
  9. # First command is ':' to allow us to use + in front of the rule
  10. define rule_vmlinux__
  11.         :
  12.         $(if $(CONFIG_KALLSYMS),,+$(call cmd,vmlinux_version))
  13.  
  14.         $(call cmd,vmlinux__)
  15.         $(Q)echo 'cmd_$@ := $(cmd_vmlinux__)' > $(@D)/.$(@F).cmd
  16.  
  17.         $(Q)$(if $($(quiet)cmd_sysmap), \
  18.           echo ' $($(quiet)cmd_sysmap) System.map' &&) \
  19.         $(cmd_sysmap) $@ System.map; \
  20.         if [ $$? -ne 0 ]; then \
  21.                 rm -f $@; \
  22.                 /bin/false; \
  23.         fi;
  24.         $(verify_kallsyms)
  25. endef

在 rule_vmlinux__ 变量一开始,构建系统会检查是否有定义过 CONFIG_KALLSYMS,如果没有定义过,它就使用 cmd_vmlinux_version 来递增链接版本。还记得么,如果定义过 CONFIG_KALLSYS,它又是在哪里递增版本的?rule_vmlinux__ 接下来才会用 cmd_vmlinux__ 去把 vmlinux 链接出来

在这里,我们倒是可以将链接时所使用的命令贴出来看一下:

链接生成 kallsyms.o 文件的命令

从上面链接 vmlinux 的命令可以看出,其使用的链接脚本是 .../arch/arm/kernel/vmlinux.lds。

注意上 面的链接将把 .../arch/arm/kernel/head.o 放在映像文件 vmlinux 的最前面,

这是由链接器脚本所规定的,后续其他文章中的分析可能会告诉你 head.o 正是整个 Linux 开始的地方。

接下来,它使用 cmd_sysmap 所定义的命令来生成基本内核符号表文件 System.map

在其中包含有所有内核符号以及它们的地址,

实际上前面用 kallsyms 包含在基本内核映像中的函数/变量地址及名称信息都等同于 System.map 中的内容,

我们以后对内核代码的分析过程会经常引用这个文件。

rule_vmlinux__ 最后会额外做一步,用 verify_kallsyms 来确认前面的 kallsyms 是否工作正常。

确认的方法是先拿前面的 .tmp_vmlinux2 重新生成一份新的map: .tmp_System.map。

接着构建系统会比较 .tmp_System.map 和 System.map,

如果不一致,那说明 kallsyms 子系统代码工作不正常,所以构建系统会建议你设置

CONFIG_KALLSYMS_EXTRA_PASS 来重新make vmlinux。verify_kallsyms 的定义为:

  1. define verify_kallsyms
  2.         $(Q)$(if $($(quiet)cmd_sysmap), \
  3.           echo ' $($(quiet)cmd_sysmap) .tmp_System.map' &&) \
  4.           $(cmd_sysmap) .tmp_vmlinux$(last_kallsyms) .tmp_System.map
  5.         $(Q)cmp -s System.map .tmp_System.map || \
  6.                 (echo Inconsistent kallsyms data; \
  7.                  echo Try setting CONFIG_KALLSYMS_EXTRA_PASS; \
  8.                  rm .tmp_kallsyms* ; /bin/false )
  9. endef

前面有说当在make 命令中不明确指定目标时,其使用的缺省目标是_all,而_all又依赖于all。
构建系统对all的处理又是按顺序处理三个目 标:vmlinux,zImage 和 modules。
那我们这里已经把 vmlinux 的处理讨论完毕,其结果是产生两个输出文件:
vmlinux 和 System.map,前者是基本内核的ELF映像,后者是基本内核符号表文件。

如何编译模块?
不管是内部模块,还是外部模块,其编译都要分两个阶段进行。
阶段一生成组成模块的对应 .o 文件和 .mod 文件,
阶段二要用 scripts/mod/modpost 来生成 .mod.c 文件,并将其编译成 .mod.o 对象文件,
最后将 .mod.o 连同前面的 .o 一起链接成 .ko 模块文件。


先在顶层 Makefile 中(框架中的E1部分)找到处理 modules 目标的规则:
  1. all: modules
  2.  
  3. # Build modules
  4. #
  5. # A module can be listed more than once in obj-m resulting in
  6. # duplicate lines in modules.order files. Those are removed
  7. # using awk while concatenating to the final file.
  8.  
  9. PHONY += modules
  10. modules: $(vmlinux-dirs) $(if $(KBUILD_BUILTIN),vmlinux)
  11.         $(Q)$(AWK) '!x[$$0]++' $(vmlinux-dirs:%=$(objtree)/%/modules.order) > $(objtree)/modules.order
  12.         @$(kecho) ' Building modules, stage 2.';
  13.         $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
  14.         $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.fwinst obj=firmware __fw_modbuild

上面显示 modules 目标依赖于 $(vmlinux-dirs)。

这种依赖就意味着内部模块处理的第一阶段就已经在处理 vmlinux-dirs 的过程中完成了。

前面对 vmlinux-dirs 的讨论过程也说了如何编译出构成模块的那些 .o 对象文件,以及如何生成 .mod 文件。

很显然,既然内部模块的第一阶段已经完成,那处理 modules 目标规则的命令部分就是来完成内部模块的第二阶段了。

命令部分中的第一行用一个awk调用来将各子目录中 modules.order 文件内容归集到顶层目录的 modules.order 文件中。该文件列出了构建系统构建内部模块的次序。

上面命令部分中最关键的就是接下来那一行:$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost。由于该命令没有指定make的目标,所以它会构建 Makefile.modpost 中的缺省目标 _modpost。而在同一个文件中查看一下 _modpost 的相关规则:
  1. PHONY := _modpost
  2. _modpost: __modpost
  3. ......
  4. # Stop after building .o files if NOFINAL is set. Makes compile tests quicker
  5. _modpost: $(if $(KBUILD_MODPOST_NOFINAL), $(modules:.ko:.o),$(modules))
_modpost 依赖于 __modpost。同时,如果有定义过KBUILD_MODPOST_NOFINAL,那么它还依赖于那些和模块名称对应的 .o 文件。打个比方,如果有两个对象文件 part1.o 和 part2.o组成一个模块 MyModule.ko,那么它就依赖于 MyModule.o 对象文件。另外如果没有定义过,那它还依赖于所有的内部模块。所以变量 KBUILD_MODPOST_NOFINAL 的定义就意味着我们只是生成 MyModule.o,而不要再继续从 MyModule.o 出发生成 MyModule.ko 模块。变量 modules 被这样定义:
  1. # Step 1), find all modules listed in $(MODVERDIR)/
  2. __modules := $(sort $(shell grep -h '\.ko' /dev/null $(wildcard $(MODVERDIR)/*.mod)))
  3. modules := $(patsubst %.o,%.ko, $(wildcard $(__modules:.ko=.o)))
这个定义用 grep 搜索目录 $(MODVERDIR)/ 中的所有 *.mod 文件,找出其中包含模块文件名称后缀 .ko 的那些行。效果上也就是等价于找出所有的内部模块名称,组成列表赋给 modules。还记得么?前面提到过,目录$(MODVERDIR)就是 .../.tmp_version/,其中存有模块处理第一阶段中生成的所有 .mod 文件。
如何生成的.mod文件呢?

  1. PHONY += __modpost
  2. __modpost: $(modules:.ko=.o) FORCE
  3. $(call cmd,modpost) $(wildcard vmlinux) $(filter-out FORCE,$^)
  4. $(Q)echo 'cmd_$@ := (call cmd,modpost) $(wildcard vmlinux) $(filter-out FORCE,$^)' > $(@D)/.$(@F).cmd
似曾相识对吧?没错,我们在前面讨论 vmlinux.o 的处理的时候,就已经碰到工具程序 .../scripts/mod/modpost 的使用了。只不过,那时候使用它的是变量 cmd_kernel-mod,而非cmd_modpost。当时,构建系统用来完成两个动作:mis-match section的检查和生成基本内核导出符号文件 Module.symvers,其中包含基本内核所导出的所有符号及CRC校验。那此处调用 .../scripts/mod/modpost 来做何用途呢?
上面处理 __modpost 规则中的命令实际上就是:

scripts/mod/modpost -o /home/yihect/linux-2.6.31/Module.symvers -S -c -s vmlinux MyModule.o YouModule.o HisModule.o ....

其中,命令后半部分包括省略号所表示的,是与各内部模块名称对应的 .o 文件。这个命令在这里主要也是要完成两项工作:

a) 解 析出 vmlinux以及各对应的 .o 文件内的符号,并重新将它们连同各自的CRC校验写入到顶层目录中的文件 Modules.symvers 内。所以最后该文件内不仅包含基本内核的符号及CRC校验,还包括各内部模块所导出的符号及CRC校验,在结果上是前面处理 vmlinux.o 时所生成的 Modules.symvers 的超集;

b) 针对各个内部模块,生成对应的 *.mod.c 文件。

生成 *.mod.c 文件的代码在 modpost.c 文件的main函数中:


  1. int main(int argc, char **argv)
  2. {
  3.         struct module *mod;
  4.         struct buffer buf = { };
  5.         //......
  6.         for (mod = modules; mod; mod = mod->next) {
  7.                 char fname[strlen(mod->name) + 10];
  8.  
  9.                 if (mod->skip)
  10.                         continue;
  11.  
  12.                 buf.pos = 0;
  13.  
  14.                 add_header(&buf, mod);
  15.                 add_staging_flag(&buf, mod->name);
  16.                 err |= add_versions(&buf, mod);
  17.                 add_depends(&buf, mod, modules);
  18.                 add_moddevtable(&buf, mod);
  19.                 add_srcversion(&buf, mod);
  20.  
  21.                 sprintf(fname, "%s.mod.c", mod->name);
  22.                 write_if_changed(&buf, fname);
  23.         }
  24.         //.......
  25.         return err;
  26. }
具体的生成代码就分布在不同的 add_* 函数当中.
为了完整的说明 *.mod.c 文件的内容,我们特意修改了 .../.config 配置文件,将 CONFIG_MODVERSIONS 及 CONFIG_MODULE_SRCVERSION_ALL 两变量设置为y。也就是打开了内核的 Module versioning 功能。我们列出文件 cfg80211.mod.c 的内容(有删减): Module symvers 文件的内容

该文件大部分的代码是定义一些变量,并将其放在三个不同的 elf section 内(后面构建系统会编译这个 .mod.c 形成对象文件,链接进 .ko):

a) 定 义struct module结构变量 __this_module,并将其放在 .gnu.linkonce.this_module section 中。在将模块加载进运行着的内核时,内核负责将这个对象加到内部的modules list中。modules list 是内核维护所有已加载模块的一个双向链表(更多请看:

b) 定 义 struct modversion_info 结构数组 ____versions,并将其放到 __versions sectiong 中。该数组中存放的都是该模块中使用到,但没被定义的符号,也就是所谓的 unresolved symbol,它们或在基本内核中定义,或在其他模块中定义,内核使用它们来做 Module versioning。注意其中的 module_layout 符号,这是一个 dummy symbol。内核使用它来跟踪不同内核版本关于模块处理的相关数据结构的变化。当一个模块在A版本的内核中编译后,又在另外一个B版本的内核中加载,如 果两个内核中处理modules的那些数据结构体定义发生变化了,那内核就拒绝继续做其他 Module versioning 工作,也就是拒绝加载模块。

c) 最后,.mod.c 中会将很多信息塞进 .modinfo section 中,包括:vermagic字符串,模块依赖信息,srcversion信息等等(还有其他很多信息)。我们以vermagic来举例分析。

MODULE_INFO(pppp, "qqq"); 那么经过C预处理,就会展开成:
  1. static const char __mod_pppp21[] __used __attribute__((section(".modinfo"),unused)) = "pppp" "=" "qqq";

实际上,就是定义了一个名为 __mod_pppp21 的字符数组,将其初始化成字符串 "pppp=qqq" 形式后放入 .modinfo section 中。

_modpost 要依赖于 $(modules),构建系统还要负责构建出各个内部模块(.ko)。
  1. $(modules): %.ko :%.o %.mod.o FORCE
  2.         $(call if_changed,ld_ko_o)
这是一条静态匹配规则,可以看出内部模块文件 *.ko 要依赖于同名的 *.o 和 同名的 *.mod.o 。
同名*.o已经在第一阶段处理 vmlinux-dirs 时准备妥当,但是同名*.mod.o还未生成,
所以构建系统必须用下面的规则来生成它:
  1. quiet_cmd_cc_o_c = CC $@
  2.       cmd_cc_o_c = $(CC) $(c_flags) $(KBUILD_CFLAGS_MODULE) $(CFLAGS_MODULE) \
  3.                    -c -o $@ $<

  4. $(modules:.ko=.mod.o): %.mod.o: %.mod.c FORCE
  5.         $(call if_changed_dep,cc_o_c)
由处理 $(modules) 的规则看出,生成 *.mod.o 后,构建系统使用变量 cmd_ld_ko_o 定义的命令来将同名*.o和同名*.mod.o链接成*.ko:
  1. # Step 6), final link of the modules
  2. quiet_cmd_ld_ko_o = LD [M] $@
  3. cmd_ld_ko_o = $(LD) -r $(LDFLAGS) $(LDFLAGS_MODULE) -o $@ \
  4. $(filter-out FORCE,$^)
 
总结以下Makefile.modpost的作用
    在script目录下有许多Makefile文件,由于各种情况;
    其中,Makefile.modpost用于module的生成。
    第一步:
    a) 编译驱动的每个.o文件。
    b)将每个.o文件链接到.o。
    c)在$(MODVERDIR)/生成一个.mod文件,列出.ko及每个.o文件。
 
    第二步:
    1)找出所有在$(MODVERDIR)/的modules。
    2)接着使用modpost
    3)为每个module创建.mod.c
    4)创建一个Module.symvers文件,保存了所有引出符号及其CRC校验。
    5)编译全部 .mod.c文件。
    6)链接所有的module成为一个文件。
 
    第三步:替换module里一些ELF段,包括:
    Version magic (see include/vermagic.h for full details)
    Kernel release
    SMP is CONFIG_SMP
    PREEMPT is CONFIG_PREEMPT
    GCC Version
    Module info
    Module version (MODULE_VERSION)
    Module alias'es (MODULE_ALIAS)
    Module license (MODULE_LICENSE)
    See include/linux/module.h for more details
    第四步:
    Step 4 is solely used to allow module versioning in external modules,
    where the CRC of each module is retrieved from the Module.symers file.
    KBUILD_MODPOST_WARN can be set to avoid error out in case of undefined
    symbols in the final module linking stage
    KBUILD_MODPOST_NOFINAL can be set to skip the final link of modules.
    This is solely usefull to speed up test compiles
    具体清参考Makefile.modpost文件。

阅读(2864) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~