Linux内核剖析(五)Linux内核的构建过程 - AlanTu

时间:2024-03-01 11:08:43

Linux内核剖析(五)Linux内核的构建过程

参考
一次实验引发的故事 – kernel build system探索—vmlinux是如何炼成的– kernel makefile

深度探索Linux操作系统:系统构建和原理解析.pdf

问题


在前面的博文中,我们先是为自己的Ubuntu安装了一套内核源码树,然后为了方便进行嵌入式交叉编译,我们又为arm板子构建了一套源码树。
那么现在我们已经知道如何自己的电脑上去构建、安装一个定制化的Linux内核,但是我们还是要在唠叨一些。
当你在内核源码路径里敲下make时究竟发生什么
这里写图片描述

当我们刚刚开始接触内核代码时,毫无头绪,这时候Makefile是往往是我们打开的第一个文件,这个makefile是Linux内核代码的根makefile,内核构建就始于此处。是的,它的内容很多,但是如果你已经读过内核源代码,你就会发现每个包含代码的目录都有一个自己的Makefile。当然了,我们不会去描述每个代码文件是怎么编译链接的,所以我们将只会挑选一些通用的例子来说明问题。而你不会在这里找到构建内核的文档、如何整洁内核代码、tags的生成和交叉编译相关的说明,等等。

我们仅仅将从make开始,使用标准的内核配置文件,一直到生成了内核镜像bzImage或者zImage结束。
当然在着之前我们需要了解,我们make究竟是要构建一个什么样的目标,我想这个

构建的目标vmlinux,vmlinuz,bzImage,zImage


对于Linux内核,编译可以生成不同格式的映像文件,例如:

make zImag
make uImage

 

zImage是ARM Linux常用的一种压缩映像文件,uImage是U-boot专用的映像文件,它是在zImage之前加上一个长度为0x40的“头”,说明这个映像文件的类型、加载位置、生成时间、大小等信息。换句话说,如果直接从uImage的0x40位置开始执行,zImage和uImage没有任何区别。另外,Linux2.4内核不支持uImage,Linux2.6内核加入了很多对嵌入式系统的支持,但是uImage的生成也需要设置。

几种linux内核文件的区别:

1、vmlinux 编译出来的最原始的内核文件,未压缩。
2、zImage 是vmlinux经过gzip压缩后的文件。适用于小内核
3、bzImage bz表示“big zImage”,不是用bzip2压缩的。两者的不同之处在于,zImage解压缩内核到低端内存(第一个640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么采用zImage或bzImage都行,如果比较大应该用bzImage。适用于大内核
4、uImage U-boot专用的映像文件,它是在zImage之前加上一个长度为0x40的tag。
5、vmlinuz 是bzImage/zImage文件的拷贝或指向bzImage/zImage的链接。
6、initrd 是“initial ramdisk”的简写。一般被用来临时的引导硬件到实际内核vmlinuz能够接管并继续引导的状态。

vmlinux


vmlinux是未压缩的内核,是make工作编译出的原始内核,vmlinuz是vmlinux的压缩文件。

vmlinux 是ELF文件,即编译出来的最原始的文件。

zImage, bzImage和vmlinuz


vmlinuz是可引导的、压缩的内核。“vm”代表“Virtual Memory”。Linux 支持虚拟内存,不像老的操作系统比如DOS有640KB内存的限制。Linux能够使用硬盘空间作为虚拟内存,因此得名“vm”。vmlinuz是可执行的Linux内核,它位于/boot/vmlinuz,它一般是一个软链接,是bzImage/zImage文件的拷贝或指向bzImage/zImage的链接。

vmlinuz的建立有两种方式。

一是编译内核时通过“make zImage”创建,然后通过:“cp /usr/src/linux-2.4/arch/i386/linux/boot/zImage /boot/vmlinuz”产生。zImage适用于小内核的情况,它的存在是为了向后的兼容性。

二是内核编译时通过命令make bzImage创建,然后通过:“cp /usr/src/linux-2.4/arch/i386/linux/boot/bzImage /boot/vmlinuz”产生。bzImage是压缩的内核映像,需要注意,bzImage不是用bzip2压缩的,bzImage中的bz容易引起误解,bz表示“big zImage”。 bzImage中的b是“big”意思。

zImage(vmlinuz)和bzImage(vmlinuz)都是用gzip压缩的。它们不仅是一个压缩文件,而且在这两个文件的开头部分内嵌有gzip解压缩代码。所以你不能用gunzip 或 gzip –dc解包vmlinuz。

内核文件中包含一个微型的gzip用于解压缩内核并引导它。两者的不同之处在于,老的zImage解压缩内核到低端内存(第一个640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么可以采用zImage 或bzImage之一,两种方式引导的系统运行时是相同的。大的内核采用bzImage,不能采用zImage。

但是注意通常情况下是不能用vmlinuz解压缩得到vmlinux的

initrd-x.x.x.img


initrd是“initial ramdisk”的简写。initrd一般被用来临时的引导硬件到实际内核vmlinuz能够接管并继续引导的状态。

initrd 映象文件是使用mkinitrd创建的。mkinitrd实用程序能够创建initrd映象文件。这个命令是RedHat专有的。其它Linux发行版或许有相应的命令。这是个很方便的实用程序。具体情况请看帮助:man mkinitrd下面的命令创建initrd映象文件。

最后生成的内核镜象有两种 zImage 以及 uImage 。其中 zImage 下载到目标板中后,可以直接用 uboot 的命令 go 来进行直接跳转。这时候内核直接解压启动。但是无法挂载文件系统,因为 go 命令没有将内核需要的相关的启动参数传递给内核。传递启动参数我们必须使用命令 bootm 来进行跳转。 Bootm 命令跳转只处理 uImage 的镜象。

uboot 源代码的 tools/ 目录下有 mkimage 工具,这个工具可以用来制作不压缩或者压缩的多种可启动映象文件。

mkimage 在制作映象文件的时候,是在原来的可执行映象文件的前面加上一个 0x40 字节的头,记录参数所指定的信息,这样 uboot 才能识别这个映象是针对哪个 CPU 体系结构的,哪个 OS 的,哪种类型,加载内存中的哪个位置, 入口点在内存的那个位置以及映象名是什么

uImage文件


  vmlinux是内核文件,zImage是一般情况下默认的压缩内核映像文件,压缩vmlinux,加上一段解压启动代码得到。而uImage 则是使用工具mkimage对普通的压缩内核映像文件(zImage)加工而得。它是uboot专用的映像文件,它是在zImage之前加上一个长度为 64字节的“头”,说明这个内核的版本、加载位置、生成时间、大小等信息;其0x40之后与zImage没区别。
其实就是一个自动跟手动的区别,有了uImage头部的描述,u-boot就知道对应Image的信息,如果没有头部则需要自己手动去搞那些参数。
如何生成 uImage文件?首先在uboot的/tools目录下寻找mkimage文件,把其copy到系统/usr/local/bin目录下,这样就完成制 作工具。然后在内核目录下运行make uImage,如果成功,便可以在arch/arm/boot/目录下发现uImage文件,其大小比 zImage多64个字节。
此外,平时调试用uImage,不用去管调整了哪些东西;zImage则是一切OK后直接烧0X0。开机就运行

编译内核前的准备


在开始编译前要进行很多准备工作。最主要的就是找到并配置好配置文件,make命令要使用到的参数都需要从这些配置文件获取。现在就让我们深入内核的根makefile吧

内核版本设定


内核的根Makefile负责构建两个主要的文件:vmlinux(内核镜像可执行文件)和模块文件moudles。
我们先看看内核的Makefile开始的几行head -n 6 Makefile

VERSION = 4
PATCHLEVEL = 2
SUBLEVEL = 3
EXTRAVERSION =
NAME = Hurr durr I\'ma sheep

 

这里写图片描述

Makefile在开始的时候定义了几个变量,这些变量决定了当前内核的版本,并且被使用在很多不同的地方,比如同一个Makefile中的KERNELVERSION
这里写图片描述

关于版本号

Linux内核有三个不同的命名方案。

参见 https://zh.wikipedia.org/wiki/Linux%E5%86%85%E6%A0%B8

早期版本
  第一种方式用于1.0版本之前(包括1.0)。第一个版本的内核是0.01。其次是0.02,0.03,0.10,0.11,0.12(第一GPL版本),0.95,0.96,0.97,0.98,0.99及1.0。从0.95版有许多的补丁发布于主要版本版本之间。

旧计划
  第二种方式用于1.0之后到2.6,版本的格式为A.B.C,其中A,B,C代表:

  A大幅度转变的内核。这是很少发生变化,只有当发生重大变化的代码和核心发生才会发生。在历史上曾改变两次的内核:1994年的1.0及1996年的2.0。

  B是指一些重大修改的内核。内核使用了传统的奇数次要版本号码的软件号码系统(用偶数的次要版本号码来表示稳定版本)。

  C是指轻微修订的内核。这个数字当有安全补丁,bug修复,新的功能或驱动程序,内核便会有变化。
  这样稳定版本来源于上一个测试版升级版本号,而一个稳定版本发展到完全成熟后就不再发展。

  自2.6.0(2003年12月)发布后,人们认识到,更短的发布周期将是有益的。自那时起,版本的格式为A.B.C.D,其中A,B,C,D代表:

  A和B是无关紧要的
  C是内核的版本

新计划
  自3.0(2011年7月)发布后,版本的格式为3.A.B,其中A,B代表:

  A是内核的版本
  B是安全补丁

  使用一种“time-based”的方式。3.0版本之前,是一种“A.B.C.D”的格式。七年里,前两个数字A.B即“2.6”保持不变,C随着新版本的发布而增加,D代表一些bug修复,安全更新,添加新特性和驱动的次数。
  3.0版本之后是“A.B.C”格式,B随着新版本的发布而增加, C代表一些bug修复,安全更新,新特性和驱动的次数。第三种方式中不再使用偶数代表稳定版,奇数代表开发版这样的命名方式。举个例子:3.7.0代表的不是开发版,而是稳定版!

  而4.0(2015年4月)发布后,则延续3.A.B的命名格式,只是将主版号变更为4。

make参数传递


  接下来我们会看到很多ifeq条件判断语句,它们负责检查传递给make的参数。内核的Makefile提供了一个特殊的编译选项makehelp,这个选项可以生成所有的可用目标和一些能传给make的有效的命令行参数。
  举个例子,首先出现的就是-V ,那么make V=1会在构建过程中输出详细的编译信息,第一个ifeq就是检查传递给make的V=n选项。

参数-v在控制构建过程中输出编译信息

使用cat -n Makefile | head -n 83 | tail -n +23 查看

# Avoid interference with shell env settings
unexport GREP_OPTIONS

# We are using a recursive build, so we need to do a little thinking
# to get the ordering right.
#
# Most importantly: sub-Makefiles should only ever modify files in
# their own directory. If in some directory we have a dependency on
# a file in another dir (which doesn\'t happen often, but it\'s often
# unavoidable when linking the built-in.o targets which finally
# turn into vmlinux), we will call a sub make in that other dir, and
# after that we are sure that everything which is in that other dir
# is now up to date.
#
# The only cases where we need to modify files which have global
# effects are thus separated out and done before the recursive
# descending is started. They are now explicitly listed as the
# prepare rule.

# Beautify output
# ---------------------------------------------------------------------------
#
# Normally, we echo the whole command before executing it. By making
# that echo $($(quiet)$(cmd)), we now have the possibility to set
# $(quiet) to choose other forms of output instead, e.g.
#
#         quiet_cmd_cc_o_c = Compiling $(RELDIR)/$@
#         cmd_cc_o_c       = $(CC) $(c_flags) -c -o $@ $<
#
# If $(quiet) is empty, the whole command will be printed.
# If it is set to "quiet_", only the short version will be printed.
# If it is set to "silent_", nothing will be printed at all, since
# the variable $(silent_cmd_cc_o_c) doesn\'t exist.
#
# A simple variant is to prefix commands with $(Q) - that\'s useful
# for commands that shall be hidden in non-verbose mode.
#
#   $(Q)ln $@ :<
#
# If KBUILD_VERBOSE equals 0 then the above command will be hidden.
# If KBUILD_VERBOSE equals 1 then the above command is displayed.
#
# To put more focus on warnings, be less verbose as default
# Use \'make V=1\' to see the full commands

ifeq ("$(origin V)", "command line")
  KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
  KBUILD_VERBOSE = 0
endif

ifeq ($(KBUILD_VERBOSE),1)
  quiet =
  Q =
else
  quiet=quiet_
  Q = @
endif

 

  如果V=n这个选项传给了make,系统就会给变量KBUILD_VERBOSE选项附上V的值,否则的话KBUILD_VERBOSE就会为0。然后系统会检查KBUILD_VERBOSE的值,以此来决定quiet和Q的值。符号@控制命令的输出,如果它被放在一个命令之前,这条命令的输出将会是CCscripts/mod/empty.o,而不是Compiling….scripts/mod/empty.o(LCTT译注:CC在makefile中一般都是编译命令)。在这段最后,系统导出了所有的变量。

参数-s

然后是-s 的控制

# If the user is running make -s (silent mode), suppress echoing of
# commands

ifneq ($(filter 4.%,$(MAKE_VERSION)),)   # make-4
ifneq ($(filter %s ,$(firstword x$(MAKEFLAGS))),)
  quiet=silent_
endif
else                    # make-3.8x
ifneq ($(filter s% -s%,$(MAKEFLAGS)),)
  quiet=silent_
endif
endif

export quiet Q KBUILD_VERBOSE

 

参数-O


  下一个ifeq语句检查的是传递给make的选项O=/dir,这个选项允许在指定的目录dir输出所有的结果文件
使用 cat -n Makefile | head -n 153 | tail -n +127 查看

# Cancel implicit rules on top Makefile
$(CURDIR)/Makefile Makefile: ;

ifneq ($(KBUILD_OUTPUT),)
# Invoke a second make in the output directory, passing relevant variables
# check that the output directory actually exists
saved-output := $(KBUILD_OUTPUT)
KBUILD_OUTPUT := $(shell mkdir -p $(KBUILD_OUTPUT) && cd $(KBUILD_OUTPUT) \
                                && /bin/pwd)
$(if $(KBUILD_OUTPUT),, \
     $(error failed to create output directory "$(saved-output)"))

PHONY += $(MAKECMDGOALS) sub-make

$(filter-out _all sub-make $(CURDIR)/Makefile, $(MAKECMDGOALS)) _all: sub-make
    @:

sub-make: FORCE
    $(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \
    -f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))

# Leave processing to above invocation of make
skip-makefile := 1
endif # ifneq ($(KBUILD_OUTPUT),)
endif # ifeq ($(KBUILD_SRC),)

  
  系统会检查变量KBUILD_SRC,它代表内核代码的顶层目录,如果它是空的(第一次执行makefile时总是空的),我们会设置变量KBUILD_OUTPUT为传递给选项O的值(如果这个选项被传进来了)。下一步会检查变量KBUILD_OUTPUT,如果已经设置好,那么接下来会做以下几件事:
  将变量KBUILD_OUTPUT的值保存到临时变量saved-output;
  尝试创建给定的输出目录;
  检查创建的输出目录,如果失败了就打印错误;
  如果成功创建了输出目录,那么就在新目录重新执行make命令(参见选项-C)。
  

选项C

下一个ifeq语句会检查传递给make的选项C
使用 cat -n Makefile | head -n 178 | tail -n +153 查看

# We process the rest of the Makefile if this is the final invocation of make
ifeq ($(skip-makefile),)

# Do not print "Entering directory ...",
# but we want to display it when entering to the output directory
# so that IDEs/editors are able to understand relative filenames.
MAKEFLAGS += --no-print-directory

# Call a source code checker (by default, "sparse") as part of the
# C compilation.
#
# Use \'make C=1\' to enable checking of only re-compiled files.
# Use \'make C=2\' to enable checking of *all* source files, regardless
# of whether they are re-compiled or not.
#
# See the file "Documentation/sparse.txt" for more details, including
# where to get the "sparse" utility.

ifeq ("$(origin C)", "command line")
  KBUILD_CHECKSRC = $(C)
endif
ifndef KBUILD_CHECKSRC
  KBUILD_CHECKSRC = 0
endif

 

选项C会告诉makefile需要使用环境变量$CHECK提供的工具来检查全部c代码,默认情况下会使用sparse。

我们可以看到之前先检查了skip-makefile ,这个变量在选项O的时候被定义为1

选项M

选项M会用来编译外部模块
使用cat -n Makefile | head -n 198 | tail -n +178 查看

# Use make M=dir to specify directory of external module to build
# Old syntax make ... SUBDIRS=$PWD is still supported
# Setting the environment variable KBUILD_EXTMOD take precedence
ifdef SUBDIRS
  KBUILD_EXTMOD ?= $(SUBDIRS)
endif

ifeq ("$(origin M)", "command line")
  KBUILD_EXTMOD := $(M)
endif

# If building an external module we do not care about the all: rule
# but instead _all depend on modules
PHONY += all
ifeq ($(KBUILD_EXTMOD),)
    _all: all
else
    _all: modules
endif

 

设置objtree

紧接着系统检查了变量KBUILD_SRC ,如果KBUILD_SRC 没有被设置,系统会设置变量srctree为当前目录./, 使用cat -n Makefile | head -n 215 | tail -n +198 进行查看

ifeq ($(KBUILD_SRC),)
        # building in the source tree
        srctree := .
else
        ifeq ($(KBUILD_SRC)/,$(dir $(CURDIR)))
                # building in a subdirectory of the source tree
                srctree := ..
        else
                srctree := $(KBUILD_SRC)
        endif
endif
objtree     := .
src     := $(srctree)
obj     := $(objtree)

VPATH       := $(srctree)$(if $(KBUILD_EXTMOD),:$(KBUILD_EXTMOD))

export srctree objtree VPATH

 

这将会告诉Makefile内核的源码树就在执行make命令的目录,然后要设置objtree和其他变量为这个目录,并且将这些变量导出。

SUBARCH获取系统架构

接着就是要获取SUBARCH的值,这个变量代表了当前的系统架构(LCTT译注:一般都指CPU架构):
使用cat Makefile | head -n 230 | tail -n +217查看

# SUBARCH tells the usermode build what the underlying arch is.  That is set
# first, and if a usermode build is happening, the "ARCH=um" on the command
# line overrides the setting of ARCH below.  If a native build is happening,
# then ARCH is assigned, getting whatever value it gets normally, and
# SUBARCH is subsequently ignored.

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

 

它其实就是执行了如下的命令

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/ 

 

我的机子是Ubuntu-Gnome14.04 LTS x86的(即x86架构)运行一下
得到如下信息
这里写图片描述

如你所见,系统执行uname得到机器、操作系统和架构的信息。因为我们得到的是uname的输出,所以我们需要做一些处理再赋给变量SUBARCH。

依据SUBARCH设置SRCARCH和hfr-arch

获得SUBARCH之后就要设置SRCARCHhfr-arch

SRCARCH提供了硬件架构相关代码的目录
hfr-arch提供了相关头文件的目录

ARCH        ?= $(SUBARCH)
CROSS_COMPILE   ?= $(CONFIG_CROSS_COMPILE:"%"=%)

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

# Additional ARCH settings for x86
ifeq ($(ARCH),i386)
        SRCARCH := x86
endif
ifeq ($(ARCH),x86_64)
        SRCARCH := x86
endif

# Additional ARCH settings for sparc
ifeq ($(ARCH),sparc32)
       SRCARCH := sparc
endif
ifeq ($(ARCH),sparc64)
       SRCARCH := sparc
endif

# Additional ARCH settings for sh
ifeq ($(ARCH),sh64)
       SRCARCH := sh
endif

# Additional ARCH settings for tile
ifeq ($(ARCH),tilepro)
       SRCARCH := tile
endif
ifeq ($(ARCH),tilegx)
       SRCARCH := tile
endif

# Where to locate arch specific headers
hdr-arch  := $(SRCARCH)

 

注意:ARCH是SUBARCH的别名。

设置KCONFIG_CONFIG

如果没有设置过代表内核配置文件路径的变量KCONFIG_CONFIG,下一步系统会设置它,默认情况下就是.config,这个文件是不是很熟悉,它就是我们make menuconfig后的那个.config配置文件,里面写入我们内核编译的所有信息
使用cat -n Makefile | head -n 292 | tail -n +289 查看

KCONFIG_CONFIG  ?= .config
export KCONFIG_CONFIG

 

CONFIG_SHELL编译内核过程中要用到的shell

使用cat -n Makefile | head -n 297 | tail -n +292 查看

# SHELL used by kbuild
CONFIG_SHELL := $(shell if [ -x "$$BASH" ]; then echo $$BASH; \
      else if [ -x /bin/bash ]; then echo /bin/bash; \
      else echo sh; fi ; fi)

 

这里写图片描述

编译器以及编译选项

接下来就要设置一组和编译内核的编译器相关的变量。我们会设置主机的C和C++的编译器及相关配置项
使用cat -n Makefile | head -n 307 | tail -n +297 查看

HOSTCC       = gcc
HOSTCXX      = g++
HOSTCFLAGS   = -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89
HOSTCXXFLAGS = -O2

ifeq ($(shell $(HOSTCC) -v 2>&1 | grep -c "clang version"), 1)
HOSTCFLAGS  += -Wno-unused-value -Wno-unused-parameter \
        -Wno-missing-field-initializers -fno-delete-null-pointer-checks
endif

 

我们可以看到Makefile在这里开始适配代表C/C++编译器的变量CC和CXX
那为什么还要HOST*这些变量呢?这是因为CC是编译内核过程中要使用的目标架构的编译器,但是HOSTCC是要被用来编译一组host程序的(下面我们就会看到)。

KBUILD_编译的目标

然后我们就看到变量KBUILD_MODULES和KBUILD_BUILTIN的定义,这两个变量决定了我们要编译什么东西(内核、模块或者两者都有):
使用cat -n Makefile | head -n 337 | tail -n +307 查看


# Decide whether to build built-in, modular, or both.
# Normally, just do built-in.

KBUILD_MODULES :=
KBUILD_BUILTIN := 1

# If we have only "make modules", don\'t compile built-in objects.
# When we\'re building modules with modversions, we need to consider
# the built-in objects during the descend as well, in order to
# make sure the checksums are up to date before we record them.

ifeq ($(MAKECMDGOALS),modules)
  KBUILD_BUILTIN := $(if $(CONFIG_MODVERSIONS),1)
endif

# If we have "make <whatever> modules", compile modules
# in addition to whatever we do anyway.
# Just "make" or "make all" shall build modules as well

ifneq ($(filter all _all modules,$(MAKECMDGOALS)),)
  KBUILD_MODULES := 1
endif

ifeq ($(MAKECMDGOALS),)
  KBUILD_MODULES := 1
endif

export KBUILD_MODULES KBUILD_BUILTIN
export KBUILD_CHECKSRC KBUILD_SRC KBUILD_EXTMOD

 

在这我们可以看到这些变量的定义,并且,如果们仅仅传递了modules给make,变量KBUILD_BUILTIN会依赖于内核配置选项CONFIG_MODVERSIONS。

Kbuild

接着下一步操作是引入下面的文件:
使用查看 cat Makefile | head -n 341 | tail -n +337

# We need some generic definitions (do not try to remake the file).
scripts/Kbuild.include: ;
include scripts/Kbuild.include

 

文件Kbuild或者又叫做KernelBuildSystem是一个用来管理构建内核及其模块的特殊框架。kbuild文件的语法与makefile一样。文件scripts/Kbuild.include为kbuild系统提供了一些常规的定义。因为我们包含了这个kbuild文件,我们可以看到和不同工具关联的这些变量的定义,这些工具会在内核和模块编译过程中被使用(比如链接器、编译器、来自binutils的二进制工具包,等等):

# Make variables (CC, etc...)
AS      = $(CROSS_COMPILE)as
LD      = $(CROSS_COMPILE)ld
CC      = $(CROSS_COMPILE)gcc
CPP     = $(CC) -E
AR      = $(CROSS_COMPILE)ar
NM      = $(CROSS_COMPILE)nm
STRIP       = $(CROSS_COMPILE)strip
OBJCOPY     = $(CROSS_COMPILE)objcopy
OBJDUMP     = $(CROSS_COMPILE)objdump
AWK     = awk
GENKSYMS    = scripts/genksyms/genksyms
INSTALLKERNEL  := installkernel
DEPMOD      = /sbin/depmod
PERL        = perl
PYTHON      = python
CHECK       = sparse

CHECKFLAGS     := -D__linux__ -Dlinux -D__STDC__ -Dunix -D__unix__ \
          -Wbitwise -Wno-return-void $(CF)
CFLAGS_MODULE   =
AFLAGS_MODULE   =
LDFLAGS_MODULE  =
CFLAGS_KERNEL   =
AFLAGS_KERNEL   =
CFLAGS_GCOV = -fprofile-arcs -ftest-coverage

 

在这些定义好的变量后面,我们又定义了两个变量:USERINCLUDE和LINUXINCLUDE。他们包含了头文件的路径(第一个是给用户用的,第二个是给内核用的),使用cat Makefile | head -n 387 | tail -n +369 查看

# Use USERINCLUDE when you must reference the UAPI directories only.
USERINCLUDE    := \
        -I$(srctree)/arch/$(hdr-arch)/include/uapi \
        -Iarch/$(hdr-arch)/include/generated/uapi \
        -I$(srctree)/include/uapi \
        -Iinclude/generated/uapi \
                -include $(srctree)/include/linux/kconfig.h

# Use LINUXINCLUDE when you must reference the include/ directory.
# Needed to be compatible with the O= option
LINUXINCLUDE    := \
        -I$(srctree)/arch/$(hdr-arch)/include \
        -Iarch/$(hdr-arch)/include/generated/uapi \
        -Iarch/$(hdr-arch)/include/generated \
        $(if $(KBUILD_SRC), -I$(srctree)/include) \
        -Iinclude \
        $(USERINCLUDE)

 

以及给C编译器的标准标志,使用cat Makefile | head -n 419 | tail -n +387查看


KBUILD_CPPFLAGS := -D__KERNEL__

KBUILD_CFLAGS   := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
           -fno-strict-aliasing -fno-common \
           -Werror-implicit-function-declaration \
           -Wno-format-security \
           -std=gnu89

KBUILD_AFLAGS_KERNEL :=
KBUILD_CFLAGS_KERNEL :=
KBUILD_AFLAGS   := -D__ASSEMBLY__
KBUILD_AFLAGS_MODULE  := -DMODULE
KBUILD_CFLAGS_MODULE  := -DMODULE
KBUILD_LDFLAGS_MODULE := -T $(srctree)/scripts/module-common.lds

# Read KERNELRELEASE from include/config/kernel.release (if it exists)
KERNELRELEASE = $(shell cat include/config/kernel.release 2> /dev/null)
KERNELVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)

export VERSION PATCHLEVEL SUBLEVEL KERNELRELEASE KERNELVERSION
export ARCH SRCARCH CONFIG_SHELL HOSTCC HOSTCFLAGS CROSS_COMPILE AS LD CC
export CPP AR NM STRIP OBJCOPY OBJDUMP
export MAKE AWK GENKSYMS INSTALLKERNEL PERL PYTHON UTS_MACHINE
export HOSTCXX HOSTCXXFLAGS LDFLAGS_MODULE CHECK CHECKFLAGS

export KBUILD_CPPFLAGS NOSTDINC_FLAGS LINUXINCLUDE OBJCOPYFLAGS LDFLAGS
export KBUILD_CFLAGS CFLAGS_KERNEL CFLAGS_MODULE CFLAGS_GCOV CFLAGS_KASAN
export KBUILD_AFLAGS AFLAGS_KERNEL AFLAGS_MODULE
export KBUILD_AFLAGS_MODULE KBUILD_CFLAGS_MODULE KBUILD_LDFLAGS_MODULE
export KBUILD_AFLAGS_KERNEL KBUILD_CFLAGS_KERNEL
export KBUILD_ARFLAGS

 

这并不是最终确定的编译器标志,它们还可以在其他makefile里面更新(比如arch/里面的kbuild)。变量定义完之后,全部会被导出供其他makefile使用。

下面的两个变量RCS_FIND_IGNORE和RCS_TAR_IGNORE包含了被版本控制系统忽略的文件,使用cat Makefile | head -n 432 | tail -n +419 查看

# When compiling out-of-tree modules, put MODVERDIR in the module
# tree rather than in the kernel tree. The kernel tree might
# even be read-only.
export MODVERDIR := $(if $(KBUILD_EXTMOD),$(firstword $(KBUILD_EXTMOD))/).tmp_versions

# Files to ignore in find ... statements

export RCS_FIND_IGNORE := \( -name SCCS -o -name BitKeeper -o -name .svn -o    \
              -name CVS -o -name .pc -o -name .hg -o -name .git \) \
              -prune -o
export RCS_TAR_IGNORE := --exclude SCCS --exclude BitKeeper --exclude .svn \
             --exclude CVS --exclude .pc --exclude .hg --exclude .git

 

然后后面的一大块内容负责根据各种配置文件(make*.config)生成不同目标内核的
可以使用cat Makefile | head -n 593 | tail -n +432 进行查看,内容较多,我们在这里就不一一列举了。
下面让我么直接进入make构建的过程。

内核编译过程


现在我们已经完成了所有的配置工作,根makefile的下一步工作就是和编译内核相关的了。
在这之前,我们不会在终端看到make命令输出的任何东西。

