Skip to content

LV080-顶层Makefile基础解析 ​

本节使用的是 4.19.71 版本的内核。

一、概述 ​

其实大概翻一下 linux 内核的顶层 Makefile 就会发现,它和 u-boot 的顶层 Makefile 结构是很类似的,大部分都差不多,之前已经详细去分析过 u-boot 的顶层 Makefile 了,可以看这里:15-系统镜像/10-uboot/LV100-顶层Makefile基础解析.md。这里一些基础的东西就不再详细分析了。

二、make xxx_defconfig ​

1. 打印信息 ​

先看一下配置过程的打印信息吧:

shell
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig

我们会在终端看到以下打印信息:

shell
make -f ./scripts/Makefile.build obj=scripts/basic
  gcc -Wp,-MD,scripts/basic/.fixdep.d -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89         -o scripts/basic/fixdep scripts/basic/fixdep.c   
rm -f .tmp_quiet_recordmcount
make -f ./scripts/Makefile.build obj=scripts/kconfig imx_v6_v7_defconfig
  gcc -Wp,-MD,scripts/kconfig/.conf.o.d -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89       -c -o scripts/kconfig/conf.o scripts/kconfig/conf.c
  bison -oscripts/kconfig/zconf.tab.c -t -l scripts/kconfig/zconf.y
  flex -oscripts/kconfig/zconf.lex.c -L scripts/kconfig/zconf.l
  gcc -Wp,-MD,scripts/kconfig/.zconf.tab.o.d -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89      -Iscripts/kconfig -c -o scripts/kconfig/zconf.tab.o scripts/kconfig/zconf.tab.c
  gcc   -o scripts/kconfig/conf scripts/kconfig/conf.o scripts/kconfig/zconf.tab.o   
scripts/kconfig/conf  --defconfig=arch/arm/configs/imx_v6_v7_defconfig Kconfig
#

# configuration written to .config
#

后面我们分析的时候会用到。

2. %config 目标 ​

第一次编译 Linux 之前都要使用“make xxx_defconfig”先配置 Linux 内核,在顶层 Makefile 中有“%config”这个目标。它定义在 Makefile - %config:

makefile
%config: scripts_basic outputmakefile FORCE
	$(Q)$(MAKE) $(build)=scripts/kconfig $@

“%config”依赖 scripts_basic、outputmakefile 和 FORCE,“%config”真正有意义的依赖就只有 scripts_basic。其他的这几个变量这里直接加打印信息来看吧,就不逐个分析了:

makefile
%config: scripts_basic outputmakefile FORCE
	@echo "Q=$(Q)"
	@echo "MAKE=$(MAKE)"
	@echo "build=$(build)"
	@echo "@=$@"
	$(Q)$(MAKE) $(build)=scripts/kconfig $@

然后执行:

shell
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig

就会看到如下信息:

shell
Q=
MAKE=make
build=-f ./scripts/Makefile.build obj
@=imx_v6_v7_defconfig

所以上面的规则展开就是:

makefile
%config: scripts_basic outputmakefile FORCE
	make -f ./scripts/Makefile.build obj=scripts/kconfig imx_v6_v7_defconfig # 也可以有@,视配置而定

3. build 变量 ​

这个 build 变量定义在 Kbuild.include - scripts/Kbuild.include - build:

makefile
###
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj

这个 srctree 是定义在顶层 Makefile - srctree 中,分析之后 srctree = .。所以这里就是:

makefile
build := -f ./scripts/Makefile.build obj

4. scripts_basic ​

接下来看一下这个 scripts_basic 规则,它定义在 Makefile - scripts_basic :

makefile
# Basic helpers built in scripts/basic/
PHONY += scripts_basic
scripts_basic:
	$(Q)$(MAKE) $(build)=scripts/basic
	$(Q)rm -f .tmp_quiet_recordmcount

前面我们已经打印了相关的变量的值,所以这里其实就是:

makefile
scripts_basic:
    @make -f ./scripts/Makefile.build obj=scripts/basic # 也可以没有@,视配置而定
    @rm -f . tmp_quiet_recordmcount                     # 也可以没有@

5. Makefile.build ​

从前面的%config 和 scripts_basic 目标可以知道,“make xxx_defconfig“配置 Linux 的时候如下两行命令会执行脚本 Makefile.build - scripts/Makefile.build :

makefile
@make -f ./scripts/Makefile.build obj=scripts/basic
@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig

5.1 scripts_basic 目标对应的命令 ​

scripts_basic 目标对应的命令为:

makefile
@make -f ./scripts/Makefile.build obj=scripts/basic

我们打开 Makefile.build - scripts/Makefile.build 文件,会看到这两个变量:

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)

这里的 src 变量定义在 Makefile.build - scripts/Makefile.build - src

makefile
src := $(obj)

