你知道 Linux 内核是如何构建的吗?

简介:

介绍

我不会告诉你怎么在自己的电脑上去构建、安装一个定制化的 Linux 内核,这样的资料太多了,它们会对你有帮助。本文会告诉你当你在内核源码路径里敲下make 时会发生什么。

当我刚刚开始学习内核代码时,Makefile 是我打开的第一个文件,这个文件看起来真令人害怕 :)。那时候这个Makefile 还只包含了1591 行代码,当我开始写本文时,内核已经是4.2.0的第三个候选版本 了。

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

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

让我们开始吧!

(题图来自:adafruit.com)

编译内核前的准备

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

内核的根 Makefile 负责构建两个主要的文件:vmlinux (内核镜像可执行文件)和模块文件。内核的Makefile 从定义如下变量开始:


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

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


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

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


 
 
  1. ifeq ("$(origin V)", "command line")
  2. KBUILD_VERBOSE = $(V)
  3. endif
  4. ifndef KBUILD_VERBOSE
  5. KBUILD_VERBOSE = 0
  6. endif
  7. ifeq ($(KBUILD_VERBOSE),1)
  8. quiet =
  9. Q =
  10. else
  11. quiet=quiet_
  12. Q = @
  13. endif
  14. export quiet Q KBUILD_VERBOSE

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

下一个 ifeq 语句检查的是传递给 make 的选项 O=/dir,这个选项允许在指定的目录 dir 输出所有的结果文件:


 
 
  1. ifeq ($(KBUILD_SRC),)
  2. ifeq ("$(origin O)", "command line")
  3. KBUILD_OUTPUT := $(O)
  4. endif
  5. ifneq ($(KBUILD_OUTPUT),)
  6. saved-output := $(KBUILD_OUTPUT)
  7. KBUILD_OUTPUT := $(shell mkdir -p $(KBUILD_OUTPUT) && cd $(KBUILD_OUTPUT) \
  8. && /bin/pwd)
  9. $(if $(KBUILD_OUTPUT),, \
  10. $(error failed to create output directory "$(saved-output)"))
  11. sub-make: FORCE
  12. $(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \
  13. -f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))
  14. skip-makefile := 1
  15. endif # ifneq ($(KBUILD_OUTPUT),)
  16. endif # ifeq ($(KBUILD_SRC),)

系统会检查变量 KBUILD_SRC,它代表内核代码的顶层目录,如果它是空的(第一次执行 makefile 时总是空的),我们会设置变量 KBUILD_OUTPUT 为传递给选项 O 的值(如果这个选项被传进来了)。下一步会检查变量 KBUILD_OUTPUT ,如果已经设置好,那么接下来会做以下几件事:

  • 将变量 KBUILD_OUTPUT 的值保存到临时变量 saved-output
  • 尝试创建给定的输出目录;
  • 检查创建的输出目录,如果失败了就打印错误;
  • 如果成功创建了输出目录,那么就在新目录重新执行 make 命令(参见选项-C)。

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


 
 
  1. ifeq ("$(origin C)", "command line")
  2. KBUILD_CHECKSRC = $(C)
  3. endif
  4. ifndef KBUILD_CHECKSRC
  5. KBUILD_CHECKSRC = 0
  6. endif
  7. ifeq ("$(origin M)", "command line")
  8. KBUILD_EXTMOD := $(M)
  9. endif

第一个选项 C 会告诉 makefile 需要使用环境变量 $CHECK 提供的工具来检查全部 c 代码,默认情况下会使用sparse。第二个选项 M 会用来编译外部模块(本文不做讨论)。

系统还会检查变量 KBUILD_SRC,如果 KBUILD_SRC 没有被设置,系统会设置变量 srctree 为.


 
 
  1. ifeq ($(KBUILD_SRC),)
  2. srctree := .
  3. endif
  4. objtree := .
  5. src := $(srctree)
  6. obj := $(objtree)
  7. export srctree objtree VPATH

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


 
 
  1. SUBARCH := $(shell uname -m | sed -e s/i.86/x86/ -e s/x86_64/x86/ \
  2. -e s/sun4u/sparc64/ \
  3. -e s/arm.*/arm/ -e s/sa110/arm/ \
  4. -e s/s390x/s390/ -e s/parisc64/parisc/ \
  5. -e s/ppc.*/powerpc/ -e s/mips.*/mips/ \
  6. -e s/sh[234].*/sh/ -e s/aarch64.*/arm64/ )

如你所见,系统执行 uname 得到机器、操作系统和架构的信息。因为我们得到的是 uname 的输出,所以我们需要做一些处理再赋给变量 SUBARCH 。获得 SUBARCH 之后就要设置SRCARCH 和 hfr-archSRCARCH 提供了硬件架构相关代码的目录,hfr-arch 提供了相关头文件的目录:


 
 
  1. ifeq ($(ARCH),i386)
  2. SRCARCH := x86
  3. endif
  4. ifeq ($(ARCH),x86_64)
  5. SRCARCH := x86
  6. endif
  7. hdr-arch := $(SRCARCH)

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


 
 
  1. KCONFIG_CONFIG ?= .config
  2. export KCONFIG_CONFIG

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


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

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


 
 
  1. HOSTCC = gcc
  2. HOSTCXX = g++
  3. HOSTCFLAGS = -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89
  4. HOSTCXXFLAGS = -O2

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

然后我们就看到变量 KBUILD_MODULES 和 KBUILD_BUILTIN 的定义,这两个变量决定了我们要编译什么东西(内核、模块或者两者):


 
 
  1. KBUILD_MODULES :=
  2. KBUILD_BUILTIN := 1
  3. ifeq ($(MAKECMDGOALS),modules)
  4. KBUILD_BUILTIN := $(if $(CONFIG_MODVERSIONS),1)
  5. endif

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

下一步操作是引入下面的文件:


 
 
  1. include scripts/Kbuild.include

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


 
 
  1. AS = $(CROSS_COMPILE)as
  2. LD = $(CROSS_COMPILE)ld
  3. CC = $(CROSS_COMPILE)gcc
  4. CPP = $(CC) -E
  5. AR = $(CROSS_COMPILE)ar
  6. NM = $(CROSS_COMPILE)nm
  7. STRIP = $(CROSS_COMPILE)strip
  8. OBJCOPY = $(CROSS_COMPILE)objcopy
  9. OBJDUMP = $(CROSS_COMPILE)objdump
  10. AWK = awk
  11. ...
  12. ...
  13. ...

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


 
 
  1. USERINCLUDE := \
  2. -I$(srctree)/arch/$(hdr-arch)/include/uapi \
  3. -Iarch/$(hdr-arch)/include/generated/uapi \
  4. -I$(srctree)/include/uapi \
  5. -Iinclude/generated/uapi \
  6. -include $(srctree)/include/linux/kconfig.h
  7. LINUXINCLUDE := \
  8. -I$(srctree)/arch/$(hdr-arch)/include \
  9. ...

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


 
 
  1. KBUILD_CFLAGS := -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
  2. -fno-strict-aliasing -fno-common \
  3. -Werror-implicit-function-declaration \
  4. -Wno-format-security \
  5. -std=gnu89

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

下面的两个变量 RCS_FIND_IGNORE 和 RCS_TAR_IGNORE 包含了被版本控制系统忽略的文件:


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

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

直面内核构建

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


 
 
  1. all: vmlinux
  2. include arch/$(SRCARCH)/Makefile

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

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


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

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

第二个目标是 vmlinux-deps,它的定义如下:


 
 
  1. 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 包含下面的文件:


 
 
  1. arch/x86/kernel/vmlinux.lds arch/x86/kernel/head_64.o
  2. arch/x86/kernel/head64.o arch/x86/kernel/head.o
  3. init/built-in.o usr/built-in.o
  4. arch/x86/built-in.o kernel/built-in.o
  5. mm/built-in.o fs/built-in.o
  6. ipc/built-in.o security/built-in.o
  7. crypto/built-in.o block/built-in.o
  8. lib/lib.a arch/x86/lib/lib.a
  9. lib/built-in.o arch/x86/lib/built-in.o
  10. drivers/built-in.o sound/built-in.o
  11. firmware/built-in.o arch/x86/pci/built-in.o
  12. arch/x86/power/built-in.o arch/x86/video/built-in.o
  13. net/built-in.o

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


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

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


 
 
  1. prepare: prepare0
  2. prepare0: archprepare FORCE
  3. $(Q)$(MAKE) $(build)=.
  4. archprepare: archheaders archscripts prepare1 scripts_basic
  5. prepare1: prepare2 $(version_h) include/generated/utsrelease.h \
  6. include/config/auto.conf
  7. $(cmd_crmodverdir)
  8. prepare2: prepare3 outputmakefile asm-generic

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

第一个目标是 makefile 生成的系统调用列表(syscall table)中的 archheaders :


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

第二个目标是 makefile 里的 archscripts


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

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


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

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


 
 
  1. hostprogs-y := fixdep
  2. hostprogs-$(CONFIG_BUILD_BIN2C) += bin2c
  3. always := $(hostprogs-y)
  4. $(addprefix $(obj)/,$(filter-out fixdep,$(always))): $(obj)/fixdep

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

