[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 ???_defconfigmake all 命令流程。

2.1 make ???_defconfig

make ???_defconfig 对应的规则如下,三个依赖中,我们只需要看 scripts_basic。因为当前 outputmakefile 中的编译条件不满足,可以看成是空的;FORCE 是伪目标,用于强制更新。

Makefile
  1. %config: scripts_basic outputmakefile FORCE
  2.     $(Q)$(MAKE) $(build)=scripts/kconfig $@

scripts_basic 目标定义如下,其中 build := -f ./scripts/Makefile.build obj,定义在 scripts/Kbuild.include 文件中。

Makefile
  1. # Basic helpers built in scripts/
  2. PHONY += scripts_basic
  3. scripts_basic:
  4.     $(Q)$(MAKE) $(build)=scripts/basic
  5.     $(Q)rm -f .tmp_quiet_recordmcount

可以看到 %configscripts_basic 目标最终都会转到 scripts/Makefile.build 文件中执行,只是传递的 obj 变量不同。现在就会自然考虑是如何根据 obj 变量的不同来区分不同的编译任务的?下面我们就把目光转到 scripts/Makefile.build 文件。

如下代码所示,scripts\Makefile.build 中,obj 变量被赋予 src 变量(第 5 行)。接着会根据 src 变量来确定 kbuild-file 变量(第 42 到 43 行)。最终会将 kbuild-file 指定的文件引入(第 44 行)。正是因为引入的文件不同,里面指定的变量也会不同,所以导致了不同的依赖,从而产生不同的编译任务。

scripts/Makefile.build
  1. src := $(obj)
  2. ……
  3. # The filename Kbuild has precedence over Makefile
  4. kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
  5. kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
  6. include $(kbuild-file)

2.1.1 scripts_basic

我们先看 scripts_basic 目标,其中 obj=scripts/basic。因为没有指定目标,所以匹配默认目标 __build

按照编译时打印的内容,可以发现此时只有 always 依赖有效,其内容为 scripts/basic/fixdep(在文件 scripts/basic/Makefile 中定义)。因此 scripts_basic 目标的作用就是为了生成 fixdep 程序。

scripts/Makefile.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.     @:

2.1.2 %config

%config 目标对应的 obj=scripts/kconfig,此时引入了 scripts/kconfig/Makefile 文件。引入的文件中有可以匹配的目标,如下代码所示:

scripts/kconfig/Makefile
  1. %_defconfig: $(obj)/conf
  2.     $(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)

其作用是使用 conf 命令将指定的 defconfig 文件中的配置输出到根目录下的 .config 文件。

2.2 make all

make ???_defconfig 命令执行完毕之后,就可以使用 make(或者 make all 或者 all)进行整体编译。

此时匹配到的目标如下所示,首先是 _all。因为此时非模块编译,所以 _all 又依赖于 allall 又依赖于 vmlinux。即我们最终需要关注 vmlinux 这个目标。

vmlinux 中的 scripts/link-vmlinux.sh 依赖是脚本文件,默认情况下不会缺失;FORCE 依赖是伪目标,用于强制更新。所以我们把“焦点”放在 vmlinux-deps 依赖上。

Makefile
  1. # If building an external module we do not care about the all: rule
  2. # but instead _all depend on modules
  3. PHONY += all
  4. ifeq ($(KBUILD_EXTMOD),)
  5. _all: all
  6. else
  7. _all: modules
  8. endif
  9. ……
  10. # The all: target is the default when no target is given on the
  11. # command line.
  12. # This allow a user to issue only 'make' to build a kernel including modules
  13. # Defaults to vmlinux, but the arch makefile usually adds further targets
  14. all: vmlinux
  15. ……
  16. # Include targets which we want to
  17. # execute if the rest of the kernel build went well.
  18. vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
  19. ifdef CONFIG_HEADERS_CHECK
  20.     $(Q)$(MAKE) -f $(srctree)/Makefile headers_check
  21. endif
  22. ifdef CONFIG_SAMPLES
  23.     $(Q)$(MAKE) $(build)=samples
  24. endif
  25. ifdef CONFIG_BUILD_DOCSRC
  26.     $(Q)$(MAKE) $(build)=Documentation
  27. endif
  28. ifdef CONFIG_GDB_SCRIPTS
  29.     $(Q)ln -fsn `cd $(srctree) && /bin/pwd`/scripts/gdb/vmlinux-gdb.py
  30. endif
  31.     +$(call if_changed,link-vmlinux)

从下面代码中可以了解 vmlinux-deps 是由 head-yinit-ycore-y 等等变量组成的。

Makefile
  1. # Externally visible symbols (used by link-vmlinux.sh)
  2. export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
  3. export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y) $(drivers-y) $(net-y)
  4. export KBUILD_LDS          := arch/$(SRCARCH)/kernel/vmlinux.lds
  5. export LDFLAGS_vmlinux
  6. # used by scripts/pacmage/Makefile
  7. export KBUILD_ALLDIRS := $(sort $(filter-out arch/%,$(vmlinux-alldirs)) arch Documentation include samples scripts tools virt)
  8.  
  9. vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)