所以 src = scripts/basic,此时我们将 kbuild-dir 和 kbuild-file 展开:

makefile
kbuild-dir=./scripts/basic
kbuild-file=./scripts/basic/Makefile
include ./scripts/basic/Makefile

继续往后分析,就可以看到这个目标 Makefile.build - scripts/Makefile.build - __build:

makefile
# We keep a list of all modules in $(MODVERDIR)

__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
	 $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
	 $(subdir-ym) $(always)
	@:

__build 是默认目标,因为命令“@make -f ./scripts/Makefile.build obj = scripts/basic”没有指定目标,所以会使用到默认目标__build。在顶层 Makefile 中,KBUILD_BUILTIN 为 1, KBUILD_MODULES 为空,因此展开后目标__build 为:

makefile
__build:$(builtin-target) $(lib-target) $(extra-y)) $(subdir-ym) $(always)
	@:

可以看出目标__build 有 5 个依赖:builtin-target、lib-target、extra-y、subdir-ym 和 always。我们直接添加打印信息看一下:

makefile
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
	 $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
	 $(subdir-ym) $(always)
	@:
	@echo "builtin-target="$(builtin-target) 
	@echo "lib-target="$(lib-target) 
	@echo "extra-y="$(extra-y)
	@echo "subdir-ym="$(subdir-ym) 
	@echo "always="$(always)

然后执行:

shell
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig

会看到这 5 个依赖的具体内容如下:

makefile
builtin-target=
lib-target=
extra-y=
subdir-ym=
always=scripts/basic/fixdep

所以最终的__build 为:

makefile
__build: scripts/basic/fixdep
	@:

__build 依赖于 scripts/basic/fixdep,所以要先将 scripts/basic/fixdep 文件编译成 fixdep。

综上所述,scripts_basic 目标的作用就是编译出 scripts/basic/fixdep 软件。

5.2 %config 目标对应的命令 ​

%config 目标对应的命令为:

makefile
make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig

我们推测一下,Makefile.build - scripts/Makefile.build 里面应该有一个 defconfig 相关的目标才对,但是去搜一下好像没有,我们和前面一样,先看一下 Makefile.build - scripts/Makefile.build 中的 kbuild-dir 和 kbuild-file:

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)

我们知道 srctree = .,src = scripts/kconfig,所以这里展开就是:

makefile
kbuild-dir = ./scripts/kconfig
kbuild-file = ./scripts/kconfig/Makefile
include ./scripts/kconfig/Makefile

可以看出,Makefile.build 会读取 scripts/kconfig/Makefile 中的内容,此文件有如下所示内容 Makefile - scripts/kconfig/Makefile - %_defconfig:

makefile
%_defconfig: $(obj)/conf
	$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)

目标%_defconfig 与 xxx_defconfig 匹配,所以会执行这条规则,SRCARCH 定义在 Makefile - SRCARCH,我们编译的是 ARM 平台,这里就有 ARCARCH = arm。Kconfig 定义在 Makefile - scripts/kconfig/Makefile - Kconfig,最后它的值为 Kconfig,这里将其展开就是:

makefile
%_defconfig: scripts/kconfig/conf
    @ scripts/kconfig/conf --defconfig=arch/arm/configs/%_defconfig Kconfig

%_defconfig 依赖 scripts/kconfig/conf,所以会编译 scripts/kconfig/conf.c 生成 conf 这个软件。 此软件就会将 %_defconfig 中的配置输出到.config 文件中,最终生成 Linux kernel 根目录下的.config 文件。具体怎么生成的这里就没有仔细去研究了,大概知道这里是这样生成的就可以了。

三、make all ​

使用命令“make xxx_defconfig”配置好 Linux 内核以后就可以使用“make”或者“make all” 命令进行编译。

1. _all 目标 ​

我们先来看一下顶层 Makefile 中关于_all 目标的一些相关的内容,它在 Makefile - _all 和 Makefile - _all:

makefile
# That's our default target when none is given on the command line
PHONY := _all
_all:

# ......

# 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     # (1)
else
_all: modules # (2)
endif

_all 是默认目标,如果使用命令“make”编译 Linux 的话此目标就会被匹配。继续往下的话,我们看到还有一个变量来控制默认目标的依赖,如果 KBUILD_EXTMOD 为空的话 ifeq 中的(1)代码成立。添加打印信息就会发现这里我们执行 make all 的话,(1)是成立的,所以_all 目标又依赖于 all。

2. all 目标 ​

我们再看一下 all 目标,它定义在 Makefile - all :

makefile
# 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

目标 all 依赖 vmlinux,所以接下来的重点就是 vmlinux!

3. vmlinux 目标 ​

3.1 vmlinux 简单展开 ​

vmlinux 目标定义在 Makefile - vmlinux:

makefile
vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
ifdef CONFIG_HEADERS_CHECK
	$(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_GDB_SCRIPTS
	$(Q)ln -fsn $(abspath $(srctree)/scripts/gdb/vmlinux-gdb.py)
endif
	+$(call if_changed,link-vmlinux)

可以看出目标 vmlinux 依赖 scripts/link-vmlinux.sh、vmlinux-deps 和 FORCE,其中 vmlinux-deps 定义在 Makefile - vmlinux-deps :

makefile
vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN) $(KBUILD_VMLINUX_LIBS)

这几个变量都定义在这个位置 Makefile - Externally visible symbols:

makefile
# 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

前面我们知道 SRCARCH = arm,所以 KBUILD_LDS:= arch/arm/kernel/vmlinux.lds,所以 vmlinux 的依赖为:

makefile
scripts/link-vmlinux.sh autoksyms_recursive $(head-y) $(init-y) $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y) $(libs-y1) arch/$(SRCARCH)/kernel/vmlinux.lds FORCE

我们搜索一下 CONFIG_HEADERS_CHECK 和 CONFIG_GDB_SCRIPTS,会发现 NXP 官方的 evk 评估板所对应的默认配置文件 imx_v6_v7_defconfig 是没有这两个定义的,最终的.config 中也没有配置这两个选项。这里就先不管了,然后我们展开一下这个 vmlinux:

makefile
vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(head-y) $(init-y) $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y) $(libs-y1) arch/$(SRCARCH)/kernel/vmlinux.lds FORCE
	+$(call if_changed,link-vmlinux)

当依赖准备好后,调用 if_changed 这个函数就是生成 vmlinux。

3.2 依赖的变量 ​

3.2.1 head-y ​

head-y 这个变量是定义在 Makefile - arch/arm/Makefile - head-y:

makefile
#Default value
head-y		:= arch/arm/kernel/head$(MMUEXT).o

当不使能 MMU 的话 MMUEXT =-nommu,如果使能 MMU 的话为空,因此 head-y 最终的 值为:

makefile
head-y = arch/arm/kernel/head.o
3.2.2 init-y、drivers-y 和 net-y ​

init-y、drivers-y 和 net-y 这三个变量是定义在 Makefile - Objects we will link into vmlinux / subdirs we need to visit:

makefile
init-y		:= init/
drivers-y	:= drivers/ sound/ firmware/
net-y		:= net/

还有一个位置也有,在这里 Makefile:

makefile
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))

所以 init-y、drivers-y 和 net-y 最终的值为:

makefile
nit-y = init/built-in.a
drivers-y = drivers/built-in.a sound/built-in.a firmware/built-in.a
net-y = net/built-in.a
3.2.3 libs-y1 和 libs-y2 ​

libs-y1、 libs-y2 基本和 init-y 一样,在顶层 Makefile - libs-y1 和 libs-y2 中有如下定义:

makefile
libs-y1		:= $(patsubst %/, %/lib.a, $(libs-y))
libs-y2		:= $(patsubst %/, %/built-in.a, $(filter-out %.a, $(libs-y)))

其中 libs-y 定义在 Makefile - libs-y:

makefile
libs-y		:= lib/

另外,这个 libs-y 在 Makefile - arch/arm/Makefile - libs-y 中也有定义:

makefile
libs-y				:= arch/arm/lib/ $(libs-y)

最终 libs-y 的值为:

makefile
libs-y = arch/arm/lib lib/

libs-y2 中 filter-out 是反过滤函数,就是匹配除.a 结尾的文件以外的文件,然后再匹配目录中的 built-in.a,所以这里就有

makefile
libs-y1		:= arch/arm/lib/lib.a lib/lib.a
libs-y2		:= arch/arm/lib/built-in.a lib/built-in.a
3.2.4 core-y ​

core-y 定义在 Makefile - Objects we will link into vmlinux / subdirs we need to visit:

makefile
core-y		:= usr/

在 Makefile - core-y 这里会追加一些值:

makefile
core-y		+= kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/
#......
core-y		:= $(patsubst %/, %/built-in.a, $(core-y))

在 Makefile - arch/arm/Makefile - core-y 也会追加一些值:

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/

# If we have a machine-specific directory, then include it in the build.
core-y				+= arch/arm/kernel/ arch/arm/mm/ arch/arm/common/
core-y				+= arch/arm/probes/
core-y				+= arch/arm/net/
core-y				+= arch/arm/crypto/
core-y				+= arch/arm/firmware/
core-y				+= $(machdirs) $(platdirs)

这里有一部分根据不同的配置向 core-y 追加不同的值,比如使能 VFP 的话就会在.config 中有 CONFIG_VFP = y 这一行,那么 core-y 就会追加“arch/arm/vfp/”。另外一部分就是对 core-y 直接追加的值。所以总的来说:

makefile
core-y		        := usr/
core-y		        += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/
core-y				+= arch/arm/kernel/ arch/arm/mm/ arch/arm/common/
core-y				+= arch/arm/probes/
core-y				+= arch/arm/net/
core-y				+= arch/arm/crypto/
core-y				+= arch/arm/firmware/
core-y				+= $(machdirs) $(platdirs)
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 - core-y:

makefile
core-y		:= $(patsubst %/, %/built-in.a, $(core-y))

这里是 core-y 最终匹配的文件,就是匹配上面所有的目录中的 built-in.a 文件。

3.2.5 总结 ​

这些变量都是一些 built-in.o 或.a 等文件,这个和 uboot 一样,都是将相应目录中的源码文件进行编译,然后在各自目录下生成 built-in.o 文件,有些生成了.a 库文件。最终将这些 built-in.o 和.a 文 件进行链接即可形成 ELF 格式的可执行文件,也就是 vmlinux!但是链接是需要链接脚本的, vmlinux 的依赖 arch/arm/kernel/vmlinux.lds 就是整个 Linux 的链接脚本。

3.3 if_changed 函数 ​

接下来我们来看一下 if_changed 函数,前面我们将 vmlinux 目标的规则展开得到:

makefile
vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(head-y) $(init-y) $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y) $(libs-y1) arch/$(SRCARCH)/kernel/vmlinux.lds FORCE
	+$(call if_changed,link-vmlinux)

这里 + 表示该命令结果不可忽略。这里表示将 if_changed 函数的结果作为最终生成 vmlinux 的命令。,link-vmlinux 是函数 if_changed 的 参数,函数 if_changed 定义在文件 Kbuild.include - scripts/Kbuild.include - if_changed 中:

makefile
# 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 用于检查依赖文件是否有变化,如果依赖文件有变化那么 any-prereq 就不为 空,否则就为空。

  • arg-check 用于检查参数是否有变化,如果没有变化那么 arg-check 就为空。

  • @set -e 告诉 bash,如果任何语句的执行结果不为 true(也就是执行出错)的 话就直接退出。

  • $(echo-cmd) 用于打印命令执行过程,比如在链接 vmlinux 的时候就会输出 “LINK vmlinux”。

  • $(cmd_$(1)) 中的 $(1) 表示参数,也就是 link-vmlinux,因此 $(cmd_$(1)) 表示 执行 cmd_link-vmlinux 的内容。

3.3.1 any-prereq ​

any-prereq 定义在 Kbuild.include - scripts/Kbuild.include - any-prereq:

makefile
# Find any prerequisites that is newer than target or that does not exist.
# PHONY targets skipped in both cases.
any-prereq = $(filter-out $(PHONY),$?) $(filter-out $(PHONY) $(wildcard $^),$^)
3.3.2 arg-check ​

arg-check 定义在 Kbuild.include - scripts/Kbuild.include - arg-check:

makefile
ifneq ($(KBUILD_NOCMDDEP),1)
# Check if both arguments are the same including their order. Result is empty
# string if equal. User may override this check using make KBUILD_NOCMDDEP=1
arg-check = $(filter-out $(subst $(space),$(space_escape),$(strip $(cmd_$@))), \
                         $(subst $(space),$(space_escape),$(strip $(cmd_$1))))
else
arg-check = $(if $(strip $(cmd_$@)),,1)
endif
3.3.3 cmd_$(1) ​

接下来看一下这个 cmd_$(1),前面我们知道这个是 cmd_link-vmlinux,它定义在 Makefile - cmd_link-vmlinux:

makefile
# Final link of vmlinux with optional arch pass after final link
cmd_link-vmlinux =                                                 \
	$(CONFIG_SHELL) $< $(LD) $(KBUILD_LDFLAGS) $(LDFLAGS_vmlinux) ;    \
	$(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)
makefile
# SHELL used by kbuild
CONFIG_SHELL := $(shell if [ -x "$$BASH" ]; then echo $$BASH; \
	  else if [ -x /bin/bash ]; then echo /bin/bash; \
	  else echo sh; fi ; fi)

最终 CONFIG_SHELL =/bin/bash。

  • $< 表示目标 vmlinux 的第一个依赖文件:
makefile
vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
ifdef CONFIG_HEADERS_CHECK
	$(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_GDB_SCRIPTS
	$(Q)ln -fsn $(abspath $(srctree)/scripts/gdb/vmlinux-gdb.py)
endif
	+$(call if_changed,link-vmlinux)

在这里 $< 就是 scripts/link-vmlinux.sh。

其实这个地方由于涉及的地方较多,我们可以加个打印信息,这个 Makefile - arch/arm/Makefile 文件在 Makefile 中被调用,LDFLAGS_vmlinux 在 Makefile - LDFLAGS_vmlinux 这里被导出,我们可以加个打印信息看一下:

makefile
    echo "LDFLAGS_vmlinux=$(LDFLAGS_vmlinux)"
    +$(call if_changed,link-vmlinux)

然后我们执行以下命令:

shell
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16

会看到以下输出信息。

shell
LDFLAGS_vmlinux=-p --no-undefined -X --pic-veneer  --build-id
makefile
ARCH_POSTLINK := $(wildcard $(srctree)/arch/$(SRCARCH)/Makefile.postlink)

srctree = .,SRCARCH = arm,所以这里就是./arch/arm/Makefile.postlink,这个应该是编译生成的文件吧,我也不是很清楚,我去使用 V = 1 参数编译了一下,没有在编译过程中发现这个文件,也没有在编译完成后在 ./arch/arm 目录下发现这个文件,所以这里这个 ARCH_POSTLINK 应该是为空。

  • $@ 就是目标文件 vmlinux

所以展开就是:

makefile
# Final link of vmlinux with optional arch pass after final link
cmd_link-vmlinux =                                                 \
	/bin/bash scripts/link-vmlinux.sh -p --no-undefined -X --pic-veneer  --build-id; \
	true

我们来看一下这个脚本,它在 link-vmlinux.sh - scripts/link-vmlinux.sh:

shell
# Link of vmlinux
# ${1} - optional extra .o files
# ${2} - output file
vmlinux_link()
{
	local lds="${objtree}/${KBUILD_LDS}"
	local objects

	if [ "${SRCARCH}" != "um" ]; then
		objects="--whole-archive			\
			built-in.a				\
			--no-whole-archive			\
			--start-group				\
			${KBUILD_VMLINUX_LIBS}			\
			--end-group				\
			${1}"

		${LD} ${KBUILD_LDFLAGS} ${LDFLAGS_vmlinux} -o ${2}	\
			-T ${lds} ${objects}
	else
		objects="-Wl,--whole-archive			\
			built-in.a				\
			-Wl,--no-whole-archive			\
			-Wl,--start-group			\
			${KBUILD_VMLINUX_LIBS}			\
			-Wl,--end-group				\
			${1}"

		${CC} ${CFLAGS_vmlinux} -o ${2}			\
			-Wl,-T,${lds}				\
			${objects}				\
			-lutil -lrt -lpthread
		rm -f linux
	fi
}
# ......
info LD vmlinux
vmlinux_link "${kallsymso}" vmlinux

vmliux_link 就是最终链接出 vmlinux 的函数。

  • 第 9 行判断 SRCARCH 是否等于“um”,如 果不相等的话就执行 20~34 行的代码。因为 SRCARCH = arm,因此条件成立,执行 10~19 行的代码。

  • 第 18 ~ 19 行的代码就是我们之前的链接操作。链接脚本 lds = ./arch/arm/kernel/vmlinux.lds。需要链接的文件由变量 KBUILD_VMLINUX_INIT 和 KBUILD_VMLINUX_MAIN 来决定。

KBUILD_VMLINUX_INIT 定义在 Makefile - KBUILD_VMLINUX_INIT:

makefile
export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)

KBUILD_VMLINUX_MAIN 定义在 Makefile - KBUILD_VMLINUX_MAIN:

makefile
export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y)

前面基本已经分析过了,但 $(virt-y) 这个刚才没分析,不过都是一样的道理。

  • 第 37 行调用 vmlinux_link 函数来链接出 vmlinux。

3.4 打印信息 ​

上面分析了那么多,我们详细来看一下打印信息吧,我们添加个起始和结尾标记,方便查找:

makefile
vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
	echo "========== vmlinux-start =========="
