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)
1、$(vmlinux-deps)
作为vmlinux唯一的依赖文件,说明几乎所有内核源码最终都包含在了$(vmlinux-deps)中,那么对这个变量的分析至关重要。这个变量的定义也在顶层makefile中找到。下面结合其代码具体分析。
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)
由$(head-y) $(virt-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文件中,还将处理器架构自有的内容补充到了相应的变量中。详见如下代码。
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/
三、生成bzImage
在顶层Makefile中,包含对应体系架构的makefile,执行make命令也会自动执行包含的makefile。
include arch/$(SRCARCH)/Makefile
在arch/x86/Makefile中,有如下代码:
1 bzImage: vmlinux 2 ifeq ($(CONFIG_X86_DECODER_SELFTEST),y) 3 $(Q)$(MAKE) $(build)=arch/x86/tools posttest 4 endif 5 $(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE) 6 $(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot 7 $(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. 后两句是创建/arch/i386/boot/bzImage或者/arch/x64/boot/bzImage链接到生成的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文件制定了内核编译体系的规则,几乎子目录下的所有makefile都是通过makefile.build文件解析后执行。可以简单看一下Makefile.build文件:
1)在其中包含了$(obj)路径下的Makefile,(src在最开始被赋值为obj,kbuild-file为src目录下的Kbuild文件或者Makefile文件)
# The filename Kbuild has precedence over 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)
2)递归调用$(obj)路径下的子目录
subdir-ym为子目录,同样采用$build参数,再次makefile.build,只不过现在向其传入的obj是当前obj目录下的子目录;
PHONY += $(subdir-ym) $(subdir-ym): $(Q)$(MAKE) $(build)=$@ need-builtin=$(if $(findstring $@,$(subdir-obj-y)),1)
没有指明生成的目标文件的情况,默认生成makefile文件(makefile.build)中定义的第一个目标文件,即__build。__build依赖于我们的$(builtin-target) $(lib-target) $(extra-y)变量。其中$(builtin-target)变量定义的就是我们的当前目录下的/built-in.a。
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \ $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \ $(subdir-ym) $(always)
$(builtin-target)是依赖于$(real-obj-y)变量生成的,call if_changed函数相当于调用cmd_ar_builtin。cmd_ar_builtin函数删除了原目标文件,将$(real-obj-y)链接生成了$(obj)/built-in.a。
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)
$(real-obj-y)是在Makefile.build包含的Makefile.lib文件中定义的。obj-y最开始是在当前的$(obj)目录的Makefile中配置的,参与编译的文件和目录会添加到obj-y中。第一行代码是将obj-y中的目录(最后一个字符是/)添加built-in.a;第二行代码没太看懂????感觉是对$(obj)目录下的文件进行处理。比如abc.c定义为 abc.c: .o = -objs。第三行代码是在前面添加绝对路径。
所以$(real-obj-y) = 子目录下的built-in.a + 当前目录下的.o
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))
子目录下的built-in.a又是在本次Makefile.build中嵌套调用 makefile.build 传入子目录的obj参数后生成的。这样层层嵌套调用,直到最底层目录。通过makefile.build的子目录嵌套调用机制,将Linux的层级目录统统编译到内核镜像文件中。
前面说到,arch/x86/Makefile中执行Makefile.build传入boot目录这一行代码,定义了生成的目标文件是/arch/x86/boot/bzImage,所以就需要看在哪里定义了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的创建过程分析完成。