head-yinit-ycore-y 等等变量的构成方式都是类似的,下面我们以 core-y 进行举例。core-y 不仅是固定指定的,还可以通过条件编译选项进行增加。最后会将 core-y 变量中指定的目录名扩展成(使用 patsubst)相应目录下的 built-in.o 文件。

Makefile
  • core-y      := usr/
  • core-y      += kernel/ mm/ fs/ ipc/ security/ crypto/ block/
arch/arm/Makefile
  • 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/
Makefile
  • 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-vmlinuxif_changed 函数的定义如下:

scripts/Kbuild.include
  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.     printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd)

any-prereq 用于检查依赖文件是否有变化,如果依赖文件有变化就不为空。arg-check 用于检查参数是否有变化,如果有变化就不为空。cmd_$(1) 拼接第一参数,即会调用 cmd_link-vmlinux 命令。

$(strip text)

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 脚本。

Makefile
  1. # Final link of vmlinux
  2.       cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)
  3. quiet_cmd_link-vmlinux = LINK    $@

scripts/link-vmlinux.sh 脚本中,我们看到 vmlinux_link 函数,走的是第 56 至 58 行的分支。其中需要链接的内容为 KBUILD_VMLINUX_INITKBUILD_VMLINUX_MAIN,它们已经在顶层 makefile 中导出。最终的命令就是将所有的 .o 文件汇总链接为 vmlinux 文件。

scripts/link-vmlinux.sh
  1. # Link of vmlinux
  2. # ${1} - optional extra .o files
  3. # ${2} - output file
  4. vmlinux_link()
  5. {
  6.     local lds="${objtree}/${KBUILD_LDS}"
  7.  
  8.     if [ "${SRCARCH}" !"um" ]; then
  9.         ${LD} ${LDFLAGS} ${LDFLAGS_vmlinux} -o ${2}                  \
  10.             -T ${lds} ${KBUILD_VMLINUX_INIT}                     \
  11.             --start-group ${KBUILD_VMLINUX_MAIN} --end-group ${1}
  12.     else
  13.         ${CC} ${CFLAGS_vmlinux} -o ${2}                              \
  14.             -Wl,-T,${lds} ${KBUILD_VMLINUX_INIT}                 \
  15.             -Wl,--start-group                                    \
  16.                  ${KBUILD_VMLINUX_MAIN}                      \
  17.             -Wl,--end-group                                      \
  18.             -lutil ${1}
  19.         rm -f linux
  20.     fi
  21. }

2.3 built-in.o

现在我们只遗留下一个问题,那就是各个 built-in.o 是如何编译生成的。之前了解到 vmlinux-deps 中包含了各个 built-in.o,我们找到 vmlinux-deps 作为目标的地方。其在下方代码第 937 行,可以看到 vmlinux-deps 依赖于 vmlinux-dirsvmlinux-dirs 定义在第 889 行,内容为各个 built-in.o 文件所在的目录。

重点就落在了第 946 行的 vmlinux-dirs 目标,它的两个依赖这边就先不深究了,主要关注它的命令。vmlinux-dirs 目标对应的命令又执行到了 scripts/Makefile.build 这里,其中传递的 obj 变量就为 built-in.o 的目录,后续引入的 makefile 文件也是此目录下的。

Makefile
  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)))
  4. ……
  5. # The actual objects are generated when descending,
  6. # make sure no implicit rule kicks in
  7. $(sort $(vmlinux-deps))$(vmlinux-dirs) ;
  8. ……
  9. PHONY += $(vmlinux-dirs)
  10. $(vmlinux-dirs): prepare scripts
  11.     $(Q)$(MAKE) $(build)=$@

因为没有指定目标,所以使用的还是默认目标 __build,这个在 2.1.1 节也介绍过。

scripts/Makefile.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.     @:

与 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 文件。

scripts/Makefile.build
  1. ifneq ($(strip $(obj-y) $(obj-m) $(obj-) $(subdir-m) $(lib-target)),)
  2. builtin-target := $(obj)/built-in.o
  3. endif
  4. ……
  5. quiet_cmd_link_o_target = LD      $@
  6. # If the list of objects to link is empty, just create an empty built-in.o
  7. cmd_link_o_target = $(if $(strip $(obj-y)),\
  8.               $(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) \
  9.               $(cmd_secanalysis),\
  10.               rm -f $@; $(AR) rcs$(KBUILD_ARFLAGS) $@)
  11.  
  12. $(builtin-target)$(obj-y) FORCE
  13.     $(call if_changed,link_o_target)