对技术执着
分类: LINUX
2015-03-14 17:24:44
原文地址:Linux内核构建系统之六 作者:kyok520
回到处理 vmlinux 的那条规则上面来,在处理好 $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main)等目标后,构建系统接下来要处理的就是 vmlinux.o 和 $(kallsyms.o) 了。
内核构建系统之所以要在链接 vmlinux 之前,去链接出vmlinux.o。其原因并不是要将 vmlinux.o 链接进 vmlinux,而是要在链接 vmlinux.o 的过程中做完两个动作:
a) elf section 是否 mis-match 的检查;
b) 生成内核导出符号文件 Module.symvers(Symbol version dump文件);
其中第一个动作,由于明白它需要很多elf文件格式的知识,而本文重在解释内核构建系统的原理与实现,所以要在这里详细讲解就显得不妥,等以后有机会了再写文章来讨论。有兴趣的同学,可以先行查看 scripts/mod/modpost.c 中的代码。而第二个动作中提到的 Modules.symvers 文件,其中包含内核及内部模块所导出的各种符号及其相关CRC校验值。构建系统会在编译 vmlinux.o 的过程中将基本内核(vmlinux)所导除的符号记录在这个文件中,在后面讨论内部模块的第二步处理的时候,我们会发现它也会将各内部模块所导出的符号记录在这个文件中。
这里提到的所谓符号,你可以先将其理解成是基本内核或者内部模块导出来的,供其他人(通常是驱动程序)使用的函数。等你以后开发驱动程序进行调试的时候,你会发现有这么一个dump文件会有多么方便。构建系统在编译外部模块的时候也要使用这个文件。
为了不脱离本文主题,我在下面讨论vmlinu.o编译的过程中,特意将上面两个动作中和内核构建无关的部分略去,以后我们会专门写另外的文章来讨论。
对于 vmlinux.o 目标,在 顶层 Makefile 中,有这样的规则定义:
modpost-init := $(filter-out init/built-in.o, $(vmlinux-init)) vmlinux.o: $(modpost-init) $(vmlinux-main) FORCE $(call if_changed_rule,vmlinux-modpost)
该规则命令中的 if_changed_rule,我们前面已经介绍过。这里构建系统要调用 rule_vmlinux-modpost 变量所定义的命令。变量 rule_vmlinux-modpost 定义在顶层Makefile文件中:
# Do modpost on a prelinked vmlinux. The finally linked vmlinux has # relevant sections renamed as per the linker script. quiet_cmd_vmlinux-modpost = LD $@ cmd_vmlinux-modpost = $(LD) $(LDFLAGS) -r -o $@ \ $(vmlinux-init) --start-group $(vmlinux-main) --end-group \ $(filter-out $(vmlinux-init) $(vmlinux-main) FORCE ,$^) define rule_vmlinux-modpost : +$(call cmd,vmlinux-modpost) $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost $@ $(Q)echo 'cmd_$@ := $(cmd_vmlinux-modpost)' > $(dot-target).cmd 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 的定义:
quiet_cmd_kernel-mod = MODPOST $@ cmd_kernel-mod = $(modpost) $@ vmlinux.o: FORCE @rm -fr $(kernelmarkersfile) $(call cmd,kernel-mod)
而 modpost 变量被定义成这样:
modpost = scripts/mod/modpost \ $(if $(CONFIG_MODVERSIONS),-m) \ $(if $(CONFIG_MODULE_SRCVERSION_ALL),-a,) \ $(if $(KBUILD_EXTMOD),-i,-o) $(kernelsymfile) \ $(if $(KBUILD_EXTMOD),-I $(modulesymfile)) \ $(if $(KBUILD_EXTRA_SYMBOLS), $(patsubst %, -e %,$(KBUILD_EXTRA_SYMBOLS))) \ $(if $(KBUILD_EXTMOD),-o $(modulesymfile)) \ $(if $(CONFIG_DEBUG_SECTION_MISMATCH),,-S) \ $(if $(CONFIG_MARKERS),-K $(kernelmarkersfile)) \ $(if $(CONFIG_MARKERS),-M $(markersfile)) \ $(if $(KBUILD_EXTMOD)$(KBUILD_MODPOST_WARN),-w) \ $(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 的命令就是:
scripts/mod/modpost -o /home/yihect/linux-2.6.31/Module.symvers -S -c vmlinux.o
这个命令做的就是用工具 modpost 来解析 vmlinux.o 对象文件,并将基本内核导出的所有符号都记录到文件 Module.symvers 中去。当然,这个命令附带完成的,还有前面所说到的检查 sections 是否 mis match 的工作。你可以先浏览看看 scripts/mod/modpost.c 中的代码。打开文件 Module.symvers,你会发现针对每个内核导出的符号(symbol),都会有一行,其格式和举例如下:
等一下,你此时打开该文件后看到的符号CRC值都是0x00000000,那是因为你在配置的时并没有设置 CONFIG_MODVERSIONS。一旦设置过这个配置选项,就意味着打开了内核的 Module versioning功能。Module versioning 功能应用在我们使用模块的场合。如果Module versioning功能被打开的话,它会以每个导出符号的C原型声明作为输入,计算出对应的CRC校验值,保存在文件 Module.symvers 中。如此一来,内核在后面要加载使用模块的时候,会两相比较模块中的CRC值和保存下来的CRC值,如果发现不相等,内核就拒绝加载这个模块。
在编译完 vmlinux.o 后,在链接 vmlinux 之前,构建系统还要处理目标 $(kallsyms.o)。这里先说说所谓 kallsyms 是做什么用的。
在2.6版的内核中,为了更方便的调试内核代码,开发者考虑将内核代码中所有函数以及所有非栈变量的地址抽取出来,形成是一个简单的数据块(data blob),并将此链接进 vmlinux 中去。如此,在需要的时候,内核就可以将符号地址信息以及符号名称都显示出来,方便开发者对内核代码的调试。完成这一地址抽取+数据快组织封装功能的相关子系统就称之为 kallsyms。反之,如果没有 kallsyms 的帮助,内核只能将十六进制的符号地址呈现给外界,因为它能理解的只有符号地址,而并不包括人类可读的符号名称。
经常使用 Windows 的人都知道所谓的蓝屏是怎么回事,那是系统出了致命问题,而不能继续运行下去而show出的一个蓝色屏。那么对 Linux 来说,也会有致命错误的出现,如果这种错误使得Linux不可继续运行,那么Linux就会显示类似下面这样的屏幕内容(以运行在PowerPC架构下的Linux来举例,其他架构也差不多)以dump出出现错误那时刻的系统状态。这种现象就是所谓的Oops,也就Linux下的蓝屏。
上面出现的Oops消息中,显示了出错时的CPU各寄存器的值,以及以 "Call trace:" 一行开始的C函数调用栈。注意其中除了显示在中括号内的地址,还显示了函数的名称,这就是受助于 kallsyms 的结果,否则它只能显示地址信息。要在一个内核中启用 kallsyms 功能,你必须设置 CONFIG_KALLSYMS 选项为y;如果你要在 kallsyms 中包含全部符号信息,必须设置 CONFIG_KALLSYMS_ALL 为y。
我们既然明白了 kallsyms 的大致作用,那么内核是如何抽取函数和非堆栈变量的地址,以及如何将它们连同对应的名称保存到一个 data blog中去以便最后链接到 vmlinux 中去的。抽取函数和非堆栈变量的地址对内核构建系统来说比较简单,只需要使用 nm 工具即可,我们后面会看到。内核使用这个工具的输出作为输入,来调用主机工具程序 scripts/kallsyms,从而生成一个汇编程序文件。在这个汇编程序文件的数据段中,定义有若干个标号(你可以将其理解成用来存储数据的C数组或数据结构)。在这些标号下面就存储有前面取到的函数/变量地址和对应的名称。所以,很自然的,前面所谓的 data blog 其实就是这个汇编文件编译后的对象文件。构建系统将其链接到 vmlinux 基本内核中。
上面是一般的原理。在内核构建系统中,真正得到正确的汇编文件是一个两遍的过程。第一遍得到的名为 ./.tmp_kallsyms1.S,第二遍为 ./.tmp_kallsyms2.S。两个文件格式完全一样,不同的时其中包含的函数/变量地址和名称等等。我们先来看看这个汇编文件的格式,大致是这样的(我这里重在列出那些标号,具体数据都省略掉):
从上面的汇编代码中可以看出,有六个汇编标号(也是全局变量)的定义,分别是 kallsyms_addresses,kallsyms_num_syms,kallsyms_names,kallsyms_markers,kallsyms_token_table 和 kallsyms_token_index。这些变量的具体用法,我们这里先不予关心,因为已经超出本文的讨论范围。你目前只需要知道Linux内核通过它们来保存函数/变量地址和对应名称之间的mapping即可。因为在这里定义的时候,它们都是全局的。所以在C代码里面,只需要做一下 extern 声明就可以直接引用它们,这些 extern 声明放在文件kernel/kallsyms.c 中,具体代码如下:
/* * These will be re-linked against their real values * during the second link stage. */ extern const unsigned long kallsyms_addresses[] __attribute__((weak)); extern const u8 kallsyms_names[] __attribute__((weak)); /* * Tell the compiler that the count isn't in the small data section if the arch * has one (eg: FRV). */ extern const unsigned long kallsyms_num_syms __attribute__((weak, section(".rodata"))); extern const u8 kallsyms_token_table[] __attribute__((weak)); extern const u16 kallsyms_token_index[] __attribute__((weak)); extern const unsigned long kallsyms_markers[] __attribute__((weak))
这里需要引起注意的是,上面声明中都使用了 __attribute__((weak))。这个作何解释,我博客的其他文章中已有所介绍,这里就不再叙述。好,我们还是来看看构建系统是如何处理kallsyms 的,我们先看看变量 kallsyms.o 的定义:
ifdef CONFIG_KALLSYMS_EXTRA_PASS last_kallsyms := 3 else last_kallsyms := 2 endif kallsyms.o := .tmp_kallsyms$(last_kallsyms).o
默认情况下,CONFIG_KALLSYMS_EXTRA_PASS 是不会被配置的,因此 last_kallsyms 被默认设为 2,给它赋值为3只是为了更方便调试kallsyms系统代码来的。所以kallsyms.o变量指代的就是.tmp_kallsyms2.o。好,知道 kallsyms.o 的定义后,我们由此出发构造一个目标依赖关系链表(a-->b,表示目标a依赖于目标b),你可以据此来理解:
由这个依赖链表出发,可以很明显的看出是一个两遍的过程。只不过前后两遍的过程顺序是逆着依赖关系来的,右边为第一遍,左边为第二遍。每一遍都是以先生成一个内核映像(.tmp_vmlinux*)出发,用nm/kallsyms来生成汇编程序文件(.tmp_kallsyms*.S),并最终以编译此汇编文件产生对象文件为结束。
好了,知道这些后,我们再列出顶层 Makefile 中的代码来就比较好懂了:
# Update vmlinux version before link # Use + in front of this rule to silent warning about make -j1 # First command is ':' to allow us to use + in front of this rule cmd_ksym_ld = $(cmd_vmlinux__) define rule_ksym_ld : +$(call cmd,vmlinux_version) $(call cmd,vmlinux__) $(Q)echo 'cmd_$@ := $(cmd_vmlinux__)' > $(@D)/.$(@F).cmd endef # Generate .S file with all kernel symbols quiet_cmd_kallsyms = KSYM $@ cmd_kallsyms = $(NM) -n $ $@ .tmp_kallsyms1.o .tmp_kallsyms2.o .tmp_kallsyms3.o: %.o: %.S scripts FORCE $(call if_changed_dep,as_o_S) .tmp_kallsyms%.S: .tmp_vmlinux% $(KALLSYMS) $(call cmd,kallsyms) # .tmp_vmlinux1 must be complete except kallsyms, so update vmlinux version .tmp_vmlinux1: $(vmlinux-lds) $(vmlinux-all) FORCE $(call if_changed_rule,ksym_ld) .tmp_vmlinux2: $(vmlinux-lds) $(vmlinux-all) .tmp_kallsyms1.o FORCE $(call if_changed,vmlinux__) .tmp_vmlinux3: $(vmlinux-lds) $(vmlinux-all) .tmp_kallsyms2.o FORCE $(call if_changed,vmlinux__)
注意,处理目标 .tmp_vmlinux1 的命令并非直接调用 cmd_vmlinux__ ,而是调用了 rule_ksym_ld。在 rule_ksym_ld中,它先用 cmd_vmlinux_version 去更新基本内核的链接次数,也就是 init/version.o 中的版本号,具体代码为:
# Generate new vmlinux version quiet_cmd_vmlinux_version = GEN .version cmd_vmlinux_version = set -e; \ if [ ! -r .version ]; then \ rm -f .version; \ echo 1 >.version; \ else \ mv .version .old_version; \ expr 0$$(cat .old_version) + 1 >.version; \ fi; \ $(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.S 编译后的对象文件.tmp_kallsyms2.o去编译一个新的基本内核,那这个基本内核中所具有的函数/变量地址就将不会再发生变化。所以结论就是,这个对象文件 .tmp_kallsyms2.o 就是我们最后要得到的 data blog,即 $(kallsyms.o) 目标。上面构建系统的代码中也准备了 .tmp_kallsyms3.S,.tmp_kallsyms3.o和.tmp_vmlinux3等等,但实际上,这是为了调试 kallsyms 系统代码用的。如果真的用到了这第三遍,那就证明 scripts/kallsyms.c 文件内的代码出问题了。
回到处理vmlinux的规则上面来。至此,目标 vmlinux 的所有依赖都处理完毕了,接下来构建系统就会执行该规则内如下的命令:
ifdef CONFIG_HEADERS_CHECK $(Q)$(MAKE) -f $(srctree)/Makefile headers_check endif ifdef CONFIG_SAMPLES $(Q)$(MAKE) $(build)=samples endif ifdef CONFIG_BUILD_DOCSRC $(Q)$(MAKE) $(build)=Documentation endif $(call vmlinux-modpost) $(call if_changed_rule,vmlinux__) $(Q)rm -f .old_version
对于 s3c2410_defconfig 的默认配置来说,CONFIG_HEADERS_CHECK/CONFIG_SAMPLES/CONFIG_BUILD_DOCSRC 等都没有设置,所以这些命令中,最重要的就是 $(call if_changed_rule,vmlinux__) 一句了。这之前的 $(call vmlinux-modpost),因为我始终都找不到变量 vmlinux-modpost 的定义,所以我认为 $(call vmlinux-modpost) 是条多余的没用的命令。在内核代码里,这样的多余现象是时常有的。在实际查找过程中,我甚至还像下面这样插入一条 echo 测试过,结果显示 vmlinux-modpost 没有被定义。
$(call vmlinux-modpost) echo 'vmlinux-modpost := $(vmlinux-modpost)' > vmlinux-modpost.cmd $(call if_changed_rule,vmlinux__) $(Q)rm -f .old_version
命令 $(call if_changed_rule,vmlinux__) 会调用 rule_vmlinux__ 变量所定义的命令,在顶层 Makefile 中找到 rule_vmlinux__ 的定义:
# Generate System.map quiet_cmd_sysmap = SYSMAP cmd_sysmap = $(CONFIG_SHELL) $(srctree)/scripts/mksysmap # Link of vmlinux # If CONFIG_KALLSYMS is set .version is already updated # Generate System.map and verify that the content is consistent # Use + in front of the vmlinux_version rule to silent warning with make -j2 # First command is ':' to allow us to use + in front of the rule define rule_vmlinux__ : $(if $(CONFIG_KALLSYMS),,+$(call cmd,vmlinux_version)) $(call cmd,vmlinux__) $(Q)echo 'cmd_$@ := $(cmd_vmlinux__)' > $(@D)/.$(@F).cmd $(Q)$(if $($(quiet)cmd_sysmap), \ echo ' $($(quiet)cmd_sysmap) System.map' &&) \ $(cmd_sysmap) $@ System.map; \ if [ $$? -ne 0 ]; then \ rm -f $@; \ /bin/false; \ fi; $(verify_kallsyms) endef
在 rule_vmlinux__ 变量一开始,构建系统会检查是否有定义过 CONFIG_KALLSYMS,如果没有定义过,它就使用 cmd_vmlinux_version 来递增链接版本。还记得么,如果定义过 CONFIG_KALLSYS,它又是在哪里递增版本的?rule_vmlinux__ 接下来才会用 cmd_vmlinux__ 去把 vmlinux 链接出来。在这里,我们倒是可以将链接时所使用的命令贴出来看一下:
从上面链接 vmlinux 的命令可以看出,其使用的链接脚本是 .../arch/arm/kernel/vmlinux.lds。关于链接脚本的相关知识,我们会另外写一篇文章来讨论,我们这里不予详细说明。注意上面的链接将把 .../arch/arm/kernel/head.o 放在映像文件 vmlinux 的最前面,这是由链接器脚本所规定的,后续其他文章中的分析可能会告诉你 head.o 正是整个 Linux 开始的地方。
从 rule_vmlinux__ 的定义看,接下来,它使用 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 的定义为:
define verify_kallsyms $(Q)$(if $($(quiet)cmd_sysmap), \ echo ' $($(quiet)cmd_sysmap) .tmp_System.map' &&) \ $(cmd_sysmap) .tmp_vmlinux$(last_kallsyms) .tmp_System.map $(Q)cmp -s System.map .tmp_System.map || \ (echo Inconsistent kallsyms data; \ echo Try setting CONFIG_KALLSYMS_EXTRA_PASS; \ rm .tmp_kallsyms* ; /bin/false ) endef
前面有说当在make 命令中不明确指定目标时,其使用的缺省目标是_all,而_all又依赖于all。构建系统对all的处理又是按顺序处理三个目标:vmlinux,zImage 和 modules。那我们这里已经把 vmlinux 的处理讨论完毕,其结果是产生两个输出文件:vmlinux 和 System.map,前者是基本内核的ELF映像,后者是基本内核符号表文件。接下来,我们先将对 zImage 的讨论搁置起来,先来看构建系统对 modules 的处理。