[ARM Linux系统移植] Linux 顶层 Makefile 分析
从这篇文章开始,我们就进入到 linux 系统的移植工作了。这篇文章主要分析 linux 代码中顶层 makefile 的大致逻辑。
1. 初次编译
在分析顶层 makefile 之前,我们需要编译一下待分析的 linux 内核。因为编译过后会生成额外的文件,比如链接脚本,它们都有利于分析工作。
还有一个问题是 linux 内核版本的选择。这里我们直接使用 NXP 根据 linux 官网移植好的版本,因为自己移植芯片和外设相关的代码难度会很大。往后我们移植 linux 系统也是基于 NXP 的版本。
和编译 u-boot 时类似,依次输入如下命令编译 linux:
- > make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
- > make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
- > make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
编译成功的话,会在 arch/arm/boot 目录下生成 zImage,同时 arch/arm/boot/dts 目录下会生成很多 dtb 文件。这两个文件我们都不陌生,就是在 u-boot 引导时加载的镜像文件和设备树文件。
2. 顶层 Makefile
linux 的顶层 makefile 和 u-boot 的顶层 makefile 有很多相同的概念。这时候就发挥了记笔记的好处,我们可以快速回顾一下之前针对 u-boot 写的三篇文章:
1. [ARM Linux系统移植] U-Boot 顶层 Makefile 分析 - 基础知识
2. [ARM Linux系统移植] U-Boot make ???_defconfig 分析
3. [ARM Linux系统移植] U-Boot make 分析
关于顶层 makefile 的基础知识,在这篇文章里就不再叙述了。因为静默输出、模块编译和交叉编译器设置等等概念和代码,u-boot 和 linux 都是完全一样的。后续我们会分析 linux 下的 make ???_defconfig 和 make all 命令流程。
2.1 make ???_defconfig
make ???_defconfig 对应的规则如下,三个依赖中,我们只需要看 scripts_basic。因为当前 outputmakefile 中的编译条件不满足,可以看成是空的;FORCE 是伪目标,用于强制更新。
- %config: scripts_basic outputmakefile FORCE
- $(Q)$(MAKE) $(build)=scripts/kconfig $@
scripts_basic 目标定义如下,其中 build := -f ./scripts/Makefile.build obj,定义在 scripts/Kbuild.include 文件中。
- # Basic helpers built in scripts/
- PHONY += scripts_basic
- scripts_basic:
- $(Q)$(MAKE) $(build)=scripts/basic
- $(Q)rm -f .tmp_quiet_recordmcount
可以看到 %config 和 scripts_basic 目标最终都会转到 scripts/Makefile.build 文件中执行,只是传递的 obj 变量不同。现在就会自然考虑是如何根据 obj 变量的不同来区分不同的编译任务的?下面我们就把目光转到 scripts/Makefile.build 文件。
如下代码所示,scripts\Makefile.build 中,obj 变量被赋予 src 变量(第 5 行)。接着会根据 src 变量来确定 kbuild-file 变量(第 42 到 43 行)。最终会将 kbuild-file 指定的文件引入(第 44 行)。正是因为引入的文件不同,里面指定的变量也会不同,所以导致了不同的依赖,从而产生不同的编译任务。
- src := $(obj)
- ……
- # 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.1.1 scripts_basic
我们先看 scripts_basic 目标,其中 obj=scripts/basic。因为没有指定目标,所以匹配默认目标 __build。
按照编译时打印的内容,可以发现此时只有 always 依赖有效,其内容为 scripts/basic/fixdep(在文件 scripts/basic/Makefile 中定义)。因此 scripts_basic 目标的作用就是为了生成 fixdep 程序。
- __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
- $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
- $(subdir-ym) $(always)
- @:
2.1.2 %config
%config 目标对应的 obj=scripts/kconfig,此时引入了 scripts/kconfig/Makefile 文件。引入的文件中有可以匹配的目标,如下代码所示:
- %_defconfig: $(obj)/conf
- $(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)
其作用是使用 conf 命令将指定的 defconfig 文件中的配置输出到根目录下的 .config 文件。
2.2 make all
make ???_defconfig 命令执行完毕之后,就可以使用 make(或者 make all 或者 all)进行整体编译。
此时匹配到的目标如下所示,首先是 _all。因为此时非模块编译,所以 _all 又依赖于 all,all 又依赖于 vmlinux。即我们最终需要关注 vmlinux 这个目标。
vmlinux 中的 scripts/link-vmlinux.sh 依赖是脚本文件,默认情况下不会缺失;FORCE 依赖是伪目标,用于强制更新。所以我们把“焦点”放在 vmlinux-deps 依赖上。
- # If building an external module we do not care about the all: rule
- # but instead _all depend on modules
- PHONY += all
- ifeq ($(KBUILD_EXTMOD),)
- _all: all
- else
- _all: modules
- endif
- ……
- # The all: target is the default when no target is given on the
- # command line.
- # This allow a user to issue only 'make' to build a kernel including modules
- # Defaults to vmlinux, but the arch makefile usually adds further targets
- all: vmlinux
- ……
- # Include targets which we want to
- # execute if the rest of the kernel build went well.
- vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
- 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
- ifdef CONFIG_GDB_SCRIPTS
- $(Q)ln -fsn `cd $(srctree) && /bin/pwd`/scripts/gdb/vmlinux-gdb.py
- endif
- +$(call if_changed,link-vmlinux)
从下面代码中可以了解 vmlinux-deps 是由 head-y、init-y、core-y 等等变量组成的。
- # Externally visible symbols (used by link-vmlinux.sh)
- export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
- export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y) $(drivers-y) $(net-y)
- export KBUILD_LDS := arch/$(SRCARCH)/kernel/vmlinux.lds
- export LDFLAGS_vmlinux
- # used by scripts/pacmage/Makefile
- export KBUILD_ALLDIRS := $(sort $(filter-out arch/%,$(vmlinux-alldirs)) arch Documentation include samples scripts tools virt)
- vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)
head-y、init-y、core-y 等等变量的构成方式都是类似的,下面我们以 core-y 进行举例。core-y 不仅是固定指定的,还可以通过条件编译选项进行增加。最后会将 core-y 变量中指定的目录名扩展成(使用 patsubst)相应目录下的 built-in.o 文件。
- core-y := usr/
- core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/
- core-$(CONFIG_FPE_NWFPE) += arch/arm/nwfpe/
- core-$(CONFIG_FPE_FASTFPE) += $(FASTFPE_OBJ)
- core-$(CONFIG_VFP) += arch/arm/vfp/
- core-$(CONFIG_XEN) += arch/arm/xen/
- core-$(CONFIG_KVM_ARM_HOST) += arch/arm/kvm/
- core-$(CONFIG_VDSO) += arch/arm/vdso/
- init-y := $(patsubst %/, %/built-in.o, $(init-y))
- core-y := $(patsubst %/, %/built-in.o, $(core-y))
- drivers-y := $(patsubst %/, %/built-in.o, $(drivers-y))
- net-y := $(patsubst %/, %/built-in.o, $(net-y))
在了解了 vmlinux-deps 依赖是一大堆各个目录下的 built-in.o 文件后(还有一些 .a 库文件),我们重新回到它的命令语句(先抛开前面的各个条件判断):
- +$(call if_changed,link-vmlinux)
开头的加号代表命令结果不可忽略。这条指令调用了 if_changed 函数,传递的第一个参数为 link-vmlinux。if_changed 函数的定义如下:
- # Execute command if command has changed or prerequisite(s) are updated.
- #
- if_changed = $(if $(strip $(any-prereq) $(arg-check)), \
- @set -e; \
- $(echo-cmd) $(cmd_$(1)); \
- printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd)
any-prereq 用于检查依赖文件是否有变化,如果依赖文件有变化就不为空。arg-check 用于检查参数是否有变化,如果有变化就不为空。cmd_$(1) 拼接第一参数,即会调用 cmd_link-vmlinux 命令。
strip 函数将会从 text 中移除所有前导和接在后面的空格,并以单一空格符号来替换内部所有的空格。
cmd_link-vmlinux 展开为 cmd_link-vmlinux=/bin/bash scripts/link-vmlinux.sh arm-linux-gnueabihf-ld -EL -p --no-undefined -X --pic-veneer --build-id,即调用了 scripts/link-vmlinux.sh 脚本。
- # Final link of vmlinux
- cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)
- quiet_cmd_link-vmlinux = LINK $@
scripts/link-vmlinux.sh 脚本中,我们看到 vmlinux_link 函数,走的是第 56 至 58 行的分支。其中需要链接的内容为 KBUILD_VMLINUX_INIT 和 KBUILD_VMLINUX_MAIN,它们已经在顶层 makefile 中导出。最终的命令就是将所有的 .o 文件汇总链接为 vmlinux 文件。
- # Link of vmlinux
- # ${1} - optional extra .o files
- # ${2} - output file
- vmlinux_link()
- {
- local lds="${objtree}/${KBUILD_LDS}"
- if [ "${SRCARCH}" != "um" ]; then
- ${LD} ${LDFLAGS} ${LDFLAGS_vmlinux} -o ${2} \
- -T ${lds} ${KBUILD_VMLINUX_INIT} \
- --start-group ${KBUILD_VMLINUX_MAIN} --end-group ${1}
- else
- ${CC} ${CFLAGS_vmlinux} -o ${2} \
- -Wl,-T,${lds} ${KBUILD_VMLINUX_INIT} \
- -Wl,--start-group \
- ${KBUILD_VMLINUX_MAIN} \
- -Wl,--end-group \
- -lutil ${1}
- rm -f linux
- fi
- }
2.3 built-in.o
现在我们只遗留下一个问题,那就是各个 built-in.o 是如何编译生成的。之前了解到 vmlinux-deps 中包含了各个 built-in.o,我们找到 vmlinux-deps 作为目标的地方。其在下方代码第 937 行,可以看到 vmlinux-deps 依赖于 vmlinux-dirs。vmlinux-dirs 定义在第 889 行,内容为各个 built-in.o 文件所在的目录。
重点就落在了第 946 行的 vmlinux-dirs 目标,它的两个依赖这边就先不深究了,主要关注它的命令。vmlinux-dirs 目标对应的命令又执行到了 scripts/Makefile.build 这里,其中传递的 obj 变量就为 built-in.o 的目录,后续引入的 makefile 文件也是此目录下的。
- vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
- $(core-y) $(core-m) $(drivers-y) $(drivers-m) \
- $(net-y) $(net-m) $(libs-y) $(libs-m)))
- ……
- # The actual objects are generated when descending,
- # make sure no implicit rule kicks in
- $(sort $(vmlinux-deps)): $(vmlinux-dirs) ;
- ……
- PHONY += $(vmlinux-dirs)
- $(vmlinux-dirs): prepare scripts
- $(Q)$(MAKE) $(build)=$@
因为没有指定目标,所以使用的还是默认目标 __build,这个在 2.1.1 节也介绍过。
- __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
- $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
- $(subdir-ym) $(always)
- @:
与 2.1.1 节不同,此时的 builtin-target 不为空,我们需要重点关注它。如下代码所示,builtin-target 在第 87 行定义,定义为此目录下的 built-in.o 文件,我们需要再找到 builtin-target 目标,看它是如何生成的。builtin-target 目标在第 336 行定义,依赖是 obj-y,包含 obj 变量对应目录下编译生成的所有 .o 文件,它是通过目录下的 makefile 引入的,而所有 .o 文件都是由目录下对应的 .c 文件生成的。
最后我们看这条规则对应的命令,同样调用了 if_changed 函数,并且之后会调用 cmd_link_o_target 命令。可以看到就是 cmd_link_o_target 命令将当前目录下的所有 .o 文件链接成 built-in.o 文件。
- ifneq ($(strip $(obj-y) $(obj-m) $(obj-) $(subdir-m) $(lib-target)),)
- builtin-target := $(obj)/built-in.o
- endif
- ……
- quiet_cmd_link_o_target = LD $@
- # If the list of objects to link is empty, just create an empty built-in.o
- cmd_link_o_target = $(if $(strip $(obj-y)),\
- $(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) \
- $(cmd_secanalysis),\
- rm -f $@; $(AR) rcs$(KBUILD_ARFLAGS) $@)
- $(builtin-target): $(obj-y) FORCE
- $(call if_changed,link_o_target)