但是现在编译的第一步开始了,好吧,我们知道make后,最终的结果叫vmlinux,那我们就找找这个神奇的东西是怎么产生的吧。

终极目标vmlinux


这里我们需要从内核根makefile的594行开始,这里可以看到目标vmlinux的构建命令

使用 cat Makefile | head -n 606 | tail -n +594 查看


# The all: target is the default when no target is given on the
# command line.
# This allow a user to issue only \'make\' to build a kernel including modules
# Defaults to vmlinux, but the arch makefile usually adds further targets
all: vmlinux

# The arch Makefile can set ARCH_{CPP,A,C}FLAGS to override the default
# values of the respective KBUILD_* variables
ARCH_CPPFLAGS :=
ARCH_AFLAGS :=
ARCH_CFLAGS :=
include arch/$(SRCARCH)/Makefile

 

目标all:是在命令行如果不指定具体目标时默认使用的目标。
你可以看到这里包含了架构相关的makefile(在这里就指的是arch/x86/Makefile)。从这一时刻起,我们会从这个makefile继续进行下去。
如我们所见,目标all依赖于根makefile后面声明的vmlinux,我们可以使用cat Makefile | head -n 922 | tail -n +920 来查看

# Include targets which we want to
# execute if the rest of the kernel build went well.
vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORC

 

vmlinux是linux内核的静态链接可执行文件格式。脚本scripts/link-vmlinux.sh把不同的编译好的子模块链接到一起形成了vmlinux。

vmlinux-deps

同时我们可以发现vlinux依赖于是vmlinux-deps,我们查找一下它cat -n Makefile | grep vmlinux-deps
这里写图片描述
发现它定义在914行,内容如下

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

 

它是由内核代码下的每个*目录的built-in.o组成的。
之后我们还会检查内核所有的目录,kbuild会编译各个目录下所有的对应$(obj-y)的源文件。接着调用$(LD)-r把这些文件合并到一个build-in.o文件里。当然此时我们还没有vmlinux-deps,所以目标vmlinux现在还不会被构建。对我而言vmlinux-deps包含下面的文件:

arch/x86/kernel/vmlinux.lds arch/x86/kernel/head_64.o
arch/x86/kernel/head64.o    arch/x86/kernel/head.o
init/built-in.o             usr/built-in.o
arch/x86/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
lib/lib.a                   arch/x86/lib/lib.a
lib/built-in.o              arch/x86/lib/built-in.o
drivers/built-in.o          sound/built-in.o
firmware/built-in.o         arch/x86/pci/built-in.o
arch/x86/power/built-in.o   arch/x86/video/built-in.o
net/built-in.o

 

vmlinux-dirs


内核中有这么多目录,Makefile是怎么知道这些目录的呢,让我们继续往下看,使用cat -n Makefile | head -n 940 | tail -n +936 查看

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

 

我们会发现vmlinux-deps是基于vmlinux-dirs
继续往下,使用cat Makefile | head -n 950 | tail -n +940
查看

# Handle descending into subdirectories listed in $(vmlinux-dirs)
# Preset locale variables to speed up the build process. Limit locale
# tweaks to this spot to avoid wrong language settings when running

# make menuconfig etc.

# Error messages still appears in the original language

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

 

就像我们看到的,vmlinux-dir依赖于两部分:prepare和scripts。

prepare


第一个prepare定义在内核的根makefile中,准备工作分成三个阶段。

我们继续往下看,使用 cat -n Makefile | head -n 996 | tail -n +959


# Things we need to do before we recursively start building the kernel
# or the modules are listed in "prepare".
# A multi level approach is used. prepareN is processed before prepareN-1.
# archprepare is used in arch Makefiles and when processed asm symlink,
# version.h and scripts_basic is processed / created.

# Listed in dependency order
PHONY += prepare archprepare prepare0 prepare1 prepare2 prepare3

# prepare3 is used to check if we are building in a separate output directory,
# and if so do:
# 1) Check that make has not been executed in the kernel src $(srctree)
prepare3: include/config/kernel.release
ifneq ($(KBUILD_SRC),)
    @$(kecho) \'  Using $(srctree) as source for kernel\'
    $(Q)if [ -f $(srctree)/.config -o -d $(srctree)/include/config ]; then \
        echo >&2 "  $(srctree) is not clean, please run \'make mrproper\'"; \
        echo >&2 "  in the \'$(srctree)\' directory.";\
        /bin/false; \
    fi;
endif

# prepare2 creates a makefile if using a separate output directory
prepare2: prepare3 outputmakefile asm-generic

prepare1: prepare2 $(version_h) include/generated/utsrelease.h \
                   include/config/auto.conf
    $(cmd_crmodverdir)

archprepare: archheaders archscripts prepare1 scripts_basic

prepare0: archprepare FORCE
    $(Q)$(MAKE) $(build)=.

# All the preparing..
prepare: prepare0

 

第一个prepare0展开到archprepare,后者又展开到archheader和archscripts,这两个变量定义在对应架构目录下的Makefile,x86架构就是arch/x86让我们看看这个文件。
x86特定的makefile从变量定义开始,这些变量都是和特定架构的配置文件(defconfig,等等)有关联。在定义了编译16-bit代码的编译选项之后,根据变量BITS的值,如果是32,汇编代码、链接器、以及其它很多东西(全部的定义都可以在arch/x86/Makefile找到)对应的参数就是i386,而64就对应的是x86_84。

archheaders

首先是archheaders

查找archheaders

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

 

archscripts

接着是archscripts

查找archscripts

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

 

scripts_basic

然后是scripts_basic
通过查找发现我们可以看到archscripts是依赖于根Makefile里的scripts_basic。
查找scrpits_basic
查找scripts_basic
使用cat Makefile | head -n 441 | tail -n +432 查看

#====================================================
# Rules shared between *config targets and build targets

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

 

首先我们可以看出scripts_basic是按照scripts/basic的makefile执行make的

下面我们看看scripts/basic下的makefile都有什么
scripts/basic/Makefile

scripts/basic/Makefile包含了编译两个主机程序fixdep和bin2的目标

第一个工具是fixdep:
用来优化gcc生成的依赖列表,然后在重新编译源文件的时候告诉make。
第二个工具是bin2c,
它依赖于内核配置选项CONFIG_BUILD_BIN2C,并且它是一个用来将标准输入接口(LCTT译注:即stdin)收到的二进制流通过标准输出接口(即:stdout)转换成C头文件的非常小的C程序。你可能注意到这里有些奇怪的标志,如hostprogs-y等。这个标志用于所有的kbuild文件,更多的信息你可以从documentation获得。

在我们这里,hostprogs-y告诉kbuild这里有个名为fixed的程序,这个程序会通过和Makefile相同目录的fixdep.c编译而来。

我们make时执行make之后,终端的第一个输出就是kbuild的结果:
这里写图片描述

现在scripts_basic的工作完成了,现在archscripts 开始工作了,重新回到archscripts的地方,

    $(Q)$(MAKE) $(build)=arch/x86/tools relocs

 

