介绍
  我不会告诉你怎么在自己的电脑上去构建、安装一个定制化的 Linux 内核,这样的资料太多了,它们会对你有帮助。本文会告诉你当你在内核源码路径里敲下make 时会发生什么。
  当我刚刚开始学习内核代码时,Makefile 是我打开的第一个文件,这个文件看起来真令人害怕。那时候这个 Makefile 还只包含了1591 行代码,当我开始写本文时,内核已经是4.2.0的第三个候选版本 了。
  这个 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
  这些变量决定了当前内核的版本,并且被使用在很多不同的地方,比如同一个 Makefile 中的 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 的值,以此来决定 quiet 和Q 的值。符号 @ 控制命令的输出,如果它被放在一个命令之前,这条命令的输出将会是 CC scripts/mod/empty.o,而不是Compiling …. scripts/mod/empty.o(LCTT 译注:CC 在 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 的选项 C 和 M:
  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 和其他变量为这个目录,并且将这些变量导出。接着是要获取 SUBARCH 的值,这个变量代表了当前的系统架构(LCTT 译注:一般都指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 之后要设置SRCARCH 和 hfr-arch,SRCARCH 提供了硬件架构相关代码的目录,hfr-arch 提供了相关头文件的目录:
  ifeq ($(ARCH),i386)
  SRCARCH := x86
  endif
  ifeq ($(ARCH),x86_64)
  SRCARCH := x86
  endif
  hdr-arch  := $(SRCARCH)
  注意:ARCH 是 SUBARCH 的别名。如果没有设置过代表内核配置文件路径的变量 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)
  接下来要设置一组和编译内核的编译器相关的变量。我们会设置主机的 C 和 C++ 的编译器及相关配置项:
  HOSTCC       = gcc
  HOSTCXX      = g++
  HOSTCFLAGS   = -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89
  HOSTCXXFLAGS = -O2
  接下来会去适配代表编译器的变量 CC,那为什么还要 HOST* 这些变量呢?这是因为 CC 是编译内核过程中要使用的目标架构的编译器,但是 HOSTCC 是要被用来编译一组 host 程序的(下面我们会看到)。
  然后我们看到变量 KBUILD_MODULES 和 KBUILD_BUILTIN 的定义,这两个变量决定了我们要编译什么东西(内核、模块或者两者):
  KBUILD_MODULES :=
  KBUILD_BUILTIN := 1
  ifeq ($(MAKECMDGOALS),modules)
  KBUILD_BUILTIN := $(if $(CONFIG_MODVERSIONS),1)
  endif
  在这我们可以看到这些变量的定义,并且,如果们仅仅传递了 modules 给 make,变量 KBUILD_BUILTIN 会依赖于内核配置选项 CONFIG_MODVERSIONS。
  下一步操作是引入下面的文件:
  include scripts/Kbuild.include
  文件 Kbuild 或者又叫做 Kernel Build System 是一个用来管理构建内核及其模块的特殊框架。kbuild 文件的语法与 makefile 一样。文件scripts/Kbuild.include 为 kbuild 系统提供了一些常规的定义。因为我们包含了这个 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
  ...
  ...
  ...
  在这些定义好的变量后面,我们又定义了两个变量:USERINCLUDE 和 LINUXINCLUDE。他们包含了头文件的路径(第一个是给用户用的,第二个是给内核用的):
  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_IGNORE 和 RCS_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 代码,他们只是负责根据各种配置文件(make *.config)生成不同目标内核的,因为之前我说了这一部分我们只讨论构建内核的通用途径。
  目标 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 文件里。此时我们还没有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