Kbuild那些事儿

Kbuild机制梳理

Makefile的执行顺序

  1. 读入所有include的makefile(include时会对include命令后面的变量与通配符进行扩展,然后试着读入该Makefile,如果成功就继续,如果失败就报告,并继续读取其余makefile。直到所有读取全部完成后,查看规则中是否有更新该Makefile的规则,如果有,就更新目标,然后重新读入该Makefile。不断重复以上流程,直到所有更新Makefile的规则都被执行后,仍不存在该Makefile,就报错退出)
  2. 初始化变量
  3. 分析规则,将其加入依赖链
  4. 根据依赖,决定哪些目标需要生成
  5. 执行生成命令

命令脚本初始化顺序

  1. 读取命令脚本
  2. 扩展变量(执行的时候才会扩展,然而目标的变量扩展会在构建规则链时)
  3. 对Make表达式求值(宏被扩展时,会为每一行增加Tab)
  4. 执行
  • tips:注意区分Make表达式和shell表达式,shell表达式会在bash执行时求值

Kbuild相关文件的作用

文件名 作用
Makefile 顶层Makefile,执行的根目录
scritps/basic/Makefile 词法分析fixdep
scripts/Kbuild.include 常用函数
scritps/Makefile.userprogs 用户程序处理
scripts/Makefile.lib 常用变量的定义
scripts/Makefile.host 本地程序编译HOSTCC
arch/x86/Makefile 架构Makefile
arch/x86/boot/Makefile 架构的启动Makefile
arch/x86/kernel/Makefile 单目录下的Makefile
arch/x86/boot/compressed/Makefile 压缩后的Makefile

prepare依赖关系

graph LR;
	prepare-->prepare0;
	prepare-->prepare-objtool;
	prepare-->prepare-resolve_btfids;
	prepare0-->archprepare;
	archprepare-->outputmakefile;
	outputmakefile-->源码目录与输出目录不一致时启用;
	archprepare-->archheaders;
	archheaders-->产生系统调用表;
	archprepare-->archscripts;
	archscripts-->scripts_basic;
	archscripts-->架构脚本相关;
	archprepare-->scripts;
	scripts-->执行scripts目录下的Makefile;
	scripts-->scripts_basic;
	scripts-->scripts_dtc;
	archprepare-->include/config/kernel.release;
	include/config/kernel.release-->内核发行版本信息;
	archprepare-->asm-generic;
	asm-generic-->内核通用头文件;
	archprepare-->version_h;
	version_h-->内核版本信息;
	archprepare-->autoksyms_h;
	autoksyms_h-->内核符号信息;
	archprepare-->include/generated/utsrelease.h;
	include/generated/utsrelease.h-->设备信息;
	archprepare-->include/generated/autoconf.h;
	include/generated/autoconf.h-->.config信息;

bzImage依赖关系

graph LR;
	bzImage-->vmlinux;

一、config流程

构建内核,首先需要产生.config文件,.config文件需要在顶层makefile中设置config-build标志,注意区分config-build和need-config两个标志,config-build是构建.config文件,而need-config是指本次构建需要.config的参与,即需要.config中的配置项。

如果在本次构建中,还有其余目标,如clean目标,single目标,config目标等,则会设置mixed-build标志,即混合构建,此时make会通过__build_one_by_one依次去处理每个目标,而不是在本make中处理所有目标。

1
2
3
4
5
__build_one_by_one:
$(Q)set -e; \ #出错就停止
for i in $(MAKECMDGOALS); do \# 遍历每个目标
$(MAKE) -f $(srctree)/Makefile $$i; \#用顶层makefile去处理每个目标
done

而如果不需要混合构建,则进入了真正的重头戏,内核make每次的主要运行流程。

首先会包含Kbuild.include,这里面有一些通用的全局函数,如build,clean,if_changed等通用函数。dot-target,depfile等通用变量。

1
include scripts/Kbuild.include

然后通过一个makefile文件来确认当前架构。

1
2
3
4
5
6
7
8
9
include scripts/subarch.include
# 其中的内容就一行,确定SUBARCH
SUBARCH := $(shell uname -m | sed -e s/i.86/x86/ -e s/x86_64/x86/ \
-e s/sun4u/sparc64/ \
-e s/arm.*/arm/ -e s/sa110/arm/ \
-e s/s390x/s390/ -e s/parisc64/parisc/ \
-e s/ppc.*/powerpc/ -e s/mips.*/mips/ \
-e s/sh[234].*/sh/ -e s/aarch64.*/arm64/ \
-e s/riscv.*/riscv/)

这里要注意一个特殊的架构,um架构,即user mode,UML这里不是统一建模语言,而是UserMode Linux的缩写,从字面上看,是在用户态运行linux内核,即将内核当作一个应用程序在跑,这样我们就可以用调试应用层程序的方法调试内核了,应用层的强大调试工具gdb就派上用场了。很多时候我们写内核代码,当遇到算法比较复杂但又不涉及底层结构的时候总是喜欢现在应用层实现并调试,然后在写到内核层。为什么,就是因为用户层调试比内核调试方便。但是UML的最大局限性就是不能调试硬件关联性强的代码,但是还是有很多方面可以应用的,比如调度算法、VFS等。

可以看到,默认的架构是编译内核的架构

1
2
3
4
5
ARCH		?= $(SUBARCH)

# Architecture as present in compile.h
UTS_MACHINE := $(ARCH)
SRCARCH := $(ARCH)

HOST:本地编译的一些工具,如HOSTCC,HOSTLD,HOSTCXX等,用来编译本机上的一些工具,如mkproggy,fixdep等


下面开始正式构建.config的流程,首先看.config的定义位置

1
2
KCONFIG_CONFIG	?= .config
export KCONFIG_CONFIG

可以看到,默认是由KCONFIG_CONFIG这个变量名字来定义.config的,并且通过export来使其他makefile文件可以使用本变量。

下面执行流到真正构建.config的过程,首先顶层makefile定义了一个过程,由ifdef config-build开始,else分支则是不构建.config的执行流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ifdef config-build
## 则包含平台相关的makefile
# 构建.config肯定要使用架构相关的信息,因此首先包含架构中的Makefile文件
include arch/$(SRCARCH)/Makefile
export KBUILD_DEFCONFIG KBUILD_KCONFIG CC_VERSION_TEXT
#
## 这里是config和 %config的构建命令,其依赖于 scripts_basic outputmakefile
config: outputmakefile scripts_basic FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@

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

else #!config-build

可以看到导出了三个变量,分别是

1
2
3
4
KBUILD_DEFCONFIG  #在顶层makefile中定义,为export KBUILD_DEFCONFIG := defconfig,即默认的defconfig
KBUILD_KCONFIG # 仅由scripts/kconfig中的Makefile文件使用,后文说明
CC_VERSION_TEXT #gcc的版本信息
# CC_VERSION_TEXT = $(shell $(CC) --version 2>/dev/null | head -n 1)

make的特性,在include某个makefile时,会同步将其展开,因此这里会直接将arch/$(SRCARCH)/Makefile进行展开,并计算其中的变量和make操作。

以x86架构为例

首先根据真实架构确定使用的defconfig是哪个,即KBUILD_DEFCONFIG

1
2
3
4
5
6
7
8
9
10
# select defconfig based on actual architecture
ifeq ($(ARCH),x86)
ifeq ($(shell uname -m),x86_64)
KBUILD_DEFCONFIG := x86_64_defconfig
else
KBUILD_DEFCONFIG := i386_defconfig
endif
else
KBUILD_DEFCONFIG := $(ARCH)_defconfig
endif

接下来会定义一些架构相关的东西,和一些特定的功能,如FUNCTION_GRAPH_TRACER之类的,比较重要的是下面这些

1
2
3
4
5
6
7
8
9
# 架构相关的脚本
archscripts: scripts_basic
$(Q)$(MAKE) $(build)=arch/x86/tools relocs

###
# Syscall table generation
# 系统调用表
archheaders:
$(Q)$(MAKE) $(build)=arch/x86/entry/syscalls all

指定boot目录

1
2
3
boot := arch/x86/boot

KBUILD_IMAGE := $(boot)/bzImage

在x86架构下的默认动作是bzImage

1
2
3
4
5
6
7
8
9
10
all: bzImage

# bzImage会依赖vmlinux,注意这里的vmlinux没有指定前缀,那么这是顶层目录下的vmlinux
bzImage: vmlinux
ifeq ($(CONFIG_X86_DECODER_SELFTEST),y)
$(Q)$(MAKE) $(build)=arch/x86/tools posttest
endif
$(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE)
$(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot
$(Q)ln -fsn ../../x86/boot/bzImage $(objtree)/arch/$(UTS_MACHINE)/boot/$@

可以看到他的逻辑,不用管需要selftest的部分,首先使用一个make去执行boot目录下的makefile,并且指定目标是boot下的bzImage,然后在输出目录下建立boot目录,建立软链接,将输出目录下的bzImage和源码目录下产生的bzImage链接起来

上面的部分执行完,bzImage应该就顺利产生了,然而,本次的目标是构建.config文件,并没有all目标,这里可以注意到一个小细节,顶层makefile的默认目标是_all,而不是all,所以当执行.config的构建路径时,all目标并不会被执行。

1
2
3
4
5
6
7
# config是默认的config,%config是其他乱七八糟的config,如defconfig,i386_defconfig等
# 并且他们都有相同的依赖(prerequiries),outputmakefile和scripts_basic
config: outputmakefile scripts_basic FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@

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

其中,outputmakefile用来为源码目录和输出目录不一致时,为输出目录生成一份makeifle。scripts_basic用来生成词法分析器fixdep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
outputmakefile:
ifdef building_out_of_srctree
$(Q)if [ -f $(srctree)/.config -o \
-d $(srctree)/include/config -o \
-d $(srctree)/arch/$(SRCARCH)/include/generated ]; then \
echo >&2 "***"; \
echo >&2 "*** The source tree is not clean, please run 'make$(if $(findstring command line, $(origin ARCH)), ARCH=$(ARCH)) mrproper'"; \
echo >&2 "*** in $(abs_srctree)";\
echo >&2 "***"; \
false; \
fi
## 在当前目录创建一个符号链接
## 链接源码目录到输出目录下的source目录 -fsn是针对目录的,若已有符号链接,不跟随且覆盖原有定义
$(Q)ln -fsn $(srctree) source
### mkmakefile在输出目录生成了一个Makefile,以使得输出目录可以编译
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile $(srctree)
# ## 若输出目录存在.gitignore则结束,否则创建.gitignore,并写入 *,即忽略输出目录的所有文件
$(Q)test -e .gitignore || \
{ echo "# this is build directory, ignore it"; echo "*"; } > .gitignore
endif

可以看到,只有源码目录和输出目录不一致时,才会在输出目录下生成makefile,否则outputmakefile只是一个空操作。

1
2
3
4
5
# scripts_basic则会使用一个子make,去scirpts/basic目录下去执行makefile
PHONY += scripts_basic
scripts_basic:
$(Q)$(MAKE) $(build)=scripts/basic
$(Q)rm -f .tmp_quiet_recordmcount

scripts/basic目录下只有三个文件,一个.gitignore,一个fixdep.c,一个Makefile。

.gitignore不用管,只是版本控制文件,fixdep.c是词法分析器的c文件,使用HOSTCC进行编译,Makefile则是scripts_basic目标具体执行的命令,可以看到上面的命令,$(build)后面并没有执行具体的目标,那么Makefile.build则会执行默认目标,_build。

先看scripts/basic中的Makefile文件

1
hostprogs-always-y	+= fixdep

可以看到,就是单纯的为hostprogs-always-y增加了fixdep

而这个hostprogs-always-y在Makefile.lib中有定义,Makefile.lib文件是提供Kbuild中的一些变量定义,如各种编译的flag,obj-y,obj-m,subdir-ym,hostprogs,always-y,extra-y等编译的目标(Makefile.lib会被包含到Makefile.build文件中)。

其中hostprogs定义如下

1
hostprogs += $(hostprogs-always-y) $(hostprogs-always-m)

可以看到,fixdep最终被包含到hostprogs中,而hostprogs会在Makefile.build中被scripts/Makefile.host处理

1
2
3
4
5
6
7
8
9
10
11
# 先包含Makefile.lib,获得hostprogs变量
include scripts/Makefile.lib

# Do not include hostprogs rules unless needed.
# $(sort ...) is used here to remove duplicated words and excessive spaces.

## 若需要编译host相关的目标,则引入Makefile.host
hostprogs := $(sort $(hostprogs))
ifneq ($(hostprogs),)
include scripts/Makefile.host
endif

上面提到过,make在include一个makefile的时候,会直接将其进行展开,Makefile.host中会对hostprogs分类进行处理

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
# C code
# Executables compiled from a single .c file
# 从单个.c文件到可执行文件,看下面的代码,首先从hostprogs中获得某个成员,
# 如fixdep,然后查看fixdep-objs或者fixdep-cxxobjs是否存在,若存在则说明不是单个.c文件,若不存在,则将其加入到host-csingle中
host-csingle := $(foreach m,$(hostprogs), \
$(if $($(m)-objs)$($(m)-cxxobjs),,$(m)))

# C executables linked based on several .o files
# 基于若干.o生成可执行c文件,看下面的代码
# 还是以fixdep为例,首先查看fixdep-cxxobjs是否存在,若存在则为空,若不存在,则看fixdep-objs是否存在,若存在就将其放入host-cmulti中,其实就是找到不在-cxxobjs,但在-objs中的变量,放入host-cmulti中
host-cmulti := $(foreach m,$(hostprogs),\
$(if $($(m)-cxxobjs),,$(if $($(m)-objs),$(m))))

# Object (.o) files compiled from .c files
# 从.c编译到.o
# 查看代码,还是以fixdep为例,获取所有fixdep-objs的变量
host-cobjs := $(sort $(foreach m,$(hostprogs),$($(m)-objs)))

# C++ code
# C++ executables compiled from at least one .cc file
# and zero or more .c files
# 从.cc到可执行文件
# 找出所有存在-cxxobjs的变量,放入host-cxxmulti中
host-cxxmulti := $(foreach m,$(hostprogs),$(if $($(m)-cxxobjs),$(m)))

# C++ Object (.o) files compiled from .cc files
# 从.cc到.o
# 从host-cxxmulti中,找存在-cxxobjs的变量
host-cxxobjs := $(sort $(foreach m,$(host-cxxmulti),$($(m)-cxxobjs)))
# 以下是将其添加前缀
host-csingle := $(addprefix $(obj)/,$(host-csingle))
host-cmulti := $(addprefix $(obj)/,$(host-cmulti))
host-cobjs := $(addprefix $(obj)/,$(host-cobjs))
host-cxxmulti := $(addprefix $(obj)/,$(host-cxxmulti))
host-cxxobjs := $(addprefix $(obj)/,$(host-cxxobjs))

以fixdep为例分析,通过以上流程,fixdep会被加入到host-csingle变量中,由host-csingle到可执行文件的代码如下

1
2
3
4
5
6
quiet_cmd_host-csingle 	= HOSTCC  $@
cmd_host-csingle = $(HOSTCC) $(hostc_flags) \
$(KBUILD_HOSTLDFLAGS) -o $@ $< \
$(KBUILD_HOSTLDLIBS) $(HOSTLDLIBS_$(target-stem))
$(host-csingle): $(obj)/%: $(src)/%.c FORCE
$(call if_changed_dep,host-csingle)

除非指定了V=1,否则默认输出quiet_cmd_host-csingle,即显示在屏幕上的是HOSTCC …

还是以fixdep为例

  • 这里的$(host-csingle)会展开为fixdep
  • $(obj)/%: $(src)/%.c是模式匹配语法,将$(obj)下面的文件全部替换成对应的.c文件,这里的obj是执行Makefile.build时传入的,即scripts/basic,该目录下面只有三个文件,.gitignore是版本控制文件,不用管,Makefile是当前正在执行的Makefile,就还剩下fixdep.c文件,也就是说,这里就是指fixdep依赖fixdep.c文件
  • 注意后面的FORCE,意味着,不论这个.c是否比目标更新,永远都会执行下面的命令,因为FORCE是伪目标,伪目标永远是最新的

下面分析if_changed_dep

1
2
3
4
5
6
7
# Execute the command and also postprocess generated .d dependencies file.
if_changed_dep = $(if $(newer-prereqs)\
$(cmd-check),$(cmd_and_fixdep),@:)

cmd_and_fixdep = $(cmd);\
scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).cmd;\
rm -f $(depfile)

