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
之后就要设置SRCARCH
和hfr-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:
$(Q)$(MAKE) $(build)=arch/x86/entry/syscalls all
archscripts
接着是archscripts
archscripts: scripts_basic
$(Q)$(MAKE) $(build)=arch/x86/tools relocs
scripts_basic
然后是scripts_basic
通过查找发现我们可以看到archscripts是依赖于根Makefile里的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包含了编译两个主机程序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)
,真相正在一步步浮出水面。
我们查看一下这个命令
这个命令是cmd_link-vmlinux
,就定义在主Makefile中第917行
cmd_link-vmlinux = $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)
(C
< 表示第一个以来目标,那么在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和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
,这个是一个汇编文件
但是我们不能现在就编译它,因为这个目标依赖于下面两个头文件:
$(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连接到一起。最终我们会看到输出结果,就和那些用源码编译过内核的同行的结果一样: