一、zImage编译

在这里插入图片描述
_all 是默认目标,如果使用命令make或者make all编译 Linux 的话此目标就会被匹配。
KBUILD_EXTMOD 为空的,因此194 行的代码成立, 因此_all依赖all。all又依赖vmlinux,开始编译vmlinux。

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16

1.1 vmlinux编译

vmlinux 是未经压缩、原始 ELF 格式的内核可执行文件,是内核编译最核心的基础产物,所有镜像(zImage/Image/uImage/zImage-dtb)都由它加工而来。存放在源码根目录下 vmlinux。执行 make zImage 第一步就是先生成 vmlinux。
找到vmlinux目标开始分析:
在这里插入图片描述
目标 vmlinux 依赖 scripts/link-vmlinux.sh $(vmlinux-deps) FORCE。

vmlinux-deps= $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)
其中:

KBUILD_VMLINUX_INIT= $(head-y) $(init-y)
KBUILD_VMLINUX_MAIN = $(core-y) $(libs-y) $(drivers-y) $(net-y)
KBUILD_LDS= arch/$(SRCARCH)/kernel/vmlinux.lds //链接脚本

综上所述,vmlinux 的依赖为:

scripts/link-vmlinux.sh
$(head-y) 、$(init-y)、$(core-y) 、
$(libs-y) 、$(drivers-y) 、$(net-y)、arch/arm/kernel/vmlinux.lds 和 FORCE

1.1.1 head-y

head-y 定义在文件 arch/arm/Makefile 中:

head-y := arch/arm/kernel/head$(MMUEXT).o

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

head-y = arch/arm/kernel/head.o

1.1.2 init-y、drivers-y、net-y

顶层Makefile内容如下:
前:
在这里插入图片描述
后:
在这里插入图片描述
patsubst函数执行展开后:

init-y = init/built-in.o
drivers-y = drivers/built-in.o sound/built-in.o firmware/built-in.o
net-y = net/built-in.o

1.1.3 libs-y

顶层Makefile中:

libs-y := lib/

在arch/arm/Makefile中,对libs-y又追加了:

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

展开后:

libs-y = arch/arm/lib lib/

回到顶层Makefile:

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

展开后:

libs-y = arch/arm/lib/lib.a lib/lib.a arch/arm/lib/built-in.o lib/built-in.o

1.1.4 core-y

顶层Makefile中:

532 core-y := usr/
…
887 core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/
在 arch/arm/Makefile 中会对 core-y 进行追加,代码如下:

269 core-$(CONFIG_FPE_NWFPE) += arch/arm/nwfpe/
270 core-$(CONFIG_FPE_FASTFPE) +=$(FASTFPE_OBJ)
271 core-$(CONFIG_VFP) += arch/arm/vfp/
272 core-$(CONFIG_XEN) += arch/arm/xen/
273 core-$(CONFIG_KVM_ARM_HOST) += arch/arm/kvm/
274 core-$(CONFIG_VDSO) += arch/arm/vdso/
275
276 # If we have a machine-specific directory, then include it in the
build.
277 core-y += arch/arm/kernel/ arch/arm/mm/ arch/arm/common/
278 core-y += arch/arm/probes/
279 core-y += arch/arm/net/
280 core-y += arch/arm/crypto/
281 core-y += arch/arm/firmware/
282 core-y += $(machdirs) $(platdirs)

比如使能 VFP 的话就会在.config中有 CONFIG_VFP=y 这一行,那么 core-y 就会追加“arch/arm/vfp/”。
顶层 Makefile 中还有:

897 core-y := $(patsubst %/, %/built-in.o, $(core-y))
最终 core-y 的值为:

core-y = usr/built-in.o arch/arm/vfp/built-in.o
arch/arm/vdso/built-in.o arch/arm/kernel/built-in.o
arch/arm/mm/built-in.o arch/arm/common/built-in.o
arch/arm/probes/built-in.o arch/arm/net/built-in.o
arch/arm/crypto/built-in.o arch/arm/firmware/built-in.o
arch/arm/mach-imx/built-in.o kernel/built-in.o
mm/built-in.o fs/built-in.o
ipc/built-in.o security/built-in.o
crypto/built-in.o block/built-in.o

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

