linux内核编译学习笔记
linux内核由于庞大的代码量和复杂的代码结构,使用通用的makefile形式不仅存在很大的工作量,而且内核的可配置性不好,每次裁剪模块都需要深入到每一层的目录结构修改makefile,并不现实。所以linux提供了一套configure和makefile体系,根据config中的配置操作生成各个子目录下的makefile,决定哪些文件参与编译。内核本身包含了顶层makefile文件,该文件指示了通用的框架。而各个子目录下的makefile文件也不像传统的makefile文件编写格式,它们是结合scripts/目录下的一系列规则文件使用的,只需要指明需要参与编译的子目录/文件即可。在学习了解内核编译体系结构时,因之前基础薄弱,而且对makefile的语法并不熟悉,所以感觉晦涩难懂。后来学习参考同行的博客,对整个脉络有了整体的思路,对makefile体系架构有了更深的了解。
本文重点记录已经学习到的知识,从最终生成的image文件反推,一步一步看如何生成了image文件。首先需要知道,make命令肯定是要执行顶层目录下的makefile文件。
一、makefile文件系统结构
1、scripts/目录下的makefile规则文件
scripts目录下包含makefile规则文件。这些文件相当于制定了一套规则,会解析子目录下的makefile文件。由顶层makefile文件、/sripts子目录下的makefile规则文件和子目录下的makefile文件共同形成了内核的makefile体系。
2、顶层Makefile文件
在顶层目录下,执行make命令生成内核镜像文件,必然是由顶层Makefile作为入口。对于linux来说,顶层Makefile定义的是通用的规则,与体系架构相关的都在各自目录/arch/*/的makefile中定义;顶层Makefile中生成的通用的中间文件是vmlinux,这个文件是将参与编译的所有源码(不包含镜像压缩后的解压缩代码)编译、链接后生成的目标文件;在/arch/arm/和/arch/x86/目录下,定义了由vmlinux生成最终镜像文件的过程,对于arm体系架构来说,要生成的是uImage,对于x86体系架构来说,要生成的内核镜像文件是bzImage。
3、子目录下的makefile文件
子目录下的makefile文件,基本(还是全部?)都是在顶层makfile中嵌套调用的,而且这些makefile文件不是直接执行的,而是通过/scripts/makefile.build文件对其解析后执行。
二、生成vmlinux
在顶层makefile中,直接搜索vmlinux,即可以vmlinux的生成规则。
按照makefile的编译规则,vmlinux为目标文件,依赖于冒号后面跟的文件。生成由依赖文件生成目标文件的动作由后面的代码实现。
宏定义的部分暂不讨论,重点看call if_changed,link-vmlinux 一句话。$(call if_changed,link-vmlinux)是调用了if_changed函数,并向其传入了link-vmlinux参数,其实现的功能是调用 cmd_link-vmlinux函数。cmd_link-vmlinux函数的定义,将第一个语句展开:
/bin/bash scripts/link-vmlinux.sh ld -m elf_i386 --emit-relocs --build-id (以i386架构为例)
所以调用了link-vmlinux.sh脚本文件执行,在link-vmlinux.sh中通过编译、链接,最终根据$(vmlinux-deps)变量生成了vmlinux。
1 # Final link of vmlinux with optional arch pass after final link 2 cmd_link-vmlinux = \ 3 $(CONFIG_SHELL) $< $(LD) $(KBUILD_LDFLAGS) $(LDFLAGS_vmlinux) ; \ 4 $(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true) 5 6 vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE 7 ifdef CONFIG_HEADERS_CHECK 8 $(Q)$(MAKE) -f $(srctree)/Makefile headers_check 9 endif 10 ifdef CONFIG_GDB_SCRIPTS 11 $(Q)ln -fsn $(abspath $(srctree)/scripts/gdb/vmlinux-gdb.py) 12 endif 13 +$(call if_changed,link-vmlinux)
那么$(vmlinux-deps)变量又是什么呢?下面即对其进行分析。
1、$(init-y) $(drivers-y) $(core-y)等变量
顶层Makefile中定义了$(init-y) $(init-m) $(core-y) $(core-m) $(drivers-y) $(drivers-m) $(net-y) $(net-m) $(libs-y) $(libs-m) $(virt-y) $(head-y)变量。由这些变量的名字可以推测它们是按照内核的各个组件定义的,包含了内核的各个组成部分。core-y定义了内核的核心内容,包括kernel、mm、fs、ipc、usr等目录下的内容,drivers-y定义了驱动相关的driver目录下的内容,net-y定义了网络相关的net目录下的内容。他们都是在顶层Makefile中定义的。
ifeq ($(KBUILD_EXTMOD),) # Objects we will link into vmlinux / subdirs we need to visit init-y := init/ drivers-y := drivers/ sound/ firmware/ net-y := net/ libs-y := lib/ core-y := usr/ virt-y := virt/ core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/ endif # KBUILD_EXTMOD
在顶层Makefile中是没有定义$(head-y)的,因为这是与体系结构相关的内容。所以是在顶层makefile中包含的/arch/x86/Makefile文件定义的。另外,处理器架构相关的makefile文件中,还将处理器架构自有的内容补充到了相应的变量中。详见如下代码(/arch/x86/Makefile)。
head-y := arch/x86/kernel/head_$(BITS).o head-y += arch/x86/kernel/head$(BITS).o head-y += arch/x86/kernel/ebda.o head-y += arch/x86/kernel/platform-quirks.o libs-y += arch/x86/lib/ # See arch/x86/Kbuild for content of core part of the kernel core-y += arch/x86/ # drivers-y are linked after core-y drivers-$(CONFIG_MATH_EMULATION) += arch/x86/math-emu/ drivers-$(CONFIG_PCI) += arch/x86/pci/ # must be linked after kernel/ drivers-$(CONFIG_OPROFILE) += arch/x86/oprofile/ # suspend and hibernation support drivers-$(CONFIG_PM) += arch/x86/power/ drivers-$(CONFIG_FB) += arch/x86/video/
由上面的代码看出,这些变量中除了$(head-y)中直接指定.o文件,其余均是目录。显然vmlinux不会直接依赖于目录,还是要依赖于目录下的文件。所以又对这些目录进行了处理,在目录后面添加built-in.a。built-in.a文件是由当前目录及其子目录下所有参与编译的源文件共同生成的,这样才能覆盖到内核的所有源文件。patsubst命令,筛选出符合格式的参数,并用后面的内容替代。也就是对最后一个字符是'/'的目录,添加built-in.a,组成drivers/built-in.a kernel/built-in.a等形式。
另外,注意再添加built-in.a之前,也就是$(init-y)等变量中还都是目录形式的时候,定义了vmlinux-dirs变量,而且将$(init-y)等变量中的目录的'/'删除,这样vmlinux-dirs中定义的就是init kernel drivers等格式的目录了。
vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \ $(core-y) $(core-m) $(drivers-y) $(drivers-m) \ $(net-y) $(net-m) $(libs-y) $(libs-m) $(virt-y))) init-y := $(patsubst %/, %/built-in.a, $(init-y)) core-y := $(patsubst %/, %/built-in.a, $(core-y)) drivers-y := $(patsubst %/, %/built-in.a, $(drivers-y)) net-y := $(patsubst %/, %/built-in.a, $(net-y)) libs-y1 := $(patsubst %/, %/lib.a, $(libs-y)) libs-y2 := $(patsubst %/, %/built-in.a, $(filter-out %.a, $(libs-y))) virt-y := $(patsubst %/, %/built-in.a, $(virt-y))
2、$(vmlinux-deps)变量
vmlinux-deps通过KBUILD_VMLINUX-INIT/ MAIN/ LIBS/ LDS变量,又指向了 $(head-y) $(init-y) $(core-y) $(libs-y) $(drivers-y) $(net-y) $(virt-y)变量,以及相应体系架构下的链接文件vmlinux.lds。
# Externally visible symbols (used by link-vmlinux.sh)
export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y)
export KBUILD_VMLINUX_LIBS := $(libs-y1)
export KBUILD_LDS := arch/$(SRCARCH)/kernel/vmlinux.lds
export LDFLAGS_vmlinux
# used by scripts/package/Makefile
export KBUILD_ALLDIRS := $(sort $(filter-out arch/%,$(vmlinux-alldirs)) arch Documentation include samples scripts tools)
vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN) $(KBUILD_VMLINUX_LIBS)
目前的分析来看,vmlinux依赖的次顶层目录下的built-in.a(/arch/目录下与处理器体系架构相关的是单独添加的,并不包含/arch/built-in.a)已经解释清楚了。这些次顶层目录下的built-in.a需要覆盖其所在目录下所有的源文件(包括子目录下的源文件)才可以。因为linux源码的层级目录复杂,次顶层目录drivers mm等下面都还有多层目录结构,那么如何覆盖所有子目录下的文件,会是一项值得研究的工作。
3、编译生成$(vmlinux-deps)
直观的思路呢,可以在每个次顶层目录下编写一个makefile,令其built-in.a依赖于当前目录下的所有文件及子目录下的文件。但是这些维护起来比较复杂,首先次顶层目录下包含的文件非常多,会令makefile的维护难度增大,而且内核的配置项繁多,定制化的内核经常需要裁剪或者添加某个文件,文件的增删都会很复杂。
还有一种方式,为每个目录及其子目录都编写makefile文件,令其当前目录下的built-in.a包含当前目录下的源文件对应的.o文件和子目录下的built-in.a文件;这样层层传递,可以覆盖内核源码中的每个文件。这样实现,每一层的makefile都会比较单纯,只需要考虑当前目录下的文件和子目录。要增删某个文件,只需要到其所在目录下修改makefile文件即可。这样看呢,每层目录的makefile都有相同的实现方式,就是根据当前目录下的文件及子目录的built-in.a生成当前目录下的built-in.a。从可维护性考虑,linux将这些相同的编译规则提炼出来,放到了/scripts目录下的makefile.build文件中,在编译时对每层目录都是用makefile.build文件作为入口,将待编译的目录路径作为参数传入,形成了makefile.build(待编译目录)的方式。而每个目录下仍然有makefile文件,只不过这些文件中只需要定义哪些文件参与编译就可以,他们都会经过makefile.build处理并生成相应的built-in.a。
在顶层Makefile中,linux定义了次顶层目录built-in.a的生成规则,其依赖于$(vmlinux-dirs); 而$(vmlinux-dirs)的生成规则参见如下代码,它是通过调用
“$(Q)$(MAKE) $(build)=$@ need-builtin=1”语句生成的。
$(sort $(vmlinux-deps)): $(vmlinux-dirs) ; PHONY += $(vmlinux-dirs) $(vmlinux-dirs): prepare scripts $(Q)$(MAKE) $(build)=$@ need-builtin=1
$(build)变量是在/scritps/build.include文件中定义的。
### # Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj= # Usage: # $(Q)$(MAKE) $(build)=dir build := -f $(srctree)/scripts/Makefile.build obj
将$(build)变量展开后,相当于
make -f $(srctree)/scripts/makefile.build obj=$@
而$@符号所指的目标文件就是$(vmlinux-dirs)变量,前面讲过,这个变量中包含的就是所有编译的目录,init kernel drivers等等目录。make命令的参数-f是用于指定专门的makefile文件,-f指定了使用的makefile文件为scripts目录下的makefile.build, 然后传入了obj=$@。这一行代码相当于调用makefile.build分别处理init kernel drivers等等目录。如何生成这些目录下的built-in.a文件,则需要对makefile.build文件进行具体分析。
三、makefile.build文件解析
scripts/makefile.build文件的内容比较复杂,其不仅实现了我们上面说到的主线工作,还会涉及内核模块的编译规则定义等内容。本身Makefile的语法也晦涩难懂,所以此处主要讲我们上面分析到的主线内容。
Makefile.build是通过$(build)变量定义的,经常使用的语法如下(compressed为示例的子目录名称,可以为当前目录下的任意子目录):
make $(build)=$(obj)/compressed
展开后,相当于
make -f $(srctree)/scripts/makefile.build obj=$(obj)/compressed
make命令如果不指定-f参数,那么默认使用当前目录下的makefile。如果使用-f参数,则使用后面指定的makefile文件,此处就是scripts目录下的makefile.build。而且make命令还可以向makefile文件中传入参数,传入的参数obj就是待编译的子目录。而当前的$(obj)又是由编译当前目录的上一级目录传入,是当前目录的路径。所以使用$(obj)/compressed就表示了当前目录下的compressed子目录。
按照上文的梳理,makefile.build应该具备两个功能,1)定义当前$(obj)/built-in.a文件的生成规则;2)通过makefile.build处理子目录,相当于makefile.build的嵌套调用(递归操作)。下文继续梳理这两个功能相关的脉络。
1、生成$(obj)/built-in.a
1)包含$(obj)下的makefile文件
首先需要知道,在调用makefile.build的同时向其传递了参数$(obj)=待处理的目录。因为参与编译的文件都是在$(obj)目录的makefile文件中定义的,所以在makefile.build中需要包含$(obj)目录下的makefile,这样才能获取源文件列表。
makefile.build文件中定义了新变量src,并且src=$(obj)。下面代码首先判断传入的路径是否是绝对路径,如果不是则将其修改为绝对路径。然后包含obj目录下的kbuild文件或者Makefile文件。
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src)) kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile) include $(kbuild-file)
一般来说,各级目录中的makefile中只需要将所有编译到内核中的文件或者目录添加到obj-y变量中,以及将需要编译成模块的文件或者目录添加到obj-m变量中:
obj-y += time/ obj-$(CONFIG_FUTEX) += futex.o
2)生成built-in.a
在makefile.build中,定义了生成目标builtin-target := $(obj)/built-in.a。
built-in.a是依赖于$(real-obj-y)变量的,将$(real-obj-y)变量链接生成了built-in.a。这个变量显然是需要与各目录下的makefile定义的$(obj-y)变量串联起来的。
quiet_cmd_ar_builtin = AR $@ cmd_ar_builtin = rm -f $@; \ $(AR) rcSTP$(KBUILD_ARFLAGS) $@ $(filter $(real-obj-y), $^) $(builtin-target): $(real-obj-y) FORCE $(call if_changed,ar_builtin)
$(obj-y)变量只是定义了当前目录下的二进制文件和子目录,需要将其转变为$(real-obj-y),首先要将子目录转变为子目录下的built-in.a文件。然后还要精简这些文件,用strip命令删除这些二进制文件中的符号表和调试信息。这部分工作是在makefile.build包含的makefile.lib文件中实现的。(注意,include $(obj)/makefile一定要在include makefile.lib之前,因为makefile.lib中需要用到$(obj)/makefile中定义的变量)。梳理出的代码如下。
第一行代码是将obj-y中的子目录替换为“子目录/built-in.a”。第二行代码是利用strip命令精简二进制文件。第三行代码为依赖文件添加父目录的路径,将路径完整化,这样在源码顶层目录执行的make命令可以找到这些文件。最终,kernel目录的real-obj-y= (kernel/futex.o kernel/time/built-in.a 等等文件)。
obj-y := $(patsubst %/, %/built-in.a, $(obj-y)) real-obj-y := $(foreach m, $(obj-y), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))),$($(m:.o=-objs)) $($(m:.o=-y)),$(m))) real-obj-y := $(addprefix $(obj)/,$(real-obj-y))
2、通过makefile.build递归处理子目录
在makefile.build处理某个目录时,还需要递归调用makefile.build处理当前目录下的所有子目录。这样只要使用makefile.build处理一个目录,就可以实现对其内部所有文件(包括所有子目录下的文件)的编译。
子目录的定义在subdir-ym变量中,是在makefile.lib中定义的,根据$(obj-y) 和 $(obj-m)变量生成的所有子目录列表。如下代码所示,__subdir-y首先筛选出obj-y下的所有目录,并且删除其后面的'/'。比如kernel目录下的time子目录,在kernel/Makefile文件中添加到obj-y变量中 obj-y += time/,在subdir-y中删除了最后的'/'符号为time,在subidr-ym最终添加父目录路径后为kernel/time.
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y))) subdir-y += $(__subdir-y) __subdir-m := $(patsubst %/,%,$(filter %/, $(obj-m))) subdir-m += $(__subdir-m) # Subdirectories we need to descend into subdir-ym := $(sort $(subdir-y) $(subdir-m))
subdir-ym := $(addprefix $(obj)/,$(subdir-ym))
首先看__build的定义,按这个格式说明__build是需要生成的目标文件。按照make体系的编译规则,如果没有显式指定其make生成的目标文件,那么默认生成makefile文件内定义的第一个目标文件。在makefile.build中定义的第一个目标文件是__build。它后面依赖的$(builtin-target)(即 $(obj)/built-in.a)、$(subdir-ym)等也会间接成为目标文件。
<makefile.build> Line70: __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \ $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \ $(subdir-ym) $(always)
从subidr-ym的定义可知$(subdir-ym)中定义的只是目录,下面定义了$(subdir-ym)的生成规则,这段代码并不是一定会生成$(subdir-ym)这些文件夹,只是当makefile认为需要$(subdir-ym)时,会执行下面的语句。至于执行了什么操作,生成了什么文件,都是由下面的语句而定。此处即相当于递归处理了$(subdir-ym)中的子目录。这样,只需要在顶层makefile中,通过调用makefile.build,并传入需要编译的目录($(vmlinux-deps)),这样makefile.build会自动编译相应目录下所有的文件(递归编译所有子目录)生成built-in.a。
<makefile.build> Line515: PHONY += $(subdir-ym) $(subdir-ym): $(Q)$(MAKE) $(build)=$@ need-builtin=$(if $(findstring $@,$(subdir-obj-y)),1)
内核通过Makefile.build文件,将源码目录树下的所有文件串联起来,生成统一的目标文件vmlinux。
四、生成bzImage
第二章中,在顶层Makefile中生成了通用的内核目标文件vmlinux。至于最终的镜像文件,则是由各个体系架构自己定义的格式和规则来生成的,所以从vmlinux到最终的image文件,就需要到arch/$(SRCARCH)目录下研究。本章将采用反推的方式,从bzImage开始,一步一步看bzImage和vmlinux之间是如何建立联系的。
顶层Makefile中包含了对应体系架构的makefile,执行make命令自然也会自动执行包含的makefile。
include arch/$(SRCARCH)/Makefile
在arch/x86/Makefile中,有如下代码:
# KBUILD_IMAGE specify target image being built KBUILD_IMAGE := $(boot)/bzImage bzImage: vmlinux ifeq ($(CONFIG_X86_DECODER_SELFTEST),y) $(Q)$(MAKE) $(build)=arch/x86/tools posttest endif $(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE) $(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot $(Q)ln -fsn ../../x86/boot/bzImage $(objtree)/arch/$(UTS_MACHINE)/boot/$@
当我们执行make bzImage命令时,编译器就会找到该文件中对bzImage的定义。显然,bzImage依赖于vmlinux文件。通过第一个语句make $(build)=$(boot) $(KBUILD_IMAGE),生成/arch/x86/boot/bzImage. 后两句是在最终的目录下创建bzImage的链接文件,将其链接到/arch/x86/boot/bzImage。
解析第一句的语法,其中$(build)的定义为:
scripts/build.include文件:
### # Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj= # Usage: # $(Q)$(MAKE) $(build)=dir build := -f $(srctree)/scripts/Makefile.build obj
展开第一个语句,即make -f $(srctree)/scripts/Makefile.build dir=. obj=arch/x86/boot arch/x86/boot/bzImage
make命令的参数-f是用于指定专门的makefile文件,-f指定了使用的makefile文件为scripts目录下的makefile.build, 然后传入了dir和obj两个参数, 生成的是/arch/x86/boot下的zImage。makefile.build文件的分析请参照第三节。
因为此处的make命令显示制定了生成的文件/arch/x86/boot/zImage,那么就先找到在哪里定义了bzImage的生成规则。显然不会在通用规则文件里,那么就只能是在的/arch/x86/boot目录(传入的$obj参数)的makefile文件中了。
此处的$(obj)即是/arch/x86/boot目录。该目录下的目标文件是bzImage,依赖于setup.bin vmlinux.bin 和 tools/build工具,调用cmd_image函数生成目标文件bzImage。cmd_image中调用tools/build工具,根据 /arch/x86/boot/目录下的setup.bin vmlinux.bin 和 zoffset.h 生成bzImge。
quiet_cmd_image = BUILD $@ silent_redirect_image = >/dev/null cmd_image = $(obj)/tools/build $(obj)/setup.bin $(obj)/vmlinux.bin \ $(obj)/zoffset.h $@ $($(quiet)redirect_image) $(obj)/bzImage: $(obj)/setup.bin $(obj)/vmlinux.bin $(obj)/tools/build FORCE $(call if_changed,image) @$(kecho) 'Kernel: $@ is ready' ' (#'`cat .version`')'
那接下来就要依次去看这几个文件是如何生成的?
首先是核心的内核文件vmlinux.bin,它是由$(obj)/compressed/vmlinux文件生成的。call if_changed,objcopy 调用了cmd_objcopy函数,该函数在Makefile.lib中定义,就是调用了GNU的objcopy工具,将$(obj)/compressed目录下的vmlinux生成了$(obj)/vmlinux.bin。
$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)
arch/x86/boot/compressed/vmlinux如何生成?自然是去compressed目录下的Makefile。在Makefile.build的规则中已经提到,通过Makefile.build传入/arch/x86/boot路径后,会自动include /arch/x86/boot路径下的Makefile,同时对/arch/x86/boot路径下的子目录执行Makefile.build脚本,自然会处理子目录compressed下的makefile文件。
顾名思义,compressed文件夹是定义了内核文件压缩的相关内容。所以compressed目录下必然会有对内核文件的压缩动作、添加解压缩的相关代码等工作。compressed目录下的vmlinux文件是依赖于$(vmlinux-objs-y)变量的,$(vmlinux-objs-y)变量包含的文件链接生成了vmlinux。
# We need to run two commands under "if_changed", so merge them into a # single invocation. quiet_cmd_check-and-link-vmlinux = LD $@ cmd_check-and-link-vmlinux = $(cmd_check_data_rel); $(cmd_ld) $(obj)/vmlinux: $(vmlinux-objs-y) FORCE $(call if_changed,check-and-link-vmlinux)
$(vmlinux-objs-y)变量是在当前的Makefile定义的,下面的代码为$(vmlinux-objs-y)的一部分内容,会根据CONFIG配置来决定是否包含某些文件。
vmlinux-objs-y := $(obj)/vmlinux.lds $(obj)/head_$(BITS).o $(obj)/misc.o \ $(obj)/string.o $(obj)/cmdline.o $(obj)/error.o \ $(obj)/piggy.o $(obj)/cpuflags.o vmlinux-objs-$(CONFIG_EARLY_PRINTK) += $(obj)/early_serial_console.o vmlinux-objs-$(CONFIG_RANDOMIZE_BASE) += $(obj)/kaslr.o ifdef CONFIG_X86_64 vmlinux-objs-$(CONFIG_RANDOMIZE_BASE) += $(obj)/kaslr_64.o vmlinux-objs-y += $(obj)/mem_encrypt.o vmlinux-objs-y += $(obj)/pgtable_64.o endif
正常来说,$(vmlinux-objs-y)肯定是需要包含根目录下通用makefile生成的vmlinux文件的,但是并没有看到根目录下vmlinux的显示定义。为什么呢????其实,根目录下的vmlinux是隐式包含在了piggy.o中。而piggy.s也很神奇,它并不是compressed文件夹下自带的文件,在下载的内核源码中是找不到piggy.s文件的,它是在Makefile在编译过程中动态生成的。在compressed/Makefile的末尾,定义了piggy.s文件:
quiet_cmd_mkpiggy = MKPIGGY $@ cmd_mkpiggy = $(obj)/mkpiggy $< > $@ || ( rm -f $@ ; false ) targets += piggy.S $(obj)/piggy.S: $(obj)/vmlinux.bin.$(suffix-y) $(obj)/mkpiggy FORCE $(call if_changed,mkpiggy)
可见,piggy.s文件依赖于arch/x86/boot/compressed/vmlinux.bin.$(suffix-y)文件,通过cmd_mkpiggy函数,展开该函数如下
/arch/x86/boot/compressed/mkpiggy /arch/x86/boot/compressed/vmlinux.bin.$(suffix-y) > piggy.S || (rm -f piggy.S ; false)
其中$(suffix-y)是通过config定义的压缩形式,包括gz/ bz2/ xz等,此处以gz为例;
suffix-$(CONFIG_KERNEL_GZIP) := gz
所以通过调用mkpiggy程序,传入/arch/x86/boot/compressed/vmlinux.bin.gz)参数,打印消息生成了piggy.S。而mkpiggy也是在make过程中由源码文件mkpiggy.c生成的,mkpiggy.c中的打印消息,就是生成的piggy.S内容。
其中的.incbin "arch/x86/boot/compressed/vmlinux.bin.gz"就是采用包含二进制的方式,直接添加vmlinux.bin.gz文件。也就是在生成的piggy.o中会包含vmlinux.bin.gz的全部内容。
printf(".section \".rodata..compressed\",\"a\",@progbits\n"); printf(".globl z_input_len\n"); printf("z_input_len = %lu\n", ilen); printf(".globl z_output_len\n"); printf("z_output_len = %lu\n", (unsigned long)olen); printf(".globl input_data, input_data_end\n"); printf("input_data:\n"); printf(".incbin \"%s\"\n", argv[1]); printf("input_data_end:\n");
那么再看如何生成了arch/x86/boot/compressed/vmlinux.bin.gz文件,同样在compressed目录的Makefile中给出了定义。就是调用gzip工具,将arch/x86/boot/compressed/vmlinux.bin压缩成了vmlinux.bin.gz。
vmlinux.bin.all-y := $(obj)/vmlinux.bin vmlinux.bin.all-$(CONFIG_X86_NEED_RELOCS) += $(obj)/vmlinux.relocs $(obj)/vmlinux.bin.gz: $(vmlinux.bin.all-y) FORCE $(call if_changed,gzip)
那么arch/x86/boot/compressed/vmlinux.bin是如何生成呢,也是在compressed目录的Makefile中定义。利用GNU的objcopy工具处理vmlinux文件,生成了$(obj)/vmlinux.bin文件。注意,vmlinux是没有添加目录前缀的,也是顶层目录下的vmlinux。
$(obj)/vmlinux.bin: vmlinux FORCE $(call if_changed,objcopy)
至此,从顶层目录vmlinux到bzImage的创建过程分析完成。
最后,正向梳理一遍vmlinux到bzImage的创建过程。
Vmlinux--------->$(boot)/compressed/vmlinux.bin--------->$(boot)/compressed/vmlinux.bin.gz --------->$(boot)/compressed/piggy.o ----------- > $(boot)/compressed/vmlinux ---------------> $(boot)/vmlinux.bin + $(boot)/setup.bin + $(boot)/zoffset.h --------------->$(boot)/bzImage