ifdef CONFIG_HEADERS_CHECK
	$(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_GDB_SCRIPTS
	$(Q)ln -fsn $(abspath $(srctree)/scripts/gdb/vmlinux-gdb.py)
endif
	echo "========== vmlinux-if_changed =========="
	+$(call if_changed,link-vmlinux)
	echo "========== vmlinux-end =========="

然后我们执行以下命令:

shell
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16

然后就可以看到打印信息中这个 vmlinux 规则都在做些什么,这里是一些简化后的重要打印信息:

shell
echo "========== vmlinux-start =========="
========== vmlinux-start ==========
echo "========== vmlinux-if_changed =========="
========== vmlinux-if_changed ==========
  /bin/sh scripts/link-vmlinux.sh arm-linux-gnueabihf-ld  -EL -p --no-undefined -X --pic-veneer  --build-id ;  true
+ '[' arm-linux-gnueabihf-ld = clean ']'
+ case "${KCONFIG_CONFIG}" in
+ . ./.config
++ CONFIG_CC_IS_GCC=y
++ CONFIG_GCC_VERSION=40902

# 中间部分省略......

+ local objects
+ '[' arm '!=' um ']'
# 中间部分省略......
+ arm-linux-gnueabihf-ld -EL -p --no-undefined -X --pic-veneer --build-id -o vmlinux -T ./arch/arm/kernel/vmlinux.lds --whole-archive built-in.a --no-whole-archive --start-group arch/arm/lib/lib.a lib/lib.a --end-group .tmp_kallsyms2.o
+ '[' -n y ']'
+ info SORTEX vmlinux
+ '[' '' '!=' silent_ ']'
+ printf '  %-7s %s\n' SORTEX vmlinux
  SORTEX  vmlinux
+ sortextable vmlinux
+ ./scripts/sortextable vmlinux
+ info SYSMAP System.map
# 中间部分省略......
echo "========== vmlinux-end =========="
========== vmlinux-end ==========

从这里看,前面分析的一些变量的值都是正确的。

4. built-in.a 生成过程 ​

4.1 vmlinux-deps 变量 ​

前面在分析 Makefile - vmlinux 的时候:

makefile
vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
ifdef CONFIG_HEADERS_CHECK
	$(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_GDB_SCRIPTS
	$(Q)ln -fsn $(abspath $(srctree)/scripts/gdb/vmlinux-gdb.py)
endif
	+$(call if_changed,link-vmlinux)

vmlinux 是依赖 vmlinux-deps 的,而 vmlinux-deps 定义在 Makefile - vmlinux-deps :

makefile
vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN) $(KBUILD_VMLINUX_LIBS)

这几个变量都定义 Makefile - Externally visible symbols:

makefile
# 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

上面些我们前面都分析过了,他们最终是一系列的 built-in.a 文件。我们可以加个打印信息看一眼:

makefile
vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
	@echo "vmlinux-deps=$(vmlinux-deps)"
ifdef CONFIG_HEADERS_CHECK
	$(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_GDB_SCRIPTS
	$(Q)ln -fsn $(abspath $(srctree)/scripts/gdb/vmlinux-gdb.py)
endif
	+$(call if_changed,link-vmlinux)

然后我们执行以下命令:

shell
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16

可以看到有如下打印信息:

shell
vmlinux-deps=arch/arm/kernel/vmlinux.lds arch/arm/kernel/head.o  init/built-in.a  usr/built-in.a  arch/arm/vfp/built-in.a  arch/arm/vdso/built-in.a  arch/arm/kernel/built-in.a  arch/arm/mm/built-in.a  arch/arm/common/built-in.a  arch/arm/probes/built-in.a  arch/arm/net/built-in.a  arch/arm/crypto/built-in.a  arch/arm/firmware/built-in.a  arch/arm/mach-imx/built-in.a  kernel/built-in.a  certs/built-in.a  mm/built-in.a  fs/built-in.a  ipc/built-in.a  security/built-in.a  crypto/built-in.a  block/built-in.a  arch/arm/lib/built-in.a  lib/built-in.a  drivers/built-in.a  sound/built-in.a  firmware/built-in.a  net/built-in.a  virt/built-in.a  arch/arm/lib/lib.a  lib/lib.a

除了 arch/arm/kernel/vmlinux.lds 以外,其他都是要编译链接生成的。

4.2 vmlinux-deps 的依赖 ​

我们继续往下看 vmlinux-deps,会看到

makefile
# The actual objects are generated when descending,
# make sure no implicit rule kicks in
$(sort $(vmlinux-deps)): $(vmlinux-dirs) ;

sort 函数就是用来排序的,它一般格式如下:

makefile
$(sort <list>)

给字符串 list 中的单词排序(升序),会返回排序后的字符串。另外 sort 函数会去掉 list 中相同的单词。另外可以看出 vmlinux-deps 依赖 vmlinux-dirs。

4.3 vmlinux-dirs 变量 ​

vmlinux-dirs 也定义在顶层 Makefile - vmlinux-dirs:

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) $(virt-y)))
		     
# ......
# Build samples along the rest of the kernel. This needs headers_install.
ifdef CONFIG_SAMPLES
vmlinux-dirs += samples
samples: headers_install
endif

vmlinux-dirs 看名字就知道和目录有关,此变量保存着生成 vmlinux 所需源码文件的目录。这里不分析了,直接添加打印信息吧,还是在这个 vmlinux 目标这里加:

makefile
vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
	@echo "vmlinux-dirs=$(vmlinux-dirs)"
