Kbuild机制梳理 Makefile的执行顺序
读入所有include的makefile(include时会对include命令后面的变量与通配符进行扩展,然后试着读入该Makefile,如果成功就继续,如果失败就报告,并继续读取其余makefile。直到所有读取全部完成后,查看规则中是否有更新该Makefile的规则,如果有,就更新目标,然后重新读入该Makefile。不断重复以上流程,直到所有更新Makefile的规则都被执行后,仍不存在该Makefile,就报错退出)
初始化变量
分析规则,将其加入依赖链
根据依赖,决定哪些目标需要生成
执行生成命令
命令脚本初始化顺序
读取命令脚本
扩展变量(执行的时候才会扩展,然而目标的变量扩展会在构建规则链时)
对Make表达式求值(宏被扩展时,会为每一行增加Tab)
执行
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; \ 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 := $(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) UTS_MACHINE := $(ARCH) SRCARCH := $(ARCH)
HOST:本地编译的一些工具,如HOSTCC,HOSTLD,HOSTCXX等,用来编译本机上的一些工具,如mkproggy,fixdep等
下面开始正式构建.config的流程,首先看.config的定义位置
1 2 KCONFIG_CONFIG ?= .configexport 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-buildinclude arch/$(SRCARCH) /Makefileexport KBUILD_DEFCONFIG KBUILD_KCONFIG CC_VERSION_TEXTconfig: outputmakefile scripts_basic FORCE $(Q) $(MAKE) $(build) =scripts/kconfig $@ %config: outputmakefile scripts_basic FORCE $(Q) $(MAKE) $(build) =scripts/kconfig $@ else
可以看到导出了三个变量,分别是
1 2 3 4 KBUILD_DEFCONFIG KBUILD_KCONFIG CC_VERSION_TEXT
make的特性,在include某个makefile时,会同步将其展开,因此这里会直接将arch/$(SRCARCH)/Makefile进行展开,并计算其中的变量和make操作。
以x86架构为例
首先根据真实架构确定使用的defconfig是哪个,即KBUILD_DEFCONFIG
1 2 3 4 5 6 7 8 9 10 ifeq ($(ARCH) ,x86) ifeq ($(shell uname -m) ,x86_64) KBUILD_DEFCONFIG := x86_64_defconfig else KBUILD_DEFCONFIG := i386_defconfig endif else KBUILD_DEFCONFIG := $(ARCH) _defconfigendif
接下来会定义一些架构相关的东西,和一些特定的功能,如FUNCTION_GRAPH_TRACER之类的,比较重要的是下面这些
1 2 3 4 5 6 7 8 9 archscripts: scripts_basic $(Q) $(MAKE) $(build) =arch/x86/tools relocsarchheaders: $(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 ifeq ($(CONFIG_X86_DECODER_SELFTEST) ,y) $(Q) $(MAKE) $(build) =arch/x86/tools posttestendif $(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: 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 $(Q) ln -fsn $(srctree) source $(Q) $(CONFIG_SHELL) $(srctree) /scripts/mkmakefile $(srctree) $(Q) test -e .gitignore || \ { echo "# this is build directory, ignore it" ; echo "*" ; } > .gitignoreendif
可以看到,只有源码目录和输出目录不一致时,才会在输出目录下生成makefile,否则outputmakefile只是一个空操作。
1 2 3 4 5 PHONY += scripts_basicscripts_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 include scripts/Makefile.lib hostprogs := $(sort $(hostprogs) ) ifneq ($(hostprogs) ,)include scripts/Makefile.hostendif
上面提到过,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 host-csingle := $(foreach m,$(hostprogs) , \ $(if $($(m) -objs) $($(m) -cxxobjs),,$(m) )) host-cmulti := $(foreach m,$(hostprogs) ,\ $(if $($(m) -cxxobjs) ,,$(if $($(m) -objs) ,$(m) ))) host-cobjs := $(sort $(foreach m,$(hostprogs) ,$($(m) -objs) )) host-cxxmulti := $(foreach m,$(hostprogs) ,$(if $($(m) -cxxobjs) ,$(m) )) 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 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))
根据调用处的代码,$(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 := Kconfigendif
现在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)
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配置完成后的终端输出
来自于confdata.c中的函数
1 conf_message("configuration written to %s" , name);
其中name就是我们调用conf时传入的$(Kconfig),也就是.config
二、all的流程 1. __all 顶层Makefile的默认目标是
2. all 当不需要构建.config的时候,config-build变量不会被设置,因此走的是下面的分支
然后,本流程我们关注内核buildin的内容,不关注模块的内容,因此
1 2 3 4 5 6 7 8 9 ifeq ($(KBUILD_EXTMOD) ,)__all: all else __all: modules endif
我们看到,如果不是编译模块的话,默认的目标_all的依赖是all,也就是编译内核的最开始的目标
还记得我们上个流程编译的.config么?同时生成的还有auto.conf和autoconf.h,我们说过,auto.conf是用来给顶层Makefile使用的,下面他就来了
1 2 3 ifdef need-configinclude include /config/auto.confendif
看到need-config了么?只有当需要.config的时候,才会定义这个变量,然后包含auto.conf
然后定义一些需要链接到vmlinux中的子目录
1 2 3 4 5 6 7 8 ifeq ($(KBUILD_EXTMOD) ,) core-y := init/ usr/ drivers-y := drivers/ sound/ drivers-$(CONFIG_SAMPLES) += samples/ drivers-y += net/ virt/ libs-y := lib/endif
3.vmlinux 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-configifdef may-sync-configinclude 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 %/config/auto.conf %/config/auto.conf.cmd %/generated/autoconf.h: $(KCONFIG_CONFIG) +$(call cmd,syncconfig)
可以看到,这三个文件都是依赖$(KCONFIG_CONFIG),也就是.config的,如果.config不存在,那么就会打印echo的一堆错误信息
然后,执行分支到了这里
也就是,下面的代码,不需要更新auto.conf和autoconf.h,只是需要.config存在
首先将auto.conf加入到伪目标
1 2 3 4 5 6 7 8 9 10 PHONY += include /config/auto.confinclude/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 ?= vmlinuxexport INSTALL_PATH ?= /boot
==分界线—–下面很重要==
注意这个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-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_OBJS) $(KBUILD_VMLINUX_LIBS)
接下来到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
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 $(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 $(Q) ln -fsn $(srctree) source $(Q) $(CONFIG_SHELL) $(srctree) /scripts/mkmakefile $(srctree) $(Q) test -e .gitignore || \ { echo "# this is build directory, ignore it" ; echo "*" ; } > .gitignoreendif
目标archheaders
是架构Makefile中的,以x86为例
1 2 3 4 5 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 objasm-generic: uapi-asm-generic $(Q) $(MAKE) $(asm-generic)=arch/$(SRCARCH) /include /generated/asm \ generic=include /asm-genericuapi-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) /Kbuildendif
判断当前架构是不是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 := $(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 old-headers := $(wildcard $(obj) /*.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) 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$(version_h) $(autoksyms_h) include /generated/utsrelease.hinclude /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)include $(kbuild-file)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 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) 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.aendif
而这些目标最后都会包括在targets目标中
1 targets += $(targets-for-builtin) $(targets-for-modules)
这个targets是非常重要的变量,后面我们会介绍他的意义
接下来是具体文件对应的编译规则
比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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) 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 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里面,比如
让我们以这个目录为例子,捋一遍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/ obj-$(CONFIG_XEN) += xen/ obj-$(CONFIG_PVH) += platform/pvh/ 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 subdir-builtin := $(sort $(filter %/built-in.a, $(real-obj-y) )) subdir-modorder := $(sort $(filter %/modules.order, $(obj-m) ))$(subdir-builtin): $(obj) /%/built-in.a: $(obj) /% ; 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目标
根据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 posttestendif $(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' ' (
可以看到,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- := compressed 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 setup-y += video-vga.o setup-y += video-vesa.o setup-y += video-bios.o SETUP_OBJS = $(addprefix $(obj) /,$(setup-y) ) LDFLAGS_setup.elf := -m elf_i386 -T$(obj) /setup.elf: $(src) /setup.ld $(SETUP_OBJS) FORCE $(call if_changed,ld) 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 $(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.lds是放在第一个的,原因是
1 2 3 4 5 LDFLAGS_vmlinux := -pie $(call ld-option, --no-dynamic-linker) ifdef CONFIG_LD_ORPHAN_WARN LDFLAGS_vmlinux += --orphan-handling=warnendif 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' ' (
这里的$(obj)/tools/build有对应的.c文件,这个build文件也是在hostprogs里面通过Makefile.host生成的
1 hostprogs := tools/build
现在,bzImage已经生成,并且在控制台显示出了
‘Kernel: $@ is ready’ ‘ (#’cat .version
‘)’