TranslateProject/translated/tech/20150728 Process of the Linux kernel building.md
zl a4e422dd35 move file
翻译完成 Process of the Linux kernel building.md
并移动到translated
2015-08-26 10:01:29 +08:00

31 KiB
Raw Blame History

如何构建Linux 内核

介绍

我不会告诉你怎么在自己的电脑上去构建、安装一个定制化的Linux 内核,这样的资料 太多了,它们会对你有帮助。本文会告诉你当你在内核源码路径里敲下make 时会发生什么。当我刚刚开始学习内核代码时,Makefile 是我打开的第一个文件,这个文件看起来真令人害怕 :)。那时候这个Makefile 还只包含了1591 行代码,当我开始写本文是,这个Makefile 已经是第三个候选版本了。

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

如果你已经很了解make 工具那是最好,但是我也会描述本文出现的相关代码。

让我们开始吧

编译内核前的准备

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

内核的根Makefile 负责构建两个主要的文件:vmlinux (内核镜像可执行文件)和模块文件。内核的 Makefile 从此处开始:

VERSION = 4
PATCHLEVEL = 2
SUBLEVEL = 0
EXTRAVERSION = -rc3
NAME = Hurr durr I'ma sheep

这些变量决定了当前内核的版本,并且被使用在很多不同的地方,比如KERNELVERSION

KERNELVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)

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

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

export quiet Q KBUILD_VERBOSE

如果V=n 这个选项传给了make ,系统就会给变量KBUILD_VERBOSE 选项附上V 的值,否则的话KBUILD_VERBOSE 就会为0。然后系统会检查KBUILD_VERBOSE 的值,以此来决定quietQ 的值。符号@ 控制命令的输出,如果它被放在一个命令之前,这条命令的执行将会是CC scripts/mod/empty.o,而不是Compiling .... scripts/mod/empty.oCC 在makefile 中一般都是编译命令)。最后系统仅仅导出所有的变量。下一个ifeq 语句检查的是传递给make 的选项O=/dir,这个选项允许在指定的目录dir 输出所有的结果文件:

ifeq ($(KBUILD_SRC),)

ifeq ("$(origin O)", "command line")
  KBUILD_OUTPUT := $(O)
endif

ifneq ($(KBUILD_OUTPUT),)
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)"))

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

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

下一个ifeq 语句会检查传递给make 的选项CM

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

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

第一个选项C 会告诉makefile 需要使用环境变量$CHECK 提供的工具来检查全部c 代码,默认情况下会使用sparse。第二个选项M 会用来编译外部模块(本文不做讨论)。因为设置了这两个变量,系统还会检查变量KBUILD_SRC,如果KBUILD_SRC 没有被设置,系统会设置变量srctree.

ifeq ($(KBUILD_SRC),)
        srctree := .
endif
		
objtree	:= .
src		:= $(srctree)
obj		:= $(objtree)

export srctree objtree VPATH

这将会告诉Makefile 内核的源码树就在执行make 命令的目录。然后要设置objtree 和其他变量为执行make 命令的目录,并且将这些变量导出。接着就是要获取SUBARCH 的值这个变量代表了当前的系统架构一般都指CPU 架构):

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 得到机器、操作系统和架构的信息。因为我们得到的是uname 的输出,所以我们需要做一些处理在赋给变量SUBARCH 。获得SUBARCH 之后就要设置SRCARCHhfr-archSRCARCH提供了硬件架构相关代码的目录,hfr-arch 提供了相关头文件的目录:

ifeq ($(ARCH),i386)
        SRCARCH := x86
endif
ifeq ($(ARCH),x86_64)
        SRCARCH := x86
endif

hdr-arch  := $(SRCARCH)

注意:ARCHSUBARCH 的别名。如果没有设置过代表内核配置文件路径的变量KCONFIG_CONFIG,下一步系统会设置它,默认情况下就是.config

KCONFIG_CONFIG	?= .config
export KCONFIG_CONFIG

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

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

接下来就要设置一组和编译内核的编译器相关的变量。我们会设置主机的CC++ 的编译器及相关配置项:

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

下一步会去适配代表编译器的变量CC,那为什么还要HOST* 这些选项呢?这是因为CC 是编译内核过程中要使用的目标架构的编译器,但是HOSTCC 是要被用来编译一组host 程序的(下面我们就会看到)。然后我们就看看变量KBUILD_MODULESKBUILD_BUILTIN 的定义,这两个变量决定了我们要编译什么东西(内核、模块还是其他):