ifdef CONFIG_HEADERS_CHECK
	$(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_GDB_SCRIPTS
	$(Q)ln -fsn $(abspath $(srctree)/scripts/gdb/vmlinux-gdb.py)
endif
	+$(call if_changed,link-vmlinux)

和前面一样,我们编译一遍,会得到如下打印信息:

makefile
vmlinux-dirs=init usr arch/arm/vfp arch/arm/vdso arch/arm/kernel arch/arm/mm arch/arm/common arch/arm/probes arch/arm/net arch/arm/crypto arch/arm/firmware arch/arm/mach-imx kernel certs mm fs ipc security crypto block drivers sound firmware net arch/arm/lib lib virt

4.4 vmlinux-dirs 的依赖 ​

4.4.1 规则展开 ​

我们继续往后看,会看到 vmlinux-dirs 也是有依赖的,它定义在 Makefile - vmlinux-dirs:

makefile
# Handle descending into subdirectories listed in $(vmlinux-dirs)
# Preset locale variables to speed up the build process. Limit locale
# tweaks to this spot to avoid wrong language settings when running
# make menuconfig etc.
# Error messages still appears in the original language

PHONY += $(vmlinux-dirs)
$(vmlinux-dirs): prepare scripts
	$(Q)$(MAKE) $(build)=$@ need-builtin=1

目标 vmlinux-dirs 依赖 prepare 和 scripts 。这两个依赖就暂时不去详细了解了。我们重点看一下第 9 行的命令。build 前面已经说过了,它定义在 Kbuild.include - scripts/Kbuild.include - build,值为 " -f ./scripts/Makefile.build obj ",所以这一行命令展开就是:

makefile
@make -f ./scripts/Makefile.build obj=$@ need-builtin=1

$@ 表示目标文件,也就是 vmlinux-dirs 的值,将 vmlinux-dirs 中的这些目录全部带入到命令中,结果如下:

makefile
@make -f ./scripts/Makefile.build obj=init need-builtin=1
@make -f ./scripts/Makefile.build obj=usr need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/vfp need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/vdso need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/kernel need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/mm need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/common need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/probes need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/net need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/crypto need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/firmware need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/mach-imx need-builtin=1
@make -f ./scripts/Makefile.build obj=kernel need-builtin=1
@make -f ./scripts/Makefile.build obj=certs need-builtin=1
@make -f ./scripts/Makefile.build obj=mm need-builtin=1
@make -f ./scripts/Makefile.build obj=fs need-builtin=1
@make -f ./scripts/Makefile.build obj=ipc need-builtin=1
@make -f ./scripts/Makefile.build obj=security need-builtin=1
@make -f ./scripts/Makefile.build obj=crypto need-builtin=1
@make -f ./scripts/Makefile.build obj=block need-builtin=1
@make -f ./scripts/Makefile.build obj=drivers need-builtin=1
@make -f ./scripts/Makefile.build obj=sound need-builtin=1
@make -f ./scripts/Makefile.build obj=firmware need-builtin=1
@make -f ./scripts/Makefile.build obj=net need-builtin=1
@make -f ./scripts/Makefile.build obj=arch/arm/lib need-builtin=1
@make -f ./scripts/Makefile.build obj=lib need-builtin=1
@make -f ./scripts/Makefile.build obj=virt need-builtin=1

这些命令运行过程其实都是一样的,我们就以“@ make -f ./scripts/Makefile.build obj = init need-builtin = 1”这个命令为例 来分析一下。

4.4.2 命令分析 ​

我们先看这个 Makefile.build - scripts/Makefile.build 中的默认目标__build:

makefile
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
	 $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
	 $(subdir-ym) $(always)
	@:

当只编译 Linux 内核镜像文件,也就是使用“ make zImage ” 编译的时候,KBUILD_BUILTIN = 1, KBUILD_MODULES 为空。“make”命令是会编译所有的东西,包括 Linux 内核镜像文件和一些模块文件。我们这里简单一点,如果只编译 Linux 内核镜像的话, __build 目标简化为:

makefile
__build: $(builtin-target) $(lib-target) $(extra-y)) $(subdir-ym) $(always)
	@:

重点来看一下 builtin-target 这个依赖, builtin-target 同样定义在文件 Makefile.build - scripts/Makefile.build - builtin-target 中 :

makefile
ifneq ($(strip $(real-obj-y) $(need-builtin)),)
builtin-target := $(obj)/built-in.a
endif

第 2 行就是 builtin-target 变量的值,为 $(obj)/built-in.a,这就是这些 built-in.a 的来源了。要生成 built-in.a,要求 real-obj-y 和 need-builtin 变量不能全部为空。我们以这条命令为例:

makefile
@make -f ./scripts/Makefile.build obj=init need-builtin=1

可以看到这里 obj = init,need-builtin = 1,所以上面的 builtin-target 可以展开一下:

makefile
ifneq ($(strip $(real-obj-y) 1),)
builtin-target := init/built-in.a
endif

那 built-in.a 是怎么生成的?我们继续看这个 builtin-target,在文件 Makefile.build - scripts/Makefile.build 中有如下代码:

makefile
#
# Rule to compile a set of .o files into one .o file
#
ifdef builtin-target

# built-in.a archives are made with no symbol table or index which
# makes them small and fast, but unable to be used by the linker.
# scripts/link-vmlinux.sh builds an aggregate built-in.a with a symbol
# table and index.
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)

targets += $(builtin-target)
endif # builtin-target

可以看到第 14 行,目标就是 builtin-target,依赖为 real-obj-y,命令为“$(call if_changed,ar_builtin)”,前面我们分析过这个 if_changed 函数,这里参数为 ar_builtin,其返回值就是具体的命令。 我们可以放到这里再看一下 Kbuild.include - scripts/Kbuild.include - if_changed :

makefile
# 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, @:)

它会调用 cmd_$(1) 所对应的命令($(1) 就是函数的第 1 个参数),在这里就是调用 cmd_ar_builtin 所对应的命令。这里面的命令就是使用 AR 工具生成 .a 文件

4.5 打印信息 ​

我们在终端执行:

shell
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16

然后我们会看到以下打印信息:

shell
  rm -f init/built-in.a; arm-linux-gnueabihf-ar rcSTPD init/built-in.a init/main.o init/version.o init/do_mounts.o init/do_mounts_rd.o init/do_mounts_initrd.o init/initramfs.o init/calibrate.o init/init_task.o

这个就是最后将相关的.o 链接在一起生成的 init/built-in.a 文件的打印信息。命令 arm-linux-gnueabihf-ar rcSTPD 中,rcSTPD 是一系列选项的组合,用于指定 arm-linux-gnueabihf-ar 工具的操作模式。下面是每个选项的含义:

  • r:替换。如果指定的库文件中已经存在要添加的文件,则替换它。
  • c:创建。创建一个新的库文件。如果指定的库文件不存在,则创建它;如果已存在,则根据其他选项(如 r)更新它。
  • S:创建索引。为库文件创建一个目标文件索引,这可以加快之后链接时库文件的搜索速度。
  • T:使用给定的文件名作为库文件的临时索引文件。通常,这个选项后面会跟着一个文件名。但在你提供的命令中,T 后面没有直接跟随文件名,这可能是一个错误或者遗漏,或者是在特定上下文中有特殊用法。
  • P:保留路径。在将文件添加到库中时,保留文件的路径信息。默认情况下,ar 工具会将文件路径剥离,只保留文件名。使用 P 选项可以保留完整的路径。
  • D:通常 D 选项用于删除库中的文件。这里提及是为了完整性,因为 D 是 ar 工具的一个常用选项。

四、make zImage ​

1. vmlinux、 Image, zImage、 uImage 的区别 ​

其实前面已经大概了解过了:

  • vmlinux 是编译出来的最原始的 ELF 格式内核文件,是未压缩的。
  • Image 是 Linux 内核镜像文件,但是 Image 仅包含可执行的二进制数据。 Image 就是使用 objcopy 取消掉 vmlinux 中的一些其他信息,比如符号表什么的。但是 Image 是没有压缩过的, Image 保存在 arch/arm/boot 目录下 。
  • zImage 是经过 gzip 压缩后的 Image 。
  • uImage 是老版本 uboot 专用的镜像文件, uImag 是在 zImage 前面加了一个长度为 64 字节的“头”,这个头信息描述了该镜像文件的类型、加载位置、生成时间、大小等信息。但是新的 uboot 已经支持了 zImage 启动!所以已经很少用到 uImage 了,除非我们用的很古老的 uboot。

所以一般我们还是使用 zImage 这个镜像文件。

2. zImage ​

使用“ make”、“ make all”、“ make zImage”这些命令都可以编译出 zImage 镜像, 我们来看一下 BOOT_TARGETS 变量,它定义在 Makefile - arch/arm/Makefile - BOOT_TARGETS:

makefile
BOOT_TARGETS	= zImage Image xipImage bootpImage uImage
INSTALL_TARGETS	= zinstall uinstall install

PHONY += bzImage $(BOOT_TARGETS) $(INSTALL_TARGETS)

bootpImage uImage: zImage
zImage: Image

$(BOOT_TARGETS): vmlinux
	$(Q)$(MAKE) $(build)=$(boot) MACHINE=$(MACHINE) $(boot)/$@
	@$(kecho) '  Kernel: $(boot)/$@ is ready'

第 1 行。变量 BOOT_TARGETS 包含 zImage, Image, xipImage 等镜像文件。

第 9 行, BOOT_TARGETS 依赖 vmlinux,因此如果使用“make zImage”编译的 Linux 内核的话,首先肯定要先编译出 vmlinux。

第 10 行,具体的命令,比如要编译 zImage,那么命令展开以后如下所示:

makefile
@ make -f ./scripts/Makefile.build obj=arch/arm/boot MACHINE=arch/arm/boot/zImage

和前面一样,都是使用 scripts/Makefile.build 完成 vmlinux 到 zImage 的转换。