执行 make 之后,终端的第一个输出就是 kbuild 的结果:


 
 
  1. $ make
  2. HOSTCC scripts/basic/fixdep

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


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

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


 
 
  1. HOSTCC arch/x86/tools/relocs_32.o
  2. HOSTCC arch/x86/tools/relocs_64.o
  3. HOSTCC arch/x86/tools/relocs_common.o
  4. HOSTLD arch/x86/tools/relocs

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


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

我们可以在输出看到它:


 
 
  1. CHK include/config/kernel.release

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


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

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


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

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


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

脚本 scripts/Makefile.build 通过参数 obj 给定的目录找到 Kbuild 文件,然后引入 kbuild 文件:


 
 
  1. include $(kbuild-file)

并根据这个构建目标。我们这里 . 包含了生成 kernel/bounds.s 和 arch/x86/kernel/asm-offsets.s 的Kbuild 文件。在此之后,目标 prepare 就完成了它的工作。 vmlinux-dirs 也依赖于第二个目标 scripts,它会编译接下来的几个程序:filealiasmk_elfconfigmodpost 等等。之后,scripts/host-programs 就可以开始编译我们的目标 vmlinux-dirs 了。

首先,我们先来理解一下 vmlinux-dirs 都包含了那些东西。在我们的例子中它包含了下列内核目录的路径:


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

我们可以在内核的根 Makefile 里找到 vmlinux-dirs 的定义:


 
 
  1. vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
  2. $(core-y) $(core-m) $(drivers-y) $(drivers-m) \
  3. $(net-y) $(net-m) $(libs-y) $(libs-m)))
  4. init-y := init/
  5. drivers-y := drivers/ sound/ firmware/
  6. net-y := net/
  7. libs-y := lib/
  8. ...
  9. ...
  10. ...

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


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

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


 
 
  1. CC init/main.o
  2. CHK include/generated/compile.h
  3. CC init/version.o
  4. CC init/do_mounts.o
  5. ...
  6. CC arch/x86/crypto/glue_helper.o
  7. AS arch/x86/crypto/aes-x86_64-asm_64.o
  8. CC arch/x86/crypto/aes_glue.o
  9. ...
  10. AS arch/x86/entry/entry_64.o
  11. AS arch/x86/entry/thunk_64.o
  12. CC arch/x86/entry/syscall_64.o

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


 
 
  1. $ find . -name built-in.o
  2. ./arch/x86/crypto/built-in.o
  3. ./arch/x86/crypto/sha-mb/built-in.o
  4. ./arch/x86/net/built-in.o
  5. ./init/built-in.o
  6. ./usr/built-in.o
  7. ...
  8. ...

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


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

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


 
 
  1. LINK vmlinux
  2. LD vmlinux.o
  3. MODPOST vmlinux.o
  4. GEN .version
  5. CHK include/generated/compile.h
  6. UPD include/generated/compile.h
  7. CC init/version.o
  8. LD init/built-in.o
  9. KSYM .tmp_kallsyms1.o
  10. KSYM .tmp_kallsyms2.o
  11. LD vmlinux
  12. SORTEX vmlinux
  13. SYSMAP System.map

vmlinux 和System.map 生成在内核源码树根目录下。


 
 
  1. $ ls vmlinux System.map
  2. System.map vmlinux

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

制作bzImage

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


 
 
  1. all: bzImage

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


 
 
  1. bzImage: vmlinux
  2. $(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE)
  3. $(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot
  4. $(Q)ln -fsn ../../x86/boot/bzImage $(objtree)/arch/$(UTS_MACHINE)/boot/$@

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


 
 
  1. boot := arch/x86/boot

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


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

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


 
 
  1. AS arch/x86/boot/bioscall.o
  2. CC arch/x86/boot/cmdline.o
  3. AS arch/x86/boot/copy.o
  4. HOSTCC arch/x86/boot/mkcpustr
  5. CPUSTR arch/x86/boot/cpustr.h
  6. CC arch/x86/boot/cpu.o
  7. CC arch/x86/boot/cpuflags.o
  8. CC arch/x86/boot/cpucheck.o
  9. CC arch/x86/boot/early_serial_console.o
  10. CC arch/x86/boot/edd.o

下一个源码文件是 arch/x86/boot/header.S,但是我们不能现在就编译它,因为这个目标依赖于下面两个头文件:


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

第一个头文件 voffset.h 是使用 sed 脚本生成的,包含用 nm 工具从 vmlinux 获取的两个地址:


 
 
  1. #define VO__end 0xffffffff82ab0000
  2. #define VO__text 0xffffffff81000000

这两个地址是内核的起始和结束地址。第二个头文件 zoffset.h 在 arch/x86/boot/compressed/Makefile可以看出是依赖于目标 vmlinux的:


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

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


 
 
  1. LDS arch/x86/boot/compressed/vmlinux.lds
  2. AS arch/x86/boot/compressed/head_64.o
  3. CC arch/x86/boot/compressed/misc.o
  4. CC arch/x86/boot/compressed/string.o
  5. CC arch/x86/boot/compressed/cmdline.o
  6. OBJCOPY arch/x86/boot/compressed/vmlinux.bin
  7. BZIP2 arch/x86/boot/compressed/vmlinux.bin.bz2
  8. HOSTCC arch/x86/boot/compressed/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 生成、然后编译:


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

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


 
 
  1. ZOFFSET arch/x86/boot/zoffset.h

现在 zoffset.h 和 voffset.h 已经生成了,arch/x86/boot 里的源文件可以继续编译:


 
 
  1. AS arch/x86/boot/header.o
  2. CC arch/x86/boot/main.o
  3. CC arch/x86/boot/mca.o
  4. CC arch/x86/boot/memory.o
  5. CC arch/x86/boot/pm.o
  6. AS arch/x86/boot/pmjump.o
  7. CC arch/x86/boot/printf.o
  8. CC arch/x86/boot/regs.o
  9. CC arch/x86/boot/string.o
  10. CC arch/x86/boot/tty.o
  11. CC arch/x86/boot/video.o
  12. CC arch/x86/boot/video-mode.o
  13. CC arch/x86/boot/video-vga.o
  14. CC arch/x86/boot/video-vesa.o
  15. CC arch/x86/boot/video-bios.o

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


 
 
  1. LD arch/x86/boot/setup.elf

或者:


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


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

以及从 vmlinux 生成 vmlinux.bin :


 
 
  1. 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.bin 和 vmlinux.bin 打包成 bzImage:


 
 
  1. 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 连接到一起。最终我们会看到输出结果,就和那些用源码编译过内核的同行的结果一样:


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

全部结束。

结论

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


本文来自云栖社区合作伙伴“Linux中国”,原文发布日期:2015-09-11

目录
相关文章
|
24天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制
本文深入探讨了Linux操作系统中用于管理多线程和进程的并发控制的关键技术,包括原子操作、锁机制、自旋锁、互斥量以及信号量。通过详细分析这些技术的原理和应用,旨在为读者提供一个关于如何有效利用Linux内核提供的并发控制工具以优化系统性能和稳定性的综合视角。
|
23天前
|
缓存 负载均衡 算法
深入探索Linux内核的调度机制
本文旨在揭示Linux操作系统核心的心脏——进程调度机制。我们将从Linux内核的架构出发,深入剖析其调度策略、算法以及它们如何共同作用于系统性能优化和资源管理。不同于常规摘要提供文章概览的方式,本摘要将直接带领读者进入Linux调度机制的世界,通过对其工作原理的解析,展现这一复杂系统的精妙设计与实现。
66 8
|
24天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
64 4
|
6天前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
6天前
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
7天前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
7天前
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####
|
9天前
|
负载均衡 算法 Linux
深入探索Linux内核调度机制:公平与效率的平衡####
本文旨在剖析Linux操作系统内核中的进程调度机制,特别是其如何通过CFS(完全公平调度器)算法实现多任务环境下资源分配的公平性与系统响应速度之间的微妙平衡。不同于传统摘要的概览性质,本文摘要将直接聚焦于CFS的核心原理、设计目标及面临的挑战,为读者揭开Linux高效调度的秘密。 ####
26 3
|
12天前
|
负载均衡 算法 Linux
深入探索Linux内核调度器:公平与效率的平衡####
本文通过剖析Linux内核调度器的工作机制,揭示了其在多任务处理环境中如何实现时间片轮转、优先级调整及完全公平调度算法(CFS),以达到既公平又高效地分配CPU资源的目标。通过对比FIFO和RR等传统调度策略,本文展示了Linux调度器如何在复杂的计算场景下优化性能,为系统设计师和开发者提供了宝贵的设计思路。 ####
28 6
|
11天前
|
消息中间件 安全 Linux
深入探索Linux操作系统的内核机制
本文旨在为读者提供一个关于Linux操作系统内核机制的全面解析。通过探讨Linux内核的设计哲学、核心组件、以及其如何高效地管理硬件资源和系统操作,本文揭示了Linux之所以成为众多开发者和组织首选操作系统的原因。不同于常规摘要,此处我们不涉及具体代码或技术细节,而是从宏观的角度审视Linux内核的架构和功能,为对Linux感兴趣的读者提供一个高层次的理解框架。