if_changed_dep首先会检查依赖是否比目标更新,然后查看本次执行和之前保存的.d文件中的命令是否一致,若这两个条件有一个满足,那么执行cmd_and_fixdep,否则执行@:,这里的@:只是占位符,表示啥也不做

cmd_and_fixdep首先会执行本次的命令,即$(cmd)

1
2
3
4
5
6
cmd = @set -e; $(echo-cmd) $(cmd_$(1))
#cmd_host-csingle = $(HOSTCC) $(hostc_flags) \
# $(KBUILD_HOSTLDFLAGS) -o $@ $< \
# $(KBUILD_HOSTLDLIBS) $(HOSTLDLIBS_$(target-stem))
#$(host-csingle): $(obj)/%: $(src)/%.c FORCE
# $(call if_changed_dep,host-csingle)

根据调用处的代码,$(1)就是host-csingle,那么$(cmd_$(1))就是cmd_host-csingle,可以看到,就是用HOSTCC,也就是gcc进行了编译,输出是$(host-csingle),即fixdep,输入是$<,也就是第一个依赖,本例中是fixdep.c,那么到此时,fixdep就被HOST编译完成了。

再来看剩下的代码

1
2
3
cmd_and_fixdep = $(cmd);\
scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).cmd;\
rm -f $(depfile)

使用编译好的fixdep,将$(depfile)$@$(make-cmd)生成一个.cmd文件,也就是该文件上次编译的记录,用来对比本次编译和上次编译中,编译命令改变的部分,最后把临时文件$(depfile)删掉,这也是为什么fixdep叫词法分析器的原因,fixdep的c源码就不在此进行分析了,depfile是c文件的头文件依赖。

好了,现在谁还记得我们调用Makefile.build的时候没有指定目标,因此执行的默认目标_build,这也是Kbuild分析的难点,容易陷入到各种细节中去,回到我们的_build,由于内核在顶层Makefile中调用scripts/basic下的Makefile时没有指定single-build,need-builtin和need-modorder,因此执行流直接来到下面的代码处

1
2
3
4
__build: $(if $(KBUILD_BUILTIN), $(targets-for-builtin)) \
$(if $(KBUILD_MODULES), $(targets-for-modules)) \
$(subdir-ym) $(always-y)
@:

可以看到,脚本命令处只是占位符@:,因此这里_build目标的作用就是让后面的依赖生成,这里面KBUILD_BUILTIN和KBUILD_MODULES都为空,subdir-ym也为空,但是always-y不是空,因为在Makefile.lib中有如下定义

1
always-y += $(hostprogs-always-y) $(hostprogs-always-m)

可以看到我们的fixdep被always-y包含进来了,因此这里要对always-y进行生成,$(alwasy-y)展开后就是fixdep,然后上面的fixdep已经被生成了,因此到这里,本次Makefile.build的流程结束,我们回到顶层Makefile

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

两个依赖都更新了,现在开始执行命令,还是build,但是现在目录是sripts/kconfig了,还传进去一个参数,$@,就是我们的目标,%config

还是之前的分析方法,到scripts/kconfig目录下找Makefile文件,这里可以注意一下,在Makefile.build中,会首先去读取该目录下的kbuild文件,如果没有才回去读取Makefile文件,在scripts/kconfig目录下是没有kbuild文件的,因此直接读取Makefile

还记得前面定义的KBUILD_KCONFIG变量么,该变量会在这个Makefile中进行判定

1
2
3
4
5
ifdef KBUILD_KCONFIG
Kconfig := $(KBUILD_KCONFIG)
else
Kconfig := Kconfig
endif

现在Kconfig变成了我们要输出的,也是内核构建最重要的.config了

以我们熟悉的i386_defconfig为例

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

这个defconfig需要首先构建$(obj)/conf,这个obj其实就是scripts/kconfig,也就是构建scripts/kconfig/conf,conf也是一个需要用HOSTCC进行编译的程序,因为在本Makefile中也有如下定义

1
2
3
common-objs	:= confdata.o expr.o lexer.lex.o parser.tab.o preprocess.o symbol.o util.o
hostprogs += conf
conf-objs := conf.o $(common-objs)

可以看到conf被hostprogs包含了,并且还有conf-objs,结合前面对Makefile.host的分析,可以得到,conf是被host-cmulti处理的,展开如下

1
2
3
host-cmulti := conf
host-cobjs := $(conf-objs)
# 其中$(conf-objs)如上所示

host-cmulti和host-cobjs最终会通过以下渠道被编译

1
2
3
4
5
6
7
8
9
10
11
quiet_cmd_host-cmulti	= HOSTLD  $@
cmd_host-cmulti = $(HOSTCC) $(KBUILD_HOSTLDFLAGS) -o $@ \
$(addprefix $(obj)/, $($(target-stem)-objs)) \
$(KBUILD_HOSTLDLIBS) $(HOSTLDLIBS_$(target-stem))
$(host-cmulti): FORCE
$(call if_changed,host-cmulti)

quiet_cmd_host-cobjs = HOSTCC $@
cmd_host-cobjs = $(HOSTCC) $(hostc_flags) -c -o $@ $<
$(host-cobjs): $(obj)/%.o: $(src)/%.c FORCE
$(call if_changed_dep,host-cobjs)

可知,host-cmulti是链接而成的,$(conf-objs)是一群.o文件,这些文件现在尚未生成,下面的$(host-cobjs)就是那群.o文件,通过模式匹配,host-cobjs的每一个.c文件都被编译为.o,最终conf被编译成功了。

接着回到scripts/kconfig目录下的Makefile

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

conf已经生成了,下面执行命令,命令展开之后就是

1
conf --defconfig=arch/x86/configs/i386_defconfig .config

conf程序源码就不仔细分析了,我们只需要知道,.config,auto.conf,autoconf.h都是在这里生成的,其中auto.conf给顶层Makefile使用,autoconf.h给Linux内核使用。

最后是一个小彩蛋,内核中经典的.config配置完成后的终端输出

1
2
3
#
# configuration written to .config
#

来自于confdata.c中的函数

1
conf_message("configuration written to %s", name);

其中name就是我们调用conf时传入的$(Kconfig),也就是.config

二、all的流程

1. __all

顶层Makefile的默认目标是

1
2
PHONY := __all
__all:

2. all

当不需要构建.config的时候,config-build变量不会被设置,因此走的是下面的分支

1
2
3
else #!config-build
# 把all加入到伪目标
PHONY += all

然后,本流程我们关注内核buildin的内容,不关注模块的内容,因此

1
2
3
4
5
6
7
8
9
# 
## 当编译in-tree的代码时,默认的目标是伪目标all,all是只有在编译非外部模块才会用到的目标(语义上)
ifeq ($(KBUILD_EXTMOD),)
__all: all
else
#
## 编译out-tree的代码时,默认的目标是伪目标modules,modules是在编译外部或非外部模块均有可能用到的目标
__all: modules
endif

我们看到,如果不是编译模块的话,默认的目标_all的依赖是all,也就是编译内核的最开始的目标

还记得我们上个流程编译的.config么?同时生成的还有auto.conf和autoconf.h,我们说过,auto.conf是用来给顶层Makefile使用的,下面他就来了

1
2
3
ifdef need-config
include include/config/auto.conf
endif

看到need-config了么?只有当需要.config的时候,才会定义这个变量,然后包含auto.conf

然后定义一些需要链接到vmlinux中的子目录

1
2
3
4
5
6
7
8
ifeq ($(KBUILD_EXTMOD),)
# Objects we will link into vmlinux / subdirs we need to visit
core-y := init/ usr/
drivers-y := drivers/ sound/
drivers-$(CONFIG_SAMPLES) += samples/
drivers-y += net/ virt/
libs-y := lib/
endif # KBUILD_EXTMOD

3.vmlinux

vmlinux是顶层目录要生成的内核最重要的目标

1
all: vmlinux

我们看到,all依赖vmlinux,vmlinux除了上面内核的子目录,还有架构中的内容,因此需要将架构中的Makefile也包含进来

1
include arch/$(SRCARCH)/Makefile

此时会有两种情况,nead-config表示此次构建需要auto.conf和autoconf.h,may-sync-config表示此次构建需要更新auto.conf和autoconf.h,也就是下面的代码

1
2
3
4
ifdef need-config
ifdef may-sync-config

include include/config/auto.conf.cmd

该代码表示,如果本次构建需要.config的内容,并且需要更新auto.conf和autoconf.h,既然要更新,那就要知道上一次构建的情况,这里的auto.conf.cmd就是上一次构建auto.conf的命令

如果.config直接不存在,直接报错,因为更新auto.conf是需要.config作为基础的

1
2
3
4
5
6
7
8
$(KCONFIG_CONFIG):
@echo >&2 '***'
@echo >&2 '*** Configuration file "$@" not found!'
@echo >&2 '***'
@echo >&2 '*** Please run some configurator (e.g. "make oldconfig" or'
@echo >&2 '*** "make menuconfig" or "make xconfig").'
@echo >&2 '***'
@/bin/false

更新auto.conf的命令如下

1
2
3
4
5
quiet_cmd_syncconfig = SYNC    $@
cmd_syncconfig = $(MAKE) -f $(srctree)/Makefile syncconfig
## 这里是 如auto.conf.cmd的生成命令,实际上是执行 syncconfig,这个会匹配到当前文件的 %config
%/config/auto.conf %/config/auto.conf.cmd %/generated/autoconf.h: $(KCONFIG_CONFIG)
+$(call cmd,syncconfig)

可以看到,这三个文件都是依赖$(KCONFIG_CONFIG),也就是.config的,如果.config不存在,那么就会打印echo的一堆错误信息

然后,执行分支到了这里

1
else # !may-sync-config

也就是,下面的代码,不需要更新auto.conf和autoconf.h,只是需要.config存在

首先将auto.conf加入到伪目标

1
2
3
4
5
6
7
8
9
10
PHONY += include/config/auto.conf

include/config/auto.conf:
$(Q)test -e include/generated/autoconf.h -a -e $@ || ( \
echo >&2; \
echo >&2 " ERROR: Kernel configuration is invalid."; \
echo >&2 " include/generated/autoconf.h or $@ are missing.";\
echo >&2 " Run 'make oldconfig && make prepare' on kernel src to fix it."; \
echo >&2 ; \
/bin/false)

看autoconf.h是否存在,不存在则报错

继续前进

顶层Makefile首先导出了两个默认的符号,一个是默认镜像名字,一个是默认安装路径

1
2
3
export KBUILD_IMAGE ?= vmlinux

export INSTALL_PATH ?= /boot

==分界线—–下面很重要==


1
PHONY += prepare0

注意这个prepare0

还是只看内建信息的构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 非模块的构建
ifeq ($(KBUILD_EXTMOD),)
core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/

vmlinux-dirs := $(patsubst %/,%,$(filter %/, \
$(core-y) $(core-m) $(drivers-y) $(drivers-m) \
$(libs-y) $(libs-m)))

vmlinux-alldirs := $(sort $(vmlinux-dirs) Documentation \
$(patsubst %/,%,$(filter %/, $(core-) \
$(drivers-) $(libs-))))

subdir-modorder := $(addsuffix modules.order,$(filter %/, \
$(core-y) $(core-m) $(libs-y) $(libs-m) \
$(drivers-y) $(drivers-m)))

build-dirs := $(vmlinux-dirs)
clean-dirs := $(vmlinux-alldirs)

首先执行流是builtin内容的构建

需要链接到vmlinux的子目录加到core-y中来,注意这里为何要放在这里,因为内核模块构建需要的内容与其不同

接着定义了三个经典变量vmlinux-dirs,vmlinux-alldirs,subdir-modorder,subdir-modorder暂时不管,那是构建模块的,vmlinux-dirs是所有需要包含在内核内建内容中的目录,vmlinux-alldirs是所有的目录,不论是否包含在builtin中,build-dirs就是构建目录,clean-dirs就是执行make clean时需要递归的目录

下面开始,是两个重要的变量,由link-vmlinux.sh使用

1
2
3
4
5
6
KBUILD_VMLINUX_OBJS := $(head-y) $(patsubst %/,%/built-in.a, $(core-y))
KBUILD_VMLINUX_OBJS += $(addsuffix built-in.a, $(filter %/, $(libs-y)))
KBUILD_VMLINUX_LIBS := $(patsubst %/,%/lib.a, $(libs-y))
KBUILD_VMLINUX_OBJS += $(patsubst %/,%/built-in.a, $(drivers-y))
# 将这两个变量导出,使得其他文件可以使用这两个变量
export KBUILD_VMLINUX_OBJS KBUILD_VMLINUX_LIBS

可以看到,这个KBUILD_VMLINUX_OBJS包含了head-y的内容,还有core-y,libs-y中的目录,添加built-in.a后缀,还有drivers-y的built-in.a,除了head-y中的内容,剩下的全是各个目录中的built-in.a文件,这是一个归档文件,由该目录下所有的目标文件使用AR归档而成。

然后指定了KBUILD的链接脚本

1
export KBUILD_LDS          := arch/$(SRCARCH)/kernel/vmlinux.lds

4.vmlinux-deps

还有vmlinux的所有依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
## 这个是编译vmlinux的依赖文件 这里都是.a,不同版本的问题
## vmlinux-deps: arch/arm64/kernel/vmlinux.lds arch/arm64/kernel/head.o init/built-in.a ... arch/arm64/lib/lib.a lib/lib.a
## vmlinux的依赖是链接脚本,各种built-in.a和各种lib.a

vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_OBJS) $(KBUILD_VMLINUX_LIBS)

# 根据vmlinux的编译规则可知,其一共包含三个依赖项:
# 1.scripts/link-vmlinux.sh: 这是编译命令中具体执行的脚本,其没什么规则,必须存在
# 2.autoksyms_recursive: 若要drop没有用到的内核和模块的导出符号,则此目标中会有命令
# 3.$(vmlinux-deps): 包括arch相关的vmlinux链接脚本(如arch/arm64/kernel/vmlinux.lds),各种built-in.a和各种lib.a文件
# 对于依赖来说,其中最主要的是,若想编译vmlinux,那其前提就是$(vmlinux-deps)中的所有目标(vmlinux.lds, */built-in.a, */lib.a)都要编译先编译出来(这些目录名的规则见下).
# 而其编译命令一共有两条:
# 1.执行scripts/link-vmlinux.sh编译vmlinux
# 2.若平台存在Makefile.postlink,则在构建vmlinux后执行make -f Makefile.postlink

接下来到vmlinux的构建

还记得我们前面说过all目标的依赖是vmlinux么?下面他来了

1
2
3
4
5
6
cmd_link-vmlinux =                                                 \
$(CONFIG_SHELL) $< "$(LD)" "$(KBUILD_LDFLAGS)" "$(LDFLAGS_vmlinux)"; \
$(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)

vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
+$(call if_changed,link-vmlinux)

vmlinux会依赖link-vmlinux.sh,vmlinux-deps,这里的autoksyms_recursive暂时先不管,也就是说,要想生成vmlinux,首先要生成link-vmlinux.sh和vmlinux-deps,其中link-vmlinux.sh就是scripts目录下的文件,就是现成的,vmlinux-deps我们前面说过,它是要组建vmlinux的built-in.a和lib.a,我们一个个看。

1
$(sort $(vmlinux-deps) $(subdir-modorder)): descend ;

暂时不用管后面的subdir-modorder,那是模块编译的内容,这里可知,首先会将vmlinux-deps中的变量排序去重,他们共同的依赖是descend

5.descend

1
descend: $(build-dirs)

build-dirs也是前面说过的,它是所有要编译进vmlinux的目录名字,没有后面的’/‘

6.prepare

1
2
3
4
$(build-dirs): prepare
$(Q)$(MAKE) $(build)=$@ \
single-build=$(if $(filter-out $@/, $(filter $@/%, $(KBUILD_SINGLE_TARGETS))),1) \
need-builtin=1 need-modorder=1

这后面的依赖,prepare,是进行具体编译的前置准备,非常重要

1
prepare: prepare0 prepare-objtool prepare-resolve_btfids

共有三个依赖,prepare0,prepare-objtools,prepare-resolve_btfids,prepare0前面已经出现过,并被声明成伪目标,下面是prepare0的依赖

7.prepare0

1
2
3
4
5
6
7
prepare0: archprepare
$(Q)$(MAKE) $(build)=scripts/mod
$(Q)$(MAKE) $(build)=.

archprepare: outputmakefile archheaders archscripts scripts include/config/kernel.release \
asm-generic $(version_h) $(autoksyms_h) include/generated/utsrelease.h \
include/generated/autoconf.h

东西很多,但是没办法,一个一个看吧

首先是outputmakefile

这个变量是源码目录和输出目录不一致时用到的,作用是在输出目录产生一个顶层Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
outputmakefile:
# 如果定义了在源码目录外进行构建
ifdef building_out_of_srctree
# 如果源码目录不干净(进行过编译),就显示需要进行make mrproper
$(Q)if [ -f $(srctree)/.config -o \
-d $(srctree)/include/config -o \
-d $(srctree)/arch/$(SRCARCH)/include/generated ]; then \
echo >&2 "***"; \
echo >&2 "*** The source tree is not clean, please run 'make$(if $(findstring command line, $(origin ARCH)), ARCH=$(ARCH)) mrproper'"; \
echo >&2 "*** in $(abs_srctree)";\
echo >&2 "***"; \
false; \
fi
# 设置源码目录的软链接source
$(Q)ln -fsn $(srctree) source
# 在输出目录下重新生成一个Makefile
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile $(srctree)
# 生成.gitignore
$(Q)test -e .gitignore || \
{ echo "# this is build directory, ignore it"; echo "*"; } > .gitignore
endif

目标archheaders是架构Makefile中的,以x86为例

1
2
3
4
5
###
# Syscall table generation

archheaders:
$(Q)$(MAKE) $(build)=arch/x86/entry/syscalls all

就是生成系统调用表

目标archscripts也是架构Makefile中的

1
2
archscripts: scripts_basic
$(Q)$(MAKE) $(build)=arch/x86/tools relocs

其以scripts_basic为基础,也就是需要fixdep功能,然后去执行arch/x86/tools目录下Makefile文件的relocs目标

目标scripts是构建scripts目录下的Makefile

1
2
scripts: scripts_basic scripts_dtc
$(Q)$(MAKE) $(build)=$(@)

而后面的asm-generic则是生成一些公共的头文件

1
2
3
4
5
6
7
8
9
asm-generic := -f $(srctree)/scripts/Makefile.asm-generic obj

asm-generic: uapi-asm-generic
$(Q)$(MAKE) $(asm-generic)=arch/$(SRCARCH)/include/generated/asm \
generic=include/asm-generic

uapi-asm-generic:
$(Q)$(MAKE) $(asm-generic)=arch/$(SRCARCH)/include/generated/uapi/asm \
generic=include/uapi/asm-generic

注意,这里的asm-generic是单独调用Makefile.asm-generic进行生成的,而不是传统的$(build),首先看uapi-asm-generic目标,在这个过程中,obj被设置为arch/x86/include/generated/uapi/asm,还有一个变量,generic被设置为include/upai/asm-generic,接下来我们到Makefile.asm-generic里面去瞅瞅


Makefile.asm-generic


这个Makefile中有一个默认的目标,all

如果没有指定目标,那么就会执行这个默认目标

我们前面说过,这个Makefile的obj目标被设为了arch/x86/include/generated/uapi/asm,

然后他就在下面被处理了

1
2
src := $(subst /generated,,$(obj))
-include $(src)/Kbuild

obj中的/generated被换成了空,然后赋给src,src是arch/x86/include/uapi/asm

然后看$(src)目录下是否有Kbuild,如果有,就include进来,没有也不报错,因为可能有的架构中是没有这一项的,好在在x86中有这个Kbuild,内容如下

1
2
3
generated-y += unistd_32.h
generated-y += unistd_64.h
generated-y += unistd_x32.h

这三个.h文件,是系统调用相关的内容,被加入到generated-y变量中

回到Makefile.asm-generic

1
2
3
ifneq ($(SRCARCH),um)
include $(generic)/Kbuild
endif

判断当前架构是不是um架构,也就是前面说过的user mode架构,若不是,将generic路径下的Kbuild包含进来,generic变量前面提到过,是include/upai/asm-generic,不是架构下的,而是include目录下的,该目录下的Kbuild

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
mandatory-y += auxvec.h
mandatory-y += bitsperlong.h
mandatory-y += bpf_perf_event.h
mandatory-y += byteorder.h
mandatory-y += errno.h
mandatory-y += fcntl.h
mandatory-y += ioctl.h
mandatory-y += ioctls.h
mandatory-y += ipcbuf.h
mandatory-y += mman.h
mandatory-y += msgbuf.h
mandatory-y += param.h
mandatory-y += poll.h
mandatory-y += posix_types.h
mandatory-y += ptrace.h
mandatory-y += resource.h
mandatory-y += sembuf.h
mandatory-y += setup.h
mandatory-y += shmbuf.h
mandatory-y += sigcontext.h
mandatory-y += siginfo.h
mandatory-y += signal.h
mandatory-y += socket.h
mandatory-y += sockios.h
mandatory-y += stat.h
mandatory-y += statfs.h
mandatory-y += swab.h
mandatory-y += termbits.h
mandatory-y += termios.h
mandatory-y += types.h
mandatory-y += unistd.h

包含了一堆需要强制包含的头文件

1
2
3
all: $(generic-y)
$(if $(unwanted),$(call cmd,remove))
@:

默认的all目标是要依赖generic-y目标

要理解这里,需要对make的语法有一定的了解,知道其变量展开的顺序

1
2
3
4
5
6
# redundant表示冗余的,即选出在generic-y中,但是不在mandatory-y和generated-y中的变量
redundant := $(filter $(mandatory-y) $(generated-y), $(generic-y))
redundant += $(foreach f, $(generic-y), $(if $(wildcard $(srctree)/$(src)/$(f)),$(f)))
redundant := $(sort $(redundant))
$(if $(redundant),\
$(warning redundant generic-y found in $(src)/Kbuild: $(redundant)))

由于本次是执行的uapi-asm-generic分支,generic-y在此时还是为空,因此redundant也为空

1
2
3
4
5
mandatory-y := $(filter-out $(generated-y), $(mandatory-y))
generic-y += $(foreach f, $(mandatory-y), $(if $(wildcard $(srctree)/$(src)/$(f)),,$(f)))

generic-y := $(addprefix $(obj)/, $(generic-y))
generated-y := $(addprefix $(obj)/, $(generated-y))

mandatory-y中取消掉已经在generated-y中的内容,现在开始定义generic-y,其内容为,在mandatory-y中的变量,如果其源文件已经存在,就不加入generic-y,如果不存在,就加入到generic-y中,(现在知道generic-y是用来做什么的了么?就是接下来要产生的文件的集合),然后为其加上路径前缀,而这里的generated-y则是源码中已经有了的文件

1
2
3
4
# obj目录下已经有了的.h文件
old-headers := $(wildcard $(obj)/*.h)
# 已经有了的.h文件,去除掉将要产生和已经产生的,即是要废弃的
unwanted := $(filter-out $(generic-y) $(generated-y),$(old-headers))

分析完变量,接着来看命令了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
quiet_cmd_wrap = WRAP    $@
cmd_wrap = echo "\#include <asm-generic/$*.h>" > $@

quiet_cmd_remove = REMOVE $(unwanted)
cmd_remove = rm -f $(unwanted)

all: $(generic-y)
$(if $(unwanted),$(call cmd,remove))
@:

$(obj)/%.h:
$(call cmd,wrap)

# 如果没有老目录,就产生对应目录,免得生成对应.h的时候目录不存在而报错
ifeq ($(old-headers),)
$(shell mkdir -p $(obj))
endif

默认目标all依赖$(generic-y),generic-y是强制目标mandatory-y中仍没有生成的部分,generic-y是源码目录下不存在的,那么就要在输出目录,也就是obj目录下生成,即目录(arch/x86/include/generated/uapi/asm)下的,这也是为啥generic-y要加obj前缀的原因,因为要在obj目录下生成,src目录是没有generated的(arch/x86/include/uapi/asm)

最后,具体的.h文件,通过wrap命令进行生成,其实就是在obj目录下生成了一个对应的.h文件,里面有一句#include <asm-generic/***.h>

看出来了吧,其实对应的.h都已经在include/asm-generic/下面放好了,只是针对对应架构,放入到一个可以被用户包含的头文件目录里面而已,也就是uapi目录喽

uapi-asm-generic目标已经更新,那么回到asm-generic目标

1
2
3
asm-generic: uapi-asm-generic
$(Q)$(MAKE) $(asm-generic)=arch/$(SRCARCH)/include/generated/asm \
generic=include/asm-generic

可以看到,还是Makefile.asm-generic,只不过我们传入的obj和generic变量都没有了uapi这个部分,也就是,这一次产生的,是一些内核用的头文件

还是先会将obj变量的generated去掉赋值给src,然后包含src对应目录下的Kbuild

1
2
3
4
5
6
7
8
9
generated-y += syscalls_32.h
generated-y += syscalls_64.h
generated-y += unistd_32_ia32.h
generated-y += unistd_64_x32.h
generated-y += xen-hypercalls.h

generic-y += early_ioremap.h
generic-y += export.h
generic-y += mcs_spinlock.h

是不是对比uapi目录下的Kbuild多了一些东西,起码我们有一个默认的generic-y了,而uapi是没有的,和之前的分析过程一样,就不多言了,这次我们在arch/x86/include/generated/asm目录下生成了很多.h文件,这些文件里面都是一句话,将include/asm-generic目录下的某个.h文件包含进来。


最后则是一些共有的文件,他们都有各自的规则进行生成

1
2
3
4
5
6
7
8
9
10
11
12
13
#include/config/kernel.release: FORCE
# $(call filechk,kernel.release)
include/config/kernel.release
#$(version_h): FORCE
# $(call filechk,version.h)
# $(Q)rm -f $(old_version_h)
$(version_h)
$(autoksyms_h)
#include/generated/utsrelease.h: include/config/kernel.release FORCE
# $(call filechk,utsrelease.h)
include/generated/utsrelease.h
# .config流程里面通过conf生成
include/generated/autoconf.h

那么到此为止,archprepare目标已经更新完成,prepare0只有这一个依赖,因此prepare0也更新完成,prepare-objtool prepare-resolve_btfids两个本次流程不用管,因此prepare目标已经更新完毕啦!!!

我们的旅途继续~


番外:Makefile.build Kbuild.include Makefile.lib

Makefile.build是Kbuild系统的核心机制,就在这里整体将它捋透吧

在Makefile.build中,首先会清空构建目标的各种定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
obj-y :=
obj-m :=
lib-y :=
lib-m :=
always :=
always-y :=
always-m :=
targets :=
subdir-y :=
subdir-m :=
EXTRA_AFLAGS :=
EXTRA_CFLAGS :=
EXTRA_CPPFLAGS :=
EXTRA_LDFLAGS :=
asflags-y :=
ccflags-y :=
cppflags-y :=
ldflags-y :=

subdir-asflags-y :=
subdir-ccflags-y :=

在Makefile.build中,如果obj目录有Kbuild,那么优先使用Kbuild

1
2
3
4
5
6
7
8
src := $(obj)

kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
# 对应目录中需要构建的对象在这里被包含进来,如obj-y等
include $(kbuild-file)
# 包含Makefile.lib,整理具体要编译的对象,以及编译的flag
include scripts/Makefile.lib

Makefile.build会包含Makefile.lib,这里面会根据need-modorder和need-builtin两个变量对可构建目标进行整理,得到要编译的目标,比如

1
2
3
4
5
ifdef need-builtin
obj-y := $(patsubst %/, %/built-in.a, $(obj-y))
else
obj-y := $(filter-out %/, $(obj-y))
endif

obj-y目标会被定义为对应目录下的built-in.a

然后根据是否为多重目标,进一步定义

1
2
3
4
5
6
7
8
9
# If $(foo-objs), $(foo-y), $(foo-m), or $(foo-) exists, foo.o is a composite object
multi-used-y := $(sort $(foreach m,$(obj-y), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y)) $($(m:.o=-))), $(m))))
multi-used-m := $(sort $(foreach m,$(obj-m), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y)) $($(m:.o=-m)) $($(m:.o=-))), $(m))))
multi-used := $(multi-used-y) $(multi-used-m)

# Replace multi-part objects by their individual parts,
# including built-in.a from subdirectories
real-obj-y := $(foreach m, $(obj-y), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y)) $($(m:.o=-))),$($(m:.o=-objs)) $($(m:.o=-y)),$(m)))
real-obj-m := $(foreach m, $(obj-m), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y)) $($(m:.o=-m)) $($(m:.o=-))),$($(m:.o=-objs)) $($(m:.o=-y)) $($(m:.o=-m)),$(m)))

这里的real-obj-y等变量则是真正要参与编译的

回到Makefile.build,在该Makefile中,会根据real-obj-y等变量进行处理

1
2
subdir-builtin := $(sort $(filter %/built-in.a, $(real-obj-y)))
subdir-modorder := $(sort $(filter %/modules.order, $(obj-m)))

得到了subdir-builtin等变量,这里的sort是为了去重,这样subdir-builtin中包含的是各个obj-y目录下的built-in.a

重点来了

1
targets-for-builtin := $(extra-y)

这个targets-for-builtin变量,表示要编译到内核中的内容,extra-y则是表示要额外处理的变量,即编译vmlinux需要,但是不合入vmlinux,举例而言就是x86下实模式的代码

接着,将需要编译的目标都加入到targets-for-builtin中

1
2
3
ifdef need-builtin
targets-for-builtin += $(obj)/built-in.a
endif

而这些目标最后都会包括在targets目标中

1
targets += $(targets-for-builtin) $(targets-for-modules)

这个targets是非常重要的变量,后面我们会介绍他的意义

接下来是具体文件对应的编译规则

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# vmlinux.lds.S --> vmlinux.lds
quiet_cmd_cpp_lds_S = LDS $@
cmd_cpp_lds_S = $(CPP) $(cpp_flags) -P -U$(ARCH) \
-D__ASSEMBLY__ -DLINKER_SCRIPT -o $@ $<

$(obj)/%.lds: $(src)/%.lds.S FORCE
$(call if_changed_dep,cpp_lds_S)

# 将目录下的.o归档为built-in.a
quiet_cmd_ar_builtin = AR $@
cmd_ar_builtin = rm -f $@; $(AR) cDPrST $@ $(real-prereqs)

$(obj)/built-in.a: $(real-obj-y) FORCE
$(call if_changed,ar_builtin)

剩下的规则就不赘述了

来看__build目标,这是Makefile.build的默认目标

1
2
3
4
__build: $(if $(KBUILD_BUILTIN), $(targets-for-builtin)) \
$(if $(KBUILD_MODULES), $(targets-for-modules)) \
$(subdir-ym) $(always-y)
@:

可以看到,__build目标的命令是空@:,单纯是为了更新他的依赖,KBUILD_BUILTIN是在顶层Makefile中定义的,如果是1的话,那么targets-for-builtin里面的目标都会被更新,然后subdir-ym和always-y默认会被编译,subdir-ym里面是一些子目录,需要进去递归执行的

1
2
3
4
5
6
7
8
9
# Descending
# ---------------------------------------------------------------------------

PHONY += $(subdir-ym)
$(subdir-ym):
$(Q)$(MAKE) $(build)=$@ \
$(if $(filter $@/, $(KBUILD_SINGLE_TARGETS)),single-build=) \
need-builtin=$(if $(filter $@/built-in.a, $(subdir-builtin)),1) \
need-modorder=$(if $(filter $@/modules.order, $(subdir-modorder)),1)

可以看到,还是调用Makefile.build,一样的逻辑,递归到所有目标编译完成

最后,还记得那个targets么?

他来了

1
2
3
existing-targets := $(wildcard $(sort $(targets)))

-include $(foreach f,$(existing-targets),$(dir $(f)).$(notdir $(f)).cmd)

所有的targets去重后,若有通配符就将其展开,得到existing-targets,然后把对应的.cmd当作Makefile文件包含进来,这也是if_changed系列函数的基础,若没有这个,if_changed系列函数将不起作用


8.$(build-dirs)

prepare更新完成之后,我们的准备工作已经完成,开始真正的内核编译啦~

1
2
3
4
$(build-dirs): prepare
$(Q)$(MAKE) $(build)=$@ \
single-build=$(if $(filter-out $@/, $(filter $@/%, $(KBUILD_SINGLE_TARGETS))),1) \
need-builtin=1 need-modorder=1

看其内容,针对其需要编译进内核的每一个目录,使用Makefile.build作为Makefile文件,obj变量为该需要编译的目录名

single-build由KBUILD_SINGLE_TARGETS决定,本次流程中为空,因此single-build=0

由于架构目录下的Makefile在顶层Makefile中被包含,因此架构Makefile中的core-y也被加入到build-dirs里面,比如

1
core-y += arch/x86/

让我们以这个目录为例子,捋一遍build-dirs的构建流程

首先,Makefile文件被指定为Makefile.build,obj变量为arch/x86,single-build=0,need-builtin=1,need-modorder=1,没有指定目标,因此使用默认目标__build

arch/x86目录下的Kbuild里面只有obj-y和obj-m

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
obj-y += entry/

obj-$(CONFIG_PERF_EVENTS) += events/

obj-$(CONFIG_KVM) += kvm/

# Xen paravirtualization support
obj-$(CONFIG_XEN) += xen/

obj-$(CONFIG_PVH) += platform/pvh/

# Hyper-V paravirtualization support
obj-$(subst m,y,$(CONFIG_HYPERV)) += hyperv/

obj-y += realmode/
obj-y += kernel/
obj-y += mm/

obj-y += crypto/

obj-$(CONFIG_IA32_EMULATION) += ia32/

obj-y += platform/
obj-y += net/

obj-$(CONFIG_KEXEC_FILE) += purgatory/

进入Makefile.build之后,该文件会包含Makefile.lib,在Makefile.lib中,会对obj-变量进行处理,针对其由单个还是多个目标文件构成,归类到real-obj-y,multi-used-y等变量中

real-obj-y是真正需要编译的目标集合,其中含有归档文件built-in.a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# real-obj-y中的built-in.a都过滤出来,然后去重
subdir-builtin := $(sort $(filter %/built-in.a, $(real-obj-y)))
subdir-modorder := $(sort $(filter %/modules.order, $(obj-m)))
# 将subdir-builtin中的/built-in.a去掉
$(subdir-builtin): $(obj)/%/built-in.a: $(obj)/% ;
# subdir-ym是需要递归的目录,没有后面的/
# 如果subdir-builtin中含有对应目录下的built-in.a,即$@/built-in.a
# 就将need-builtin置一
PHONY += $(subdir-ym)
$(subdir-ym):
$(Q)$(MAKE) $(build)=$@ \
$(if $(filter $@/, $(KBUILD_SINGLE_TARGETS)),single-build=) \
need-builtin=$(if $(filter $@/built-in.a, $(subdir-builtin)),1) \
need-modorder=$(if $(filter $@/modules.order, $(subdir-modorder)),1)

当这个__build目标更新过后,这个目录下的所有子目录都被编译完成了

1
2
3
4
__build: $(if $(KBUILD_BUILTIN), $(targets-for-builtin)) \
$(if $(KBUILD_MODULES), $(targets-for-modules)) \
$(subdir-ym) $(always-y)
@:

回到顶层Makefile

1
2
3
4
5
descend: $(build-dirs)
$(build-dirs): prepare
$(Q)$(MAKE) $(build)=$@ \
single-build=$(if $(filter-out $@/, $(filter $@/%, $(KBUILD_SINGLE_TARGETS))),1) \
need-builtin=1 need-modorder=1

这里build-dirs是所有需要编译的目录,执行完成后,descend目标被更新

1
$(sort $(vmlinux-deps) $(subdir-modorder)): descend ;

然后所有的vmlinux-deps目标也被更新了,因为这个规则没有命令

一层一层回归

1
2
3
4
5
6
cmd_link-vmlinux =                                                 \
$(CONFIG_SHELL) $< "$(LD)" "$(KBUILD_LDFLAGS)" "$(LDFLAGS_vmlinux)"; \
$(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)

vmlinux: scripts/link-vmlinux.sh autoksyms_recursive $(vmlinux-deps) FORCE
+$(call if_changed,link-vmlinux)

最终调用link-vmlinux.sh脚本将所有的归档文件都链接为vmlinux,这样就生成了一个原始的内核,这是一个elf文件,不能被直接加载

三、bzImage流程

我们都知道,在顶层Makefile里面,会包含架构中的Makefile,在包含架构Makefile之前,会有一个all:vmlinux

1
2
3
all: vmlinux
.......
include arch/$(SRCARCH)/Makefile

其中,架构的Makefile中,也有一个all目标

1
all: bzImage

根据Make的语法,all目标会先更新第一个出现的依赖,也就是顶层目录下的vmlinux,然后找到第二个依赖,也就是bzImage

下面我们来看bzImage的构建流程

首先

1
2
3
4
5
6
7
bzImage: vmlinux
ifeq ($(CONFIG_X86_DECODER_SELFTEST),y)
$(Q)$(MAKE) $(build)=arch/x86/tools posttest
endif
$(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE)
$(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot
$(Q)ln -fsn ../../x86/boot/bzImage \ $(objtree)/arch/$(UTS_MACHINE)/boot/$@

不用看ifeq宏包裹的部分,可知,bzImage依赖vmlinux,这个vmlinux没有前缀,说明它是顶层目录的vmlinux,也就是我们上一个流程生成的vmlinux

然后会以boot目录作为obj,Makefile.build为Makefile文件,进去执行,相关定义如下

1
2
3
boot := arch/x86/boot

KBUILD_IMAGE := $(boot)/bzImage

进入到boot目录下之后,我们之前说过,Makefile.build会优先找Kbuild文件,然后才是Makefile文件,但是boot目录下没有Kbuild,因此将boot目录下的Makefile包含进来

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

可以看到,bzImage依赖boot目录下的setup.bin,vmlinux.bin,和boot目录下子目录tools里面的build,这个build是个.c文件

来一个一个看吧,首先是setup.bin

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
# 还记得么?subdir-变量会在Makefile.lib中处理为要递归编译的目录
subdir- := compressed
# setup变量,x86架构下实模式的代码
setup-y += a20.o bioscall.o cmdline.o copy.o cpu.o cpuflags.o cpucheck.o
setup-y += early_serial_console.o edd.o header.o main.o memory.o
setup-y += pm.o pmjump.o printf.o regs.o string.o tty.o video.o
setup-y += video-mode.o version.o
setup-$(CONFIG_X86_APM_BOOT) += apm.o

# The link order of the video-*.o modules can matter. In particular,
# video-vga.o *must* be listed first, followed by video-vesa.o.
# Hardware-specific drivers should follow in the order they should be
# probed, and video-bios.o should typically be last.
setup-y += video-vga.o
setup-y += video-vesa.o
setup-y += video-bios.o

SETUP_OBJS = $(addprefix $(obj)/,$(setup-y))

# 使用setup.ld为链接脚本,链接实模式的代码
LDFLAGS_setup.elf := -m elf_i386 -T
$(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE
$(call if_changed,ld)

# 简洁明了,setup.bin就是setup.elf通过objcopy变为二进制文件了
OBJCOPYFLAGS_setup.bin := -O binary
$(obj)/setup.bin: $(obj)/setup.elf FORCE
$(call if_changed,objcopy)

接下来是vmlinux.bin

1
2
3
4
5
6
OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S
$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)

$(obj)/compressed/vmlinux: FORCE
$(Q)$(MAKE) $(build)=$(obj)/compressed $@

vmlinux.bin依赖boot/compressed目录下的vmlinux,而boot/compressed/vmlinux则需要使用Makefile.build到compressed目录下去执行boot/compressed/vmlinux目标,注意,指定目标了哦,不再是__build默认目标了

现在到compressed目录下,这个目录下也是只有Makefile,没有Kbuild文件

1
2
3
# 到了compressed目录下,obj变成$(boot)/compressed了哦
$(obj)/vmlinux: $(vmlinux-objs-y) $(efi-obj-y) FORCE
$(call if_changed,ld)

看到compressed/vmlinux依赖vmlinux-objs-y和efi-obj-y,并调用链接工具将其组合为compressed/vmlinux

1
2
3
4
vmlinux-objs-y := $(obj)/vmlinux.lds $(obj)/kernel_info.o $(obj)/head_$(BITS).o \
$(obj)/misc.o $(obj)/string.o $(obj)/cmdline.o $(obj)/error.o \
$(obj)/piggy.o $(obj)/cpuflags.o
# 还有其他一些vmlinux-objs-y变量,这里不一一列出了

注意一个小细节,vmlinux.lds是放在第一个的,原因是

1
2
3
4
5
LDFLAGS_vmlinux := -pie $(call ld-option, --no-dynamic-linker)
ifdef CONFIG_LD_ORPHAN_WARN
LDFLAGS_vmlinux += --orphan-handling=warn
endif
LDFLAGS_vmlinux += -T

看到最后那个-T了么,LDFLAGS_vmlinux是ld_flags的末尾,其后紧跟着就是要链接的变量,这样-T就能直接跟着vmlinux.lds了,巧妙地指定了压缩目录下的链接脚本

vmlinux-objs-y里面的目标大多有对应的源文件,直接编译就好,但是需要特别注意以下一个目标——piggy.o,他没有对应的源文件,但是有对应的规则

1
2
3
4
5
6
quiet_cmd_mkpiggy = MKPIGGY $@
cmd_mkpiggy = $(obj)/mkpiggy $< > $@

targets += piggy.S
$(obj)/piggy.S: $(obj)/vmlinux.bin.$(suffix-y) $(obj)/mkpiggy FORCE
$(call if_changed,mkpiggy)

这里依赖的mkpiggy是hostprogs变量,由Makefile.host提前编译好了

我们发现他的依赖是compressed目录下的vmlinux.bin.$(suffix-y),这个suffix-y是压缩方式,这里我们用gz格式压缩

1
2
$(obj)/vmlinux.bin.gz: $(vmlinux.bin.all-y) FORCE
$(call if_changed,gzip)

现在依赖是vmlinux.bin.all-y

1
vmlinux.bin.all-y := $(obj)/vmlinux.bin

注意,这个vmlinux.bin.all-y就是compressed目录下的vmlinux.bin

1
2
3
OBJCOPYFLAGS_vmlinux.bin :=  -R .comment -S
$(obj)/vmlinux.bin: vmlinux FORCE
$(call if_changed,objcopy)

哦豁,是不是回来了,原来compressed目录下的vmlinux.bin就是根目录下的vmlinux通过objcopy生成的

现在,所有的依赖都更新了,compressed目录下的vmlinux已经生成,现在我们要返回boot目录下的Makefile文件了

还记得我们要回到哪里么?

1
2
$(obj)/compressed/vmlinux: FORCE
$(Q)$(MAKE) $(build)=$(obj)/compressed $@

就是这里,我们现在的compressed/vmlinux已经生成,也就是说,它可以通过objcopy变为boot目录下的vmlinux.bin啦

1
2
3
OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S
$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)

于是,我们的bzImage流程也到了最后

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

这里的$(obj)/tools/build有对应的.c文件,这个build文件也是在hostprogs里面通过Makefile.host生成的

1
hostprogs	:= tools/build

现在,bzImage已经生成,并且在控制台显示出了

‘Kernel: $@ is ready’ ‘ (#’cat .version‘)’


Kbuild那些事儿
https://yill-z.github.io/2025/01/06/Kbuild/
作者
Yill Zhang
发布于
2025年1月6日
许可协议