LV05-03-Kernel-03-01-顶层Makefile基础解析

本文主要是kernel——顶层Makefile基础解析的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、概述

其实大概翻一下linux内核的顶层 Makefile 就会发现,它和u-boot的顶层 Makefile 结构是很类似的,大部分都差不多,之前已经详细去分析过u-boot的顶层Makefile了,可以看这里:LV05-02-U-Boot-06-01-顶层Makefile基础解析 | 苏木。这里一些基础的东西就不再详细分析了。

二、make xxx_defconfig

1. 打印信息

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

1
2
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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

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

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

1
2
3
4
5
6
%config: scripts_basic outputmakefile FORCE
@echo "Q=$(Q)"
@echo "MAKE=$(MAKE)"
@echo "build=$(build)"
@echo "@=$@"
$(Q)$(MAKE) $(build)=scripts/kconfig $@

然后执行:

1
2
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

就会看到如下信息:

1
2
3
4
Q=
MAKE=make
build=-f ./scripts/Makefile.build obj
@=imx_v6_v7_defconfig

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

1
2
%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

1
2
3
4
5
###
# 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=.。所以这里就是:

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

4. scripts_basic

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

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

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

1
2
3
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

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

5.1 scripts_basic 目标对应的命令

scripts_basic 目标对应的命令为:

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

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

1
2
3
4
# 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

1
src := $(obj)

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

1
2
3
kbuild-dir=./scripts/basic
kbuild-file=./scripts/basic/Makefile
include ./scripts/basic/Makefile

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

1
2
3
4
5
6
# 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 为:

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

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

1
2
3
4
5
6
7
8
9
__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)

然后执行:

1
2
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 个依赖的具体内容如下:

1
2
3
4
5
builtin-target=
lib-target=
extra-y=
subdir-ym=
always=scripts/basic/fixdep

所以最终的__build为:

1
2
__build: scripts/basic/fixdep
@:

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

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

5.2 %config 目标对应的命令

%config 目标对应的命令为:

1
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:

1
2
3
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,所以这里展开就是:

1
2
3
kbuild-dir = ./scripts/kconfig
kbuild-file = ./scripts/kconfig/Makefile
include ./scripts/kconfig/Makefile

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

1
2
%_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,这里将其展开就是:

1
2
%_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 - _allMakefile - _all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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

1
2
3
4
5
# 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

1
2
3
4
5
6
7
8
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

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

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

1
2
3
4
5
# 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的依赖为:

1
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:

1
2
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

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

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

1
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

1
2
3
init-y		:= init/
drivers-y := drivers/ sound/ firmware/
net-y := net/

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

1
2
3
4
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 最终的值为:

1
2
3
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 中有如下定义:

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

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

1
libs-y		:= lib/

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

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

最终libs-y的值为:

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

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

1
2
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

1
core-y		:= usr/

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 直接追加的值。所以总的来说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
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目标的规则展开得到:

1
2
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 中:

1
2
3
4
5
# 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

1
2
3
# 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

1
2
3
4
5
6
7
8
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

1
2
3
4
# 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)
1
2
3
4
# 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 的第一个依赖文件:
1
2
3
4
5
6
7
8
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 这里被导出,我们可以加个打印信息看一下:

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

然后我们执行以下命令:

1
2
3
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

会看到以下输出信息。

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

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

  • $@就是目标文件vmlinux

所以展开就是:

1
2
3
4
# 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 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

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

KBUILD_VMLINUX_MAIN定义在Makefile - KBUILD_VMLINUX_MAIN

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

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

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

3.4 打印信息

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

1
2
3
4
5
6
7
8
9
10
11
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 =========="

然后我们执行以下命令:

1
2
3
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规则都在做些什么,这里是一些简化后的重要打印信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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的时候:

1
2
3
4
5
6
7
8
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

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

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

1
2
3
4
5
# 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文件。我们可以加个打印信息看一眼:

1
2
3
4
5
6
7
8
9
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)

然后我们执行以下命令:

1
2
3
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

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

1
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,会看到

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

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

1
$(sort <list>)

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

4.3 vmlinux-dirs变量

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

1
2
3
4
5
6
7
8
9
10
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目标这里加:

1
2
3
4
5
6
7
8
9
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)

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

1
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

1
2
3
4
5
6
7
8
9
# 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 “,所以这一行命令展开就是:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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:

1
2
3
4
__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 目标简化为:

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

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

1
2
3
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 变量不能全部为空。我们以这条命令为例:

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#
# 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

1
2
3
4
5
# 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 打印信息

我们在终端执行:

1
2
3
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

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

1
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 选项用于删除库中的文件。这里提及是为了完整性,因为 Dar 工具的一个常用选项。

四、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

1
2
3
4
5
6
7
8
9
10
11
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,那么命令展开以后如下所示:

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

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