当目标script_basic被执行,目标archscripts就会make arch/x86/tools下的makefile和目标relocs
包含了重定位的信息的代码relocs_32.c和relocs_64.c将会被编译,这可以在make的输出中看到,下面仍然是make的工作
这里写图片描述

下面我们继续接着进行make,我们发现在编译完relocs.c之后会检查version.h
这里写图片描述
使用 cat Makefile | head -n 1021 | tail -n +1017 查看

$(version_h): $(srctree)/Makefile FORCE
    $(call filechk,version.h)
    $(Q)rm -f $(old_version_h)

 

以及在内核的根Makefiel使用arch/x86/include/generated/asm的目标asm-generic来构建generic汇编头文件。
这里写图片描述
在目标asm-generic之后,archprepare就完成了,所以目标prepare0会接着被执行,如我上面所写:

prepare0: archprepare FORCE
    $(Q)$(MAKE) $(build)=.

 

注意build,它是定义在文件scripts/Kbuild.include,内容是这样的:
这里写图片描述
脚本scripts/Makefile.build通过参数obj给定的目录找到Kbuild文件,然后引入kbuild文件

include $(kbuild-file)

 

并根据这个构建目标。我们这里.包含了生成kernel/bounds.s和arch/x86/kernel/asm-offsets.s的Kbuild文件。在此之后,目标prepare就完成了它的工作。

scripts


vmlinux-dirs也依赖于第二个目标scripts,它会编译接下来的几个程序:filealias,mk_elfconfig,modpost等等。
这里写图片描述
与prepare类似,所以我们在这里就不细讲了。

开始编译vmlinux-dirs


之后,scripts/host-programs就可以开始编译我们的目标vmlinux-dirs了。

首先,我们先来理解一下vmlinux-dirs都包含了那些东西。在我们的例子中它包含了下列内核目录的路径
这里写图片描述
我们可以在内核的根Makefile里找到vmlinux-dirs的定义:
这里写图片描述
使用cat -n Makefile | head -n 905 | tail -n +891 查看vmlinux-dirs的定义

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

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

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

 

前面我们已经知道vmlinux-dir会依赖与prepare和scripts

$(vmlinux-dirs): prepare scripts
$(Q)$(MAKE) $(build)=$@

 

符号$@在这里代表了vmlinux-dirs,这就表明程序会递归遍历从vmlinux-dirs以及它内部的全部目录(依赖于配置),并且在对应的目录下执行make命令。我们可以在输出看到结果
这里写图片描述

在make的最后阶段,当所有的目录编译结束后,每个目录下的源代码将会被编译并且链接到built-io.o里。

神秘的built-in.o


在最后的链接过程中,我们可以看到,几乎所有的依赖条件中,都会生成一个built-in.o的文件。 那这个文件,是怎么生成的呢?
这里写图片描述

生成vmlinux


那么问题来了,makefile是怎么把内核目录中编译生成的build-in.o链接在一起生成vmlinux的呢?
现在我们回到目标vmlinux上。你应该还记得,目标vmlinux是在内核的根makefile里。在链接vmlinux之前,系统会构建samples,Documentation等等。
接着我们使用cat -n Makefile | head -n 936 | tail -n +922 查看

vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE
ifdef CONFIG_HEADERS_CHECK
    $(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif
ifdef CONFIG_SAMPLES
    $(Q)$(MAKE) $(build)=samples
endif
ifdef CONFIG_BUILD_DOCSRC
    $(Q)$(MAKE) $(build)=Documentation
endif
ifdef CONFIG_GDB_SCRIPTS
    $(Q)ln -fsn `cd $(srctree) && /bin/pwd`/scripts/gdb/vmlinux-gdb.py
endif
    +$(call if_changed,link-vmlinux)

 

我们可以看到vmlinux依赖于$(vmlinux-deps) 但是还需要一个shell脚本scripts/link-vmlinux.sh 这个脚本是用来干嘛的,不急我们慢慢来。
我们直接看最后使用+$(call if_changed,link-vmlinux),真相正在一步步浮出水面。

我们查看一下这个命令
查找link-vmlinux

这个命令是cmd_link-vmlinux,就定义在主Makefile中第917行

cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)

 

(CONFIGSHELL)bashshell

< 表示第一个以来目标,那么在vmlinux目标中,第一个目标是 scripts/link-vmlinux.sh
那么这个命令展开就成为

/bin/bash scripts/link-vmlinux.sh ld -m elf_i386 --emit-relocs --build-id

 

现在明晰了在这里调用脚本scripts/link-vmlinux.sh的,把所有的built-in.o链接成一个静态可执行文件vmlinux,和生成System.map。
那么link-vmlinux.sh是怎么做到得呢,使用该cat -n link-vmlinux.sh | head -n 239 | tail -n +229 查看这个脚本的信息
这里写图片描述

info LD vmlinux
vmlinux_link "${kallsymso}" vmlinux

if [ -n "${CONFIG_BUILDTIME_EXTABLE_SORT}" ]; then
    info SORTEX vmlinux
    sortextable vmlinux
fi

info SYSMAP System.map
mksysmap vmlinux System.map

 

使用了脚本中vmlinux_link这个函数来生成vmlinux,使用mksysmap生成System.map
下面是vmlinux_link 函数的定义,cat -n link-vmlinux.sh | head -n 69 | tail -n +51

vmlinux_link()
{
    local lds="${objtree}/${KBUILD_LDS}"

    if [ "${SRCARCH}" != "um" ]; then
        ${LD} ${LDFLAGS} ${LDFLAGS_vmlinux} -o ${2}                  \
            -T ${lds} ${KBUILD_VMLINUX_INIT}                     \
            --start-group ${KBUILD_VMLINUX_MAIN} --end-group ${1}
    else
        ${CC} ${CFLAGS_vmlinux} -o ${2}                              \
            -Wl,-T,${lds} ${KBUILD_VMLINUX_INIT}                 \
            -Wl,--start-group                                    \
                 ${KBUILD_VMLINUX_MAIN}                      \
            -Wl,--end-group                                      \
            -lutil ${1}
        rm -f linux
    fi
}

 

然后是mksysmap,使用cat -n link-vmlinux.sh | head -n 107 | tail -n +103 查看

mksysmap()
{
    ${CONFIG_SHELL} "${srctree}/scripts/mksysmap" ${1} ${2}
}

 