KBUILD_MODULES :=
KBUILD_BUILTIN := 1

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

在这我们可以看到这些变量的定义,并且,如果们仅仅传递了modulesmake,变量KBUILD_BUILTIN 会依赖于内核配置选项CONFIG_MODVERSIONS。下一步操作是引入下面的文件:

include scripts/Kbuild.include

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

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
...
...
...

在这些定义好的变量后面,我们又定义了两个变量:USERINCLUDELINUXINCLUDE。他们包含了头文件的路径(第一个是给用户用的,第二个是给内核用的):

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

LINUXINCLUDE    := \
		-I$(srctree)/arch/$(hdr-arch)/include \
		...

以及标准的C 编译器标志:

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

这并不是最终确定的编译器标志他们还可以在其他makefile 里面更新(比如arch/ 里面的kbuild。变量定义完之后全部会被导出供其他makefile 使用。下面的两个变量RCS_FIND_IGNORERCS_TAR_IGNORE 包含了被版本控制系统忽略的文件:

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

这就是全部了,我们已经完成了所有的准备工作,下一个点就是如果构建vmlinux.

直面构建内核

现在我们已经完成了所有的准备工作根makefile内核根目录下的makefile的下一步工作就是和编译内核相关的了。在我们执行make 命令之前我们不会在终端看到任何东西。但是现在编译的第一步开始了这里我们需要从内核根makefile的的598 行开始,这里可以看到目标vmlinux

all: vmlinux
	include arch/$(SRCARCH)/Makefile

不要操心我们略过的从export RCS_FIND_IGNORE.....all: vmlinux..... 这一部分makefile 代码,他们只是负责根据各种配置文件生成不同目标内核的,因为之前我就说了这一部分我们只讨论构建内核的通用途径。

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

vmlinux: scripts/link-vmlinux.sh $(vmlinux-deps) FORCE

vmlinux 是linux 内核的静态链接可执行文件格式。脚本scripts/link-vmlinux.sh 把不同的编译好的子模块链接到一起形成了vmlinux。第二个目标是vmlinux-deps,它的定义如下:

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

它是由内核代码下的每个顶级目录的built-in.o 组成的。之后我们还会检查内核所有的目录,kbuild 会编译各个目录下所有的对应$obj-y 的源文件。接着调用$(LD) -r 把这些文件合并到一个build-in.o 文件里。此时我们还没有vmloinux-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

下一个可以被执行的目标如下:

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

就像我们看到的,vmlinux-dir 依赖于两部分:preparescripts。第一个prepare 定义在内核的根makefile ,准备工作分成三个阶段:

prepare: prepare0
prepare0: archprepare FORCE
	$(Q)$(MAKE) $(build)=.
archprepare: archheaders archscripts prepare1 scripts_basic

prepare1: prepare2 $(version_h) include/generated/utsrelease.h \
                   include/config/auto.conf
	$(cmd_crmodverdir)
prepare2: prepare3 outputmakefile asm-generic

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

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

这个makefile 里第二个目标就是archscripts

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

我们可以看到archscripts 是依赖于根Makefile里的scripts_basic 。首先我们可以看出scripts_basic 是按照scripts/basic 的mekefile 执行make 的:

scripts_basic:
	$(Q)$(MAKE) $(build)=scripts/basic

scripts/basic/Makefile包含了编译两个主机程序fixdepbin2 的目标:

hostprogs-y	:= fixdep
hostprogs-$(CONFIG_BUILD_BIN2C)     += bin2c
always		:= $(hostprogs-y)

$(addprefix $(obj)/,$(filter-out fixdep,$(always))): $(obj)/fixdep

第一个工具是fixdep:用来优化gcc 生成的依赖列表然后在重新编译源文件的时候告诉make。第二个工具是bin2c,他依赖于内核配置选项CONFIG_BUILD_BIN2C,并且它是一个用来将标准输入接口即stdin收到的二进制流通过标准输出接口stdout转换成C 头文件的非常小的C 程序。你可以注意到这里有些奇怪的标志,如hostprogs-y等。这些标志使用在所有的kbuild 文件,更多的信息你可以从documentation 获得。在我们的用例hostprogs-y 中,他告诉kbuild 这里有个名为fixed 的程序,这个程序会通过和Makefile 相同目录的fixdep.c 编译而来。执行make 之后,终端的第一个输出就是kbuild 的结果:

$ make
  HOSTCC  scripts/basic/fixdep

当目标script_basic 被执行,目标archscripts 就会make arch/x86/tools 下的makefile 和目标relocs:

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

代码relocs_32.crelocs_64.c 包含了重定位 的信息,将会被编译,者可以在make 的输出中看到:

  HOSTCC  arch/x86/tools/relocs_32.o
  HOSTCC  arch/x86/tools/relocs_64.o
  HOSTCC  arch/x86/tools/relocs_common.o
  HOSTLD  arch/x86/tools/relocs

在编译完relocs.c 之后会检查version.h:

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

我们可以在输出看到它:

CHK     include/config/kernel.release

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

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

注意build,它是定义在文件scripts/Kbuild.include,内容是这样的:

build := -f $(srctree)/scripts/Makefile.build obj

或者在我们的例子中,他就是当前源码目录路径——.

$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=.

参数obj 会告诉脚本scripts/Makefile.build 那些目录包含kbuild 文件,脚本以此来寻找各个kbuild 文件:

include $(kbuild-file)

然后根据这个构建目标。我们这里. 包含了Kbuild,就用这个文件来生成kernel/bounds.sarch/x86/kernel/asm-offsets.s。这样目标prepare 就完成了它的工作。vmlinux-dirs 也依赖于第二个目标——scripts scripts会编译接下来的几个程序:filealiasmk_elfconfig,modpost等等。scripts/host-programs 编译完之后,我们的目标vmlinux-dirs 就可以开始编译了。第一步,我们先来理解一下vmlinux-dirs 都包含了那些东西。在我们的例子中它包含了接下来要使用的内核目录的路径:

init usr arch/x86 kernel mm fs ipc security crypto block
drivers sound firmware arch/x86/pci arch/x86/power
arch/x86/video net lib arch/x86/lib

我们可以在内核的根Makefile 里找到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)))

init-y		:= init/
drivers-y	:= drivers/ sound/ firmware/
net-y		:= net/
libs-y		:= lib/
...
...
...

这里我们借助函数patsubstfilter去掉了每个目录路径里的符号/,并且把结果放到vmlinux-dirs 里。所以我们就有了vmlinux-dirs 里的目录的列表,以及下面的代码:

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

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

  CC      init/main.o
  CHK     include/generated/compile.h
  CC      init/version.o
  CC      init/do_mounts.o
  ...
  CC      arch/x86/crypto/glue_helper.o
  AS      arch/x86/crypto/aes-x86_64-asm_64.o
  CC      arch/x86/crypto/aes_glue.o
  ...
  AS      arch/x86/entry/entry_64.o
  AS      arch/x86/entry/thunk_64.o
  CC      arch/x86/entry/syscall_64.o

每个目录下的源代码将会被编译并且链接到built-io.o 里:

$ find . -name built-in.o
./arch/x86/crypto/built-in.o
./arch/x86/crypto/sha-mb/built-in.o
./arch/x86/net/built-in.o
./init/built-in.o
./usr/built-in.o
...
...

好了,所有的built-in.o 都构建完了,现在我们回到目标vmlinux 上。你应该还记得,目标vmlinux 是在内核的根makefile 里。在链接vmlinux 之前,系统会构建samples, Documentation等等,但是如上文所述,我不会在本文描述这些。

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

你可以看到,vmlinux 的调用脚本scripts/link-vmlinux.sh 的主要目的是把所有的built-in.o 链接成一个静态可执行文件、生成System.map。 最后我们来看看下面的输出:

  LINK    vmlinux
  LD      vmlinux.o
  MODPOST vmlinux.o
  GEN     .version
  CHK     include/generated/compile.h
  UPD     include/generated/compile.h
  CC      init/version.o
  LD      init/built-in.o
  KSYM    .tmp_kallsyms1.o
  KSYM    .tmp_kallsyms2.o
  LD      vmlinux
  SORTEX  vmlinux
  SYSMAP  System.map

以及内核源码树根目录下的vmlinuxSystem.map

$ ls vmlinux System.map 
System.map  vmlinux

这就是全部了,vmlinux 构建好了,下一步就是创建bzImage.

制作bzImage

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

all: bzImage

让我们看看这个目标,他能帮助我们理解这个镜像是怎么构建的。我已经说过了bzImage 师被定义在arch/x86/kernel/Makefile,定义如下:

bzImage: vmlinux
	$(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/$@

在这里我们可以看到第一次为boot 目录执行make,在我们的例子里是这样的:

boot := arch/x86/boot

现在的主要目标是编译目录arch/x86/bootarch/x86/boot/compressed 的代码,构建setup.binvmlinux.bin,然后用这两个文件生成bzImage。第一个目标是定义在arch/x86/boot/Makefile$(obj)/setup.elf:

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

我们已经在目录arch/x86/boot有了链接脚本setup.ld,并且将变量SETUP_OBJS 扩展到boot 目录下的全部源代码。我们可以看看第一个输出:

  AS      arch/x86/boot/bioscall.o
  CC      arch/x86/boot/cmdline.o
  AS      arch/x86/boot/copy.o
  HOSTCC  arch/x86/boot/mkcpustr
  CPUSTR  arch/x86/boot/cpustr.h
  CC      arch/x86/boot/cpu.o
  CC      arch/x86/boot/cpuflags.o
  CC      arch/x86/boot/cpucheck.o
  CC      arch/x86/boot/early_serial_console.o
  CC      arch/x86/boot/edd.o

下一个源码文件是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.harch/x86/boot/compressed/Makefile 可以看出是依赖于目标vmlinux的:

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

目标$(obj)/compressed/vmlinux 依赖于变量vmlinux-objs-y —— 说明需要编译目录arch/x86/boot/compressed 下的源代码,然后生成vmlinux.bin, vmlinux.bin.bz2, 和编译工具 - mkpiggy。我们可以在下面的输出看出来:

  LDS     arch/x86/boot/compressed/vmlinux.lds
  AS      arch/x86/boot/compressed/head_64.o
  CC      arch/x86/boot/compressed/misc.o
  CC      arch/x86/boot/compressed/string.o
  CC      arch/x86/boot/compressed/cmdline.o
  OBJCOPY arch/x86/boot/compressed/vmlinux.bin
  BZIP2   arch/x86/boot/compressed/vmlinux.bin.bz2
  HOSTCC  arch/x86/boot/compressed/mkpiggy

vmlinux.bin 是去掉了调试信息和注释的vmlinux 二进制文件,加上了占用了u32 即4-Byte的长度信息的vmlinux.bin.all 压缩后就是vmlinux.bin.bz2。其中vmlinux.bin.all 包含了vmlinux.binvmlinux.relocsvmlinux 的重定位信息),其中vmlinux.relocsvmlinux 经过程序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.hvoffset.h 已经生成了,arch/x86/boot 里的源文件可以继续编译:

  AS      arch/x86/boot/header.o
  CC      arch/x86/boot/main.o
  CC      arch/x86/boot/mca.o
  CC      arch/x86/boot/memory.o
  CC      arch/x86/boot/pm.o
  AS      arch/x86/boot/pmjump.o
  CC      arch/x86/boot/printf.o
  CC      arch/x86/boot/regs.o
  CC      arch/x86/boot/string.o
  CC      arch/x86/boot/tty.o
  CC      arch/x86/boot/video.o
  CC      arch/x86/boot/video-mode.o
  CC      arch/x86/boot/video-vga.o
  CC      arch/x86/boot/video-vesa.o
  CC      arch/x86/boot/video-bios.o

所有的源代码会被编译,他们最终会被链接到setup.elf

  LD      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

最后,我们编译主机程序arch/x86/boot/tools/build.c,它将会用来把setup.binvmlinux.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.binvmlinux.bin 连接到一起。最终我们会看到输出结果,就和那些用源码编译过内核的同行的结果一样:

Setup is 16268 bytes (padded to 16384 bytes).
System is 4704 kB
CRC 94a88f9a
Kernel: arch/x86/boot/bzImage is ready  (#5)

全部结束。

结论

这就是本文的最后一节。本文我们了解了编译内核的全部步骤:从执行make 命令开始,到最后生成bzImage。我知道linux 内核的makefiles 和构建linux 的过程第一眼看起来可能比较迷惑但是这并不是很难。希望本文可以帮助你理解构建linux 内核的整个流程。

链接


via: https://github.com/0xAX/linux-insides/blob/master/Misc/how_kernel_compiled.md

译者:译者ID 校对:校对者ID

本文由 LCTT 原创翻译,Linux中国 荣誉推出