1.1.5 built-in.o产生过程

1.1.5.1vmlinux-deps展开

vmliux 依赖 vmlinux-deps:

vmlinux-deps= $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN)

这些变量进行展开:

$(head-y) 、$(init-y)、$(core-y) 、
$(libs-y) 、$(drivers-y) 、$(net-y)、arch/arm/kernel/vmlinux.lds

展开:

init-y      := init/
drivers-y   := drivers/ sound/
drivers-$(CONFIG_SAMPLES) += samples/
net-y       := net/
libs-y      := lib/
core-y      := usr/
virt-y      := virt/
// 每个变量的值原本是目录,将目录名后加上 built-in.a 后缀
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))
libs-y1     := $(patsubst %/, %/lib.a, $(libs-y))
libs-y2     := $(patsubst %/, %/built-in.a, $(filter-out %.a, $(libs-y)))
virt-y      := $(patsubst %/, %/built-in.a, $(virt-y))

// vmlinux 相关的目标文件
KBUILD_VMLINUX_OBJS := $(head-y) $(init-y) $(core-y) $(libs-y2) \
                  $(drivers-y) $(net-y) $(virt-y)
// vmlinux 的链接脚本
KBUILD_LDS          := arch/$(SRCARCH)/kernel/vmlinux.lds
// vmlinux 相关的库
KBUILD_VMLINUX_LIBS := $(libs-y1)

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

最后:

vmlinux-deps = arch/arm/kernel/vmlinux.lds
arch/arm/kernel/head.o
init/built-in.o usr/built-in.o
arch/arm/vfp/built-in.o arch/arm/vdso/built-in.o
arch/arm/kernel/built-in.o arch/arm/mm/built-in.o
arch/arm/common/built-in.o arch/arm/probes/built-in.o
arch/arm/net/built-in.o arch/arm/crypto/built-in.o
arch/arm/firmware/built-in.o arch/arm/mach-imx/built-in.o
kernel/built-in.o mm/built-in.o
fs/built-in.o ipc/built-in.o
security/built-in.o crypto/built-in.o
block/built-in.o arch/arm/lib/lib.a
lib/lib.a arch/arm/lib/built-in.o
lib/built-in.o drivers/built-in.o
sound/built-in.o firmware/built-in.o
net/built-in.o
除了 arch/arm/kernel/vmlinux.lds 以外,其他都是要编译链接生成的。顶层 Makefile 中有如下代码:

$(sort $(vmlinux-deps)): $(vmlinux-dirs) ;

sort 是排序函数,用于对 vmlinux-deps 的字符串列表进行排序,并且去掉重复的单词。可以看出 vmlinux-deps依赖 vmlinux-dirs,``vmlinux-dirs` 也定义在顶层 Makefile 中,定义如下(原本每一个目录名字后面都带着一条斜杠 /,先筛选只留带斜杠的文件夹,再把尾巴斜杠删掉,就得到了 vmlinux-dirs。):
在这里插入图片描述
函数作用:
在这里插入图片描述
因此递归展开的完整 vmlinux-dirs 为(子目录 Makefile 里声明的 xxx/ 下级目录,不会追加进顶层 vmlinux-dirs,而是被 Kbuild 递归遍历编译,所有层级目录编译生成的目标文件,最终汇总参与链接生成 vmlinux。):

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                             mm        fs \
		ipc                                security  crypto \
		block                              drivers   sound \
		firmware                           net       arch/arm/lib \
		lib

在顶层 Makefile 中有如下代码:
在这里插入图片描述

1.1.5.2prepare准备工作
vmlinux-deps -> vmlinux-dirs -> prepare

prepare 的框架:
在这里插入图片描述
prepare0:

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

第2行,等于make -f ./scripts/Makefile.build obj=scripts/mod
首先进入到scripts/Makefile.build ,然后包含scripts/mod/Makefile 文件,执行scripts/mod/Makefile下的默认目标:
scripts/mod/Makefile内容如下:

hostprogs-y := modpost mk_elfconfig
always      := $(hostprogs-y) empty.o

devicetable-offsets-file := devicetable-offsets.h

$(obj)/$(devicetable-offsets-file): $(obj)/devicetable-offsets.s FORCE
	$(call filechk,offsets,__DEVICETABLE_OFFSETS_H__)

这部分命令将生成 scripts/mod/devicetable-offsets.h这个全局偏移文件。
同时,modpost 和 mk_elfconfig这两个目标(这是两个主机程序)被赋值给always变量,根据 scripts/Makefile.build中的规则,将在Makefile.host中被生成。modpost 和 mk_elfconfig是两个主机程序,负责处理模块与编译符号相关的内容。

archprepare:
看到 arch 前缀就知道这是架构相关的,这部分的定义与 arch/$(ARCH)/Makefile 有非常大的关系.archprepare并没有命令部分,仅仅是四个依赖:

archprepare: archheaders archscripts prepare1 scripts

1.archheaders

archheaders:
 $(Q)$(MAKE) $(build)=arch/arm/tools uapi #定义在arch/$(ARCH)/Makfile

进入scripts/Makefile.build 中,并包含 arch/arm/tools/Makefile,编译目标为uapi:

uapi-hdrs-y := $(uapi)/unistd-common.h
uapi-hdrs-y += $(uapi)/unistd-oabi.h
uapi-hdrs-y += $(uapi)/unistd-eabi.h
uapi:   $(uapi-hdrs-y)

uapi 为用户 API 的头文件,包含unistd-common.h、unistd-oabi.h、unistd-eabi.h等通用头文件。

2.archscripts
archscripts 产生平台相关的支持脚本。

3.scripts

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

等于make -f ./scripts/Makefile.build obj=scripts
命令解析:
make 加载主控脚本 scripts/Makefile.build;脚本读取变量 obj=scripts,进入 scripts/ 目录;自动加载该目录下 Kbuild 或者 Makefile;找到scripts下的 Makefile,Makefile 下的内容是这样的:

hostprogs-$(CONFIG_BUILD_BIN2C)  += bin2c
hostprogs-$(CONFIG_KALLSYMS)     += kallsyms
hostprogs-$(CONFIG_LOGO)         += pnmtologo
hostprogs-$(CONFIG_VT)           += conmakehash
hostprogs-$(BUILD_C_RECORDMCOUNT) += recordmcount
hostprogs-$(CONFIG_BUILDTIME_EXTABLE_SORT) += sortextable
hostprogs-$(CONFIG_ASN1)     += asn1_compiler
...
hostprogs-y += unifdef
build_unifdef: $(obj)/unifdef
	@:

编译了一系列的主机程序,包括 bin2c、kallsyms、pnmtologo等。

scripts_basic在顶层Makefile学习中有笔记,编译出fixdep。

scripts_dtc定义如下:

scripts_dtc: scripts_basic
	$(Q)$(MAKE) $(build)=scripts/dtc

同样的,进入 scripts/dtc/Makefile:

hostprogs-$(CONFIG_DTC) := dtc
always      := $(hostprogs-y)
dtc-objs    := dtc.o flattree.o fstree.o data.o livetree.o treesource.o \
       srcpos.o checks.o util.o
dtc-objs    += dtc-lexer.lex.o dtc-parser.tab.o

可以看到,该目标的作用就是生成 dtc(Device Tree Compiler:把人类可读的文本格式设备树源文件 .dts / .dtsi,编译成 U-Boot、Linux 内核能识别加载的二进制设备树文件 .dtb)。

4.prepare1

prepare1: prepare3 outputMakefile asm-generic $(version_h) $(autoksyms_h) \
                    include/generated/utsrelease.h
$(cmd_crmodverdir)

cmd_crmodverdir

md_crmodverdir = $(Q)mkdir -p $(MODVERDIR) \
              $(if $(KBUILD_MODULES),; rm -f $(MODVERDIR)/*)                                                                                                                                                                              

$(MODVERDIR)仅与编译外部模块相关,这里针对外部模块的处理,如果指定编译外部模块,则不做任何事,如果没有指定编译外部模块,清除$(MODVERDIR)/目录下所有内容。

命令解析:
prepare1:后面是前置依赖项,必须全部编译生成完毕,才会执行下方缩进的Shell命令。Kbuild 识别前缀cmd_,去同名不带 cmd 的变量md_crmodverdir(create module version dir 创建模块版本目录)取值;取出里面 mkdir + 可选rm 两条 shell 命令,在终端运行,创建并清空模块版本目录。
前缀只是语义区分:

  • md_ = make dir,单纯自定义变量前缀,用来标注这是「建目录脚本」;
  • cmd_ = command,Kbuild 语法里 $(cmd_xxx) 代表执行变量 xxx 里的 shell 命令。

4.1 prepare3

prepare3: include/config/kernel.release
ifneq ($(srctree),.)
	@$(kecho) '  Using $(srctree) as source for kernel'
	$(Q)if [ -f $(srctree)/.config -o \
     	-d $(srctree)/include/config -o \
     	-d $(srctree)/arch/$(SRCARCH)/include/generated ]; then \
    	echo >&2 "  $(srctree) is not clean, please run 'make mrproper'"; \
    	echo >&2 "  in the '$(srctree)' directory.";\
    	/bin/false; \
	fi;
endif

当我们指定了O=DIR 参数时,发现如果源代码中在之前的编译中有残留的之前编译过的中间文件,编译过程就会报错,系统会提示我们使用make mrprope将之前的所有编译中间文件清除,再重新编译。prepare3的作用就是起到检查内核源码是否干净。

从源码中可以看出,prepare3将会检查的文件有:

$(srctree)/include/config、
$(srctree)/arch/$(SRCARCH)/include/generated、 $(srctree)/.config。

make help
Cleaning targets:
	clean           - Remove most generated files but keep the config and
                	enough build support to build external modules
	mrproper        - Remove all generated files + config + various backup files
	distclean       - mrproper + remove editor backup and patch files

输入make help可以看到clean目标的程度。

4.2 outputMakefile
KBUILD_SRC 不是在顶层 Makefile 里赋值,是make 命令行传参自动生成。由于KBUILD_SRC为空,不执行。否则会创建 source 软链接指向源码目录,执行mkmakefile生成输出目录的根Makefile。

4.3 asm-generic、version_h、autoksyms_h、 include/generated/utsrelease.h
产生相关头文件。

1.1.5.3vmlinux-dirs展开

在这里插入图片描述
冒号后为两个前置依赖,执行完成后才会去执行缩进的shell命令。
命令展开:

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

$@在makefile中表示target,因此就是为vmlinux-dirs 的值,将 vmlinux-dirs 中的这些目录全部带入到命令中:
@ make -f ./scripts/Makefile.build obj=init
@ make -f ./scripts/Makefile.build obj=usr
@ make -f ./scripts/Makefile.build obj=arch/arm/vfp
@ make -f ./scripts/Makefile.build obj=arch/arm/vdso
@ make -f ./scripts/Makefile.build obj=arch/arm/kernel
@ make -f ./scripts/Makefile.build obj=arch/arm/mm
@ make -f ./scripts/Makefile.build obj=arch/arm/common
@ make -f ./scripts/Makefile.build obj=arch/arm/probes
@ make -f ./scripts/Makefile.build obj=arch/arm/net
@ make -f ./scripts/Makefile.build obj=arch/arm/crypto
@ make -f ./scripts/Makefile.build obj=arch/arm/firmware
@ make -f ./scripts/Makefile.build obj=arch/arm/mach-imx
@ make -f ./scripts/Makefile.build obj=kernel
@ make -f ./scripts/Makefile.build obj=mm
@ make -f ./scripts/Makefile.build obj=fs
@ make -f ./scripts/Makefile.build obj=ipc
@ make -f ./scripts/Makefile.build obj=security
@ make -f ./scripts/Makefile.build obj=crypto
@ make -f ./scripts/Makefile.build obj=block
@ make -f ./scripts/Makefile.build obj=drivers
@ make -f ./scripts/Makefile.build obj=sound
@ make -f ./scripts/Makefile.build obj=firmware
@ make -f ./scripts/Makefile.build obj=net
@ make -f ./scripts/Makefile.build obj=arch/arm/lib
@ make -f ./scripts/Makefile.build obj=lib
这行规则就是内核 Kbuild 体系里批量启动所有顶层目录递归编译的入口,全权依靠Makefile.build实现跨目录、层级化的源码构建。

示例讲解 make -f ./scripts/Makefile.build obj=init

进入 Makefile.build 后的核心流程
步骤 1:读取 init 目录下的 Makefile/Kbuild
加载 init/Makefile,解析里面的 obj-y += main.o version.o mounts.o … 等编译条目,
把当前目录需要编译的目标文件、下级子目录全部提取出来。

步骤 2:判定 builtin-target

ifneq ($(strip $(obj-y) $(obj-m) $(obj-) $(subdir-m) $(lib-target)),)
builtin-target := $(obj)/built-in.o
endif

obj=init → $(obj) 就是 init,所以:
builtin-target = init/built-in.o
只要本目录有要编入内核镜像的内容(obj-y 非空),就会生成这个打包目标。

步骤 3:__build 总目标编排依赖
只编译 zImage(仅内核镜像、不编译模块)时:
KBUILD_BUILTIN=1、KBUILD_MODULES 无定义,__build 简化为:

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

编译顺序严格按依赖执行:
先编译所有单个 .o 文件(main.o、version.o 逐个 gcc 编译生成)
再执行 builtin-target 也就是链接打包 init/built-in.o

步骤 4:执行 link_o_target 链接脚本
builtin-target 目标绑定命令:

$(call if_changed,link_o_target)

if_changed 是内核封装工具:

  • 取出 cmd_link_o_target 预设命令
  • 对比上一次编译缓存 .built-in.o.cmd,文件没改动就跳过编译,改动则执行链接

cmd_link_o_target 拆分解释

cmd_link_o_target = $(if $(strip $(obj-y)),
# 有obj-y文件:用ld -r 可重定向链接,把一堆.o合并成一个built-in.o
$(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) $(cmd_secanalysis),
# 无obj-y:直接用ar归档生成空静态库
rm -f $@; $(AR) rcs$(KBUILD_ARFLAGS) $@)
  • LD -r:可重定位链接,不做最终地址固化,只把多个目标文件打包合并,方便后续顶层统一链接 vmlinux;
  • $@ 就是 init/built-in.o;
  • $^ 是所有前置依赖的 .o 文件列表。

步骤 5:生成编译记录文件
执行完毕后,在目录生成 .init/built-in.o.cmd,把本次完整链接命令存进去:

cmd_init/built-in.o := arm-linux-gnueabihf-ld -EL -r -o init/built-in.o init/main.o init/version.o init/mounts.o init/initramfs.o init/calibrate.o init/init_task.o

下次编译靠这个文件对比哈希,实现增量编译,避免重复打包。

1.1.6 link-vmlinux

vmlinux的依赖产生完后,调用+(call if_changed,link-vmlinux)链接生成 vmlinux。
命令“+$(call if_changed,link-vmlinux)”表示将$(call if_changed,link-vmlinux)的结果作为最终生成 vmlinux 的命令,前面的“+”表示该命令结果不可忽略。$(call if_changed,link-vmlinux)是调用函数 if_changed,link-vmlinux 是函数 if_changed 的参数,函数 if_changed 定义在文件 scripts/Kbuild.include 中:
在这里插入图片描述
总结:当依赖比目标新的时候,或者命令有改变的时候,if_changed 就会执行一些命令
“@set -e”告诉 bash,如果任何语句的执行结果不为 true(也就是执行出错)的话就直接退出。
$(echo-cmd)用于打印命令执行过程,比如在链接 vmlinux 的时候就会输出“LINK vmlinux”。$(cmd_$(1))中的$(1)表示参数,也就是 link-vmlinux,因此$(cmd_$(1))表示执行 cmd_link-vmlinux 的内容。

打开.vmlinux.cmd文件内容如下:

cmd_vmlinux := /bin/bash scripts/link-vmlinux.sh arm-linux-gnueabihf-ld -EL -p --no-undefined -X --pic-veneer --build-id

cmd_link-vmlinux 在顶层 Makefile 中定义如下:
914 # Final link of vmlinux
915 cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)
916 quiet_cmd_link-vmlinux = LINK $@

第 915 行就是 cmd_link-vmlinux 的值,其中 CONFIG_SHELL=/bin/bash,$<表示目标 vmlinux的第一个依赖文件,也就是 scripts/link-vmlinux.sh。
LD= arm-linux-gnueabihf-ld -EL,LDFLAGS 为空。LDFLAGS_vmlinux 的值由顶层 Makefile 和arch/arm/Makefile 这两个文件共同决定,最终:

LDFLAGS_vmlinux=-p --no-undefined -X --pic-veneer --build-id

因此 cmd_link-vmlinux 最终的值:

cmd_link-vmlinux = /bin/bash scripts/link-vmlinux.sh arm-linux-gnueabihf-ld -EL -p --no-undefined -X --pic-veneer --build-id

cmd_link-vmlinux 会调用 scripts/link-vmlinux.sh 这个脚本来链接出 vmlinux。脚本内容如下:
在这里插入图片描述

vmlinux_link()
{
    # 1. 定义局部变量
    local lds="${objtree}/${KBUILD_LDS}"
    local objects

    # 2. 第一层判断:架构是不是 um(用户态模拟内核)
    if [ "${SRCARCH}" != "um" ]; then
        # 我们ARM/x86嵌入式架构一定成立,进入此if
        # 3. 第二层判断:是否开启 CONFIG_THIN_ARCHIVES
        if [ -n "${CONFIG_THIN_ARCHIVES}" ]; then
            # 精简归档模式,给objects赋值极简内容
            objects="--whole-archive built-in.o ${1}"
        else
            # 【绝大多数内核默认走这里】给objects拼装完整链接文件列表
            objects="${KBUILD_VMLINUX_INIT}
                      --start-group
                      ${KBUILD_VMLINUX_MAIN}
                      --end-group
                      ${1}"
        fi

        # ======================
        # 4. 拼接完成objects变量后,执行这条LD链接命令
        # ======================
        ${LD} ${LDFLAGS} ${LDFLAGS_vmlinux} -o ${2} -T ${lds} ${objects}
    else
        # um架构专属分支,和普通嵌入式无关
        ...
    fi
    # 本函数为内核最终链接操作,属于标准ld链接流程:
# 1. 链接脚本路径 lds = ./arch/arm/kernel/vmlinux.lds,由全局变量 KBUILD_LDS 指定
# 2. 参与链接的目标文件由两大变量汇总而来:
# ① KBUILD_VMLINUX_INIT = $(head-y) $(init-y)
#    存放架构启动入口汇编head-y、init初始化目录打包文件,放在内核镜像最前端
# ② KBUILD_VMLINUX_MAIN = $(core-y) $(libs-y) $(drivers-y) $(net-y)
#    汇总内核核心、工具库、所有驱动、网络子系统编译生成的全部built-in.o
# 3. --start-group / --end-group 用于解决多个子系统之间符号循环依赖,避免链接报未定义符号
}

${LD} ${LDFLAGS} ${LDFLAGS_vmlinux} -o ${2} -T ${lds} ${objects} 参数拆解
在这里插入图片描述
在 scripts/link-vmlinux.sh 末尾调用 vmlinux_link 函数来链接出 vmlinux。
在这里插入图片描述
第一个参数( ${kallsymso}):符号表附加目标文件;第二个参数:最终输出文件 vmlinux

总结:将各个子目录下的 built-in.o、.a 等文件链接在一起,最终生成 vmlinux 这个 ELF 格式的可执行文件。链接脚本为arch/arm/kernel/vmlinux.lds(提供链接规则与内存布局约束,是配置文件),链接过程是由shell脚本scripts/link-vmlinux.s来完成的。

1.1.7 新版的link-vmlinux

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)

1.1.7.1 autoksyms_recursive
内核的符号优化。

autoksyms_recursive: $(vmlinux-deps)
ifdef CONFIG_TRIM_UNUSED_KSYMS
	$(Q)$(CONFIG_SHELL) $(srctree)/scripts/adjust_autoksyms.sh \
  		"$(MAKE) -f $(srctree)/Makefile vmlinux"
endif

如果在源码的配置阶段定义了CONFIG_TRIM_UNUSED_KSYMS这个变量,就执行:

$(Q)$(CONFIG_SHELL) $(srctree)/scripts/adjust_autoksyms.sh \
  "$(MAKE) -f $(srctree)/Makefile vmlinux"

默认情况下,这里的脚本并不会执行。
if_changed,执行link-vmlinux,也就是cmd_link-vmlinux.

1.2 make zImage过程

1.2.1 vmlinux、Image,zImage、uImage 的区别

①、vmlinux 是编译出来的最原始的内核文件,是未压缩的,比如正点原子提供的 Linux 源码编译出来的 vmlinux 差不多有 16MB。

$ ls -lh vmlinux
-rwxrwxr-x 1 robin.lee robin.lee 17M 1月 29 16:26 vmlinux
②、Image 是 Linux 内核镜像文件,但是 Image 仅包含可执行的二进制数据。Image 就是使用 objcopy 工具取消掉 vmlinux 中的一些其他信息,比如符号表什么的。但是 Image 是没有压缩过的,Image 保存在 arch/arm/boot 目录下,其大小大概在 12MB 左右。

$ ls -lh arch/arm/boot/Image
-rwxrwxr-x 1 robin.lee robin.lee 13M 1月 29 16:26 arch/arm/boot/Image

③、zImage 是经过 gzip 压缩后的 Image,经过压缩以后其大小大概在 6MB 左右。

$ ls -lh arch/arm/boot/zImage
-rwxrwxr-x 1 robin.lee robin.lee 6.5M 1月 29 16:26 arch/arm/boot/zImage
④、uImage 是老版本 uboot 专用的镜像文件,uImag 是在 zImage 前面加了一个长度为 64字节的“头”,这个头信息描述了该镜像文件的类型、加载位置、生成时间、大小等信息。但是新的 uboot 已经支持了 zImage 启动!所以已经很少用到 uImage 了,除非你用的很古老的 uboot。

1.2.2 Image的编译过程

使用“make”、“make all”、“make zImage”都可以编译出zImage, arch/arm/Makefile 中有如下代码:
在这里插入图片描述
all依赖$(KBUILD_IMAGE) $(KBUILD_DTBS), KBUILD_IMAGE为zImage, KBUILD_DTBS为dtbs。

#boot 对应目录
boot := arch/arm/boot
#实际镜像
BOOT_TARGETS    = zImage Image xipImage bootpImage uImage
#镜像生成规则
$(BOOT_TARGETS): vmlinux
    $(Q)$(MAKE) $(build)=$(boot) MACHINE=$(MACHINE) $(boot)/$@

可以看到当vmlinux产生后,就会去继续产生zImage, 展开:
@ make -f ./scripts/Makefile.build obj=arch/arm/boot MACHINE=arch/arm/boot/zImage 可以看到又是通过scripts/Makefile.build来完成的编译。

执行命令行:

rm Image zImage
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- Image V=1

Image生成过程:
进入arch/arm/boot的makefile:
在这里插入图片描述
执行cmd_objcopy,该命令定义在scripts/Makefile.lib中:

#在arch/$(ARCH)/boot/Makefile 下定义
OBJCOPYFLAGS    :=-O binary -R .comment -S
#scripts/Makefile.lib
cmd_objcopy = $(OBJCOPY) $(OBJCOPYFLAGS) $(OBJCOPYFLAGS_$(@F)) $< $@

打印如下:

make -f ./scripts/Makefile.build obj=arch/arm/boot MACHINE= arch/arm/boot/Image
	arm-linux-gnueabihf-objcopy -O binary -R .comment -S  vmlinux arch/arm/boot/Image

1.2.3 zImage的编译过程

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage V=1

zImage生成过程(arch\arm\boot\Makefile):
在这里插入图片描述
打印如下:

make -f ./scripts/Makefile.build obj=arch/arm/boot MACHINE= arch/arm/boot/zImage
make -f ./scripts/Makefile.build obj=arch/arm/boot/compressed arch/arm/boot/compressed/vmlinux
  (cat arch/arm/boot/compressed/../Image | lzop -9 && printf \000\100\303\000) > arch/arm/boot/compressed/piggy.lzo || (rm -f arch/arm/boot/compressed/piggy.lzo ; false)
  arm-linux-gnueabihf-gcc -Wp,-MD,arch/arm/boot/compressed/.piggy.lzo.o.d  -nostdinc -isystem /media/xxxxxx/robin.lee/my_test/study/openedv/toolchain/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/../lib/gcc/arm-linux-gnueabihf/4.9.4/include -I./arch/arm/include -Iarch/arm/include/generated/uapi -Iarch/arm/include/generated  -Iinclude -I./arch/arm/include/uapi -Iarch/arm/include/generated/uapi -I./include/uapi -Iinclude/generated/uapi -include ./include/linux/kconfig.h -D__KERNEL__ -mlittle-endian   -D__ASSEMBLY__ -mabi=aapcs-linux -mno-thumb-interwork -mfpu=vfp -funwind-tables -marm -D__LINUX_ARM_ARCH__=7 -march=armv7-a  -include asm/unified.h -msoft-float -DCC_HAVE_ASM_GOTO        -DZIMAGE     -c -o arch/arm/boot/compressed/piggy.lzo.o arch/arm/boot/compressed/piggy.lzo.S
  arm-linux-gnueabihf-ld -EL    --defsym _kernel_bss_size=463520 -p --no-undefined -X -T arch/arm/boot/compressed/vmlinux.lds arch/arm/boot/compressed/head.o arch/arm/boot/compressed/piggy.lzo.o arch/arm/boot/compressed/misc.o arch/arm/boot/compressed/decompress.o arch/arm/boot/compressed/string.o arch/arm/boot/compressed/hyp-stub.o arch/arm/boot/compressed/lib1funcs.o arch/arm/boot/compressed/ashldi3.o arch/arm/boot/compressed/bswapsdi2.o -o arch/arm/boot/compressed/vmlinux
  arm-linux-gnueabihf-objcopy -O binary -R .comment -S  arch/arm/boot/compressed/vmlinux arch/arm/boot/zImage

1.2.4 OBJCOPYFLAGS

OBJCOPYFLAGS 就是 objcopy 工具的参数集合,专门用来把 vmlinux 加工成裸二进制 Image。
在arch\arm\boot\Makefile中:
在这里插入图片描述

-O binary:表示输出格式为 binary,也就是二进制
-R .comment:表示移除二进制文件中的 .comment 段,这个段主要用于 debug
-S : 表示移除所有的符号以及重定位信息

2.生成的文件

  • System.map: 内核镜像中所有的符号,符号表
  • ..o.d 和 ..o.cmd:记录编译指令的内容,如:.vmlinux.cmd
  • modules.order 、 modules.build 和 modules.builtin.modinfo : 这两个文件主要负责记录编译的模块,modules.builtin.modinfo记录模块信息,以供 modprobe 使用
  • arch/$(ARCH)/boot : 编译出的文件,一般包含这几部分:镜像、内核符号表、系统dtb。
  • include/generated/* : 内核编译过程将会生成一些头文件,其中比较重要的是 autoconf.h,这是 .config 的头文件版本,以及uapi/目录下的文件,这个目录下的保存着用户头文件。
  • include/config/* : 为了解决 autoconf.h 牵一发而动全身的问题(即修改一个配置导致所有依赖 autoconf.h 的文件需要重新编译),将 autoconf.h 分散为多个头文件放在 include/config/ 下,以解决 autoconf.h 的依赖问题。

3.设备树的编译过程

Linux内核目录下make all会把dtb也编译出来。make dtbs V=1可查看详细编译过程。

3.1 生成dtc工具

在这里插入图片描述

3.2 生成dtb

在这里插入图片描述
vmlinux和zImage产生后会继续编译设备树,用dtc编译dts得到dtb。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