最后我们来看看下面的输出:
生成vmlinux
vmlinux和System.map生成在内核源码树根目录下。

这就是全部了,vmlinux构建好了,下一步就是创建bzImage.
这里写图片描述

构建bzImage


bzImage就是压缩了的linux内核镜像。我们可以在构建了vmlinux之后通过执行makebzImage获得bzImage。同时我们可以仅仅执行make而不带任何参数也可以生成bzImage,因为它是在arch/x86/kernel/Makefile里预定义的、默认生成的镜像。

我们在makefile中查找一下

这里写图片描述

我们可以看到bzImage是依赖于vmlinux生成的,

我们使用cat -n Makefile | head -n 237 | tail -n +215 查看其构建信息

####
# boot loader support. Several targets are kept for legacy purposes

boot := arch/x86/boot

BOOT_TARGETS = bzlilo bzdisk fdimage fdimage144 fdimage288 isoimage

PHONY += bzImage $(BOOT_TARGETS)

# Default kernel to build
all: bzImage

# KBUILD_IMAGE specify target image being built
KBUILD_IMAGE := $(boot)/bzImage

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/$@

 

setup.bin


在这里我们可以看到第一次为$(boot)==arch/x86/boot 目录执行了make 操作

我们进入这个目录看看。这个makefile是如何工作生成bzImage的
这里写图片描述
我们会发现bzImage依赖于setup.bin和vmlinux.bin
使用cat -n Makefile | head -n 112 | tail -n +107 我们可以查看到

$(obj)/setup.bin: $(obj)/setup.elf FORCE
    $(call if_changed,objcopy)

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

 

那么我们现在的主要目标是编译目录arch/x86/boot和arch/x86/boot/compressed的代码,构建setup.bin和vmlinux.bin,最后用这两个文件生成bzImage。

这里写图片描述

第一个目标是定义在arch/x86/boot/Makefile的$(obj)/setup.elf:
这里写图片描述
我们接着使用cat -n Makefile | head -n 105 | tail -n +103 查看如何生成setup.elf

$(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE
    $(call if_changed,ld)

 

通过setup.ld来检列所有的setup_objs的目标文件来生成setup.elf

vmlinux.bin


下一个源码文件是arch/x86/boot/header.S,这个是一个汇编文件
这里写图片描述

1. 列表内容

但是我们不能现在就编译它,因为这个目标依赖于下面两个头文件:

$(obj)/header.o: $(obj)/voffset.h $(obj)/zoffset.h

 

第一个头文件voffset.h是使用sed脚本生成的
这里写图片描述
包含用nm工具从vmlinux获取的两个地址:

#define VO__end 0xffffffff82ab0000
#define VO__text 0xffffffff81000000

 

这两个地址是内核的起始和结束地址。

第二个头文件zoffset.h在arch/x86/boot/compressed/Makefile可以看出是依赖于目标vmlinux的
这里写图片描述

$(obj)/zoffset.h: $(obj)/compressed/vmlinux FORCE
    $(call if_changed,zoffset)

 

然后编译目录arch/x86/boot/compressed下的源代码,然后生成vmlinux.bin、vmlinux.bin.bz2,和编译工具mkpiggy。
这里写图片描述

vmlinux.bin是去掉了调试信息和注释的vmlinux二进制文件,加上了占用了u32(LCTT译注:即4-Byte)的长度信息的vmlinux.bin.all压缩后就是vmlinux.bin.bz2。其中vmlinux.bin.all包含了vmlinux.bin和vmlinux.relocs(LCTT译注:vmlinux的重定位信息),其中vmlinux.relocs是vmlinux经过程序relocs处理之后的vmlinux镜像(见上文所述)。
我们现在已经获取到了这些文件,汇编文件piggy.S将会被mkpiggy生成、然后编译:

MKPIGGY arch/x86/boot/compressed/piggy.S
 AS      arch/x86/boot/compressed/piggy.o

 

这个汇编文件会包含经过计算得来的、压缩内核的偏移信息。处理完这个汇编文件,我们就可以看到zoffset生成了:

ZOFFSET arch/x86/boot/zoffset.h

 

现在zoffset.h和voffset.h已经生成了,arch/x86/boot里的源文件可以继续编译,直到
所有的源代码会被编译,他们最终会被链接到setup.elf
这里写图片描述

ld -m elf_x86_64   -T arch/x86/boot/setup.ld arch/x86/boot/a20.o arch/x86/boot/bioscall.o arch/x86/boot/cmdline.o arch/x86/boot/copy.o arch/x86/boot/cpu.o arch/x86/boot/cpuflags.o arch/x86/boot/cpucheck.o arch/x86/boot/early_serial_console.o arch/x86/boot/edd.o arch/x86/boot/header.o arch/x86/boot/main.o arch/x86/boot/mca.o arch/x86/boot/memory.o arch/x86/boot/pm.o arch/x86/boot/pmjump.o arch/x86/boot/printf.o arch/x86/boot/regs.o arch/x86/boot/string.o arch/x86/boot/tty.o arch/x86/boot/video.o arch/x86/boot/video-mode.o arch/x86/boot/version.o arch/x86/boot/video-vga.o arch/x86/boot/video-vesa.o arch/x86/boot/video-bios.o -o arch/x86/boot/setup.elf

 

最后的两件事是创建包含目录arch/x86/boot/*下的编译过的代码的setup.bin:

objcopy  -O binary arch/x86/boot/setup.elf arch/x86/boot/setup.bin

 

以及从vmlinux生成vmlinux.bin:

objcopy  -O binary -R .note -R .comment -S arch/x86/boot/compressed/vmlinux arch/x86/boot/vmlinux.bin

 

生成bzImage


最后,我们编译主机程序arch/x86/boot/tools/build.c,它将会用来把setup.bin和vmlinux.bin打包成bzImage:

arch/x86/boot/tools/build arch/x86/boot/setup.bin arch/x86/boot/vmlinux.bin arch/x86/boot/zoffset.h arch/x86/boot/bzImage

 

实际上bzImage就是把setup.bin和vmlinux.bin连接到一起。最终我们会看到输出结果,就和那些用源码编译过内核的同行的结果一样:
aa