内核编译方法
介绍
我不会告诉你怎么在自己的电脑上去构建、安装一个定制化的 Linux 内核,这样的资料太多了,它们会对你有帮助。本文会告诉你当你在内核源码路径里敲下 make
时会发生什么。
当我刚刚开始学习内核代码时,Makefile 是我打开的第一个文件,这个文件看起来真令人害怕 :)。那时候这个 Makefile 还只包含了 1591
行代码,当我开始写本文时,内核已经是 4.2.0 的第三个候选版本 了。
这个 Makefile 是 Linux 内核代码的根 Makefile ,内核构建就始于此处。是的,它的内容很多,但是如果你已经读过内核源代码,你就会发现每个包含代码的目录都有一个自己的 Makefile 。当然了,我们不会去描述每个代码文件是怎么编译链接的,所以我们将只会挑选一些通用的例子来说明问题。而你不会在这里找到构建内核的文档、如何整洁内核代码、 tags 的生成和交叉编译 相关的说明,等等。我们将从 make
开始,使用标准的内核配置文件,到生成了内核镜像 bzImage 结束。
如果你已经很了解 make 工具,那是最好,但是我也会描述本文出现的相关代码。
让我们开始吧!
编译内核前的准备
在开始编译前要进行很多准备工作。最主要的就是找到并配置好配置文件,make
命令要使用到的参数都需要从这些配置文件获取。现在就让我们深入内核的根 Makefile
吧。
内核的根 Makefile
负责构建两个主要的文件: vmlinux (内核镜像可执行文件)和模块文件。内核的 Makefile 从定义如下变量开始:
这些变量决定了当前内核的版本,并且被使用在很多不同的地方,比如同一个 Makefile
中的 KERNELVERSION
:
接下来我们会看到很多 ifeq
条件判断语句,它们负责检查传递给 make
的参数。内核的 Makefile
提供了一个特殊的编译选项 make help
,这个选项可以生成所有的可用目标和一些能传给 make
的有效的命令行参数。举个例子,make V=1
会在构建过程中输出详细的编译信息,第一个 ifeq
就是检查传递给 make
的 V=n
选项。
如果 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
输出所有的结果文件:
系统会检查变量 KBUILD_SRC
,它代表内核代码的顶层目录,如果它是空的(第一次执行 makefile 时总是空的),我们会设置变量 KBUILD_OUTPUT
为传递给选项 O
的值(如果这个选项被传进来了)。下一步会检查变量 KBUILD_OUTPUT
,如果已经设置好,那么接下来会做以下几件事:
将变量
KBUILD_OUTPUT
的值保存到临时变量saved-output
;尝试创建给定的输出目录;
检查创建的输出目录,如果失败了就打印错误;
如果成功创建了输出目录,那么就在新目录重新执行
make
命令(参见选项-C
)。
下一个 ifeq
语句会检查传递给 make 的选项 C
和 M
:
第一个选项 C
会告诉 Makefile
需要使用环境变量 $CHECK
提供的工具来检查全部 c
代码,默认情况下会使用 sparse 。第二个选项 M
会用来编译外部模块(本文不做讨论)。
系统还会检查变量 KBUILD_SRC
,如果 KBUILD_SRC
没有被设置,系统会设置变量 srctree
为 .
:
这将会告诉 Makefile
内核的源码树就在执行 make
命令的目录,然后要设置 objtree
和其他变量为这个目录,并且将这些变量导出。接着就是要获取 SUBARCH
的值,这个变量代表了当前的系统架构(LCTT 译注:一般都指CPU 架构):
如你所见,系统执行 uname 得到机器、操作系统和架构的信息。因为我们得到的是 uname
的输出,所以我们需要做一些处理再赋给变量 SUBARCH
。获得 SUBARCH
之后就要设置SRCARCH
和 hfr-arch
,SRCARCH
提供了硬件架构相关代码的目录,hfr-arch
提供了相关头文件的目录:
注意: ARCH
是 SUBARCH
的别名。如果没有设置过代表内核配置文件路径的变量 KCONFIG_CONFIG
,下一步系统会设置它,默认情况下就是 .config
:
以及编译内核过程中要用到的 shell
接下来就要设置一组和编译内核的编译器相关的变量。我们会设置主机的 C
和 C++
的编译器及相关配置项:
接下来会去适配代表编译器的变量 CC
,那为什么还要 HOST*
这些变量呢?这是因为 CC
是编译内核过程中要使用的目标架构的编译器,但是 HOSTCC
是要被用来编译一组 host
程序的(下面我们就会看到)。
然后我们就看到变量 KBUILD_MODULES
和 KBUILD_BUILTIN
的定义,这两个变量决定了我们要编译什么东西(内核、模块或者两者):
在这我们可以看到这些变量的定义,并且,如果们仅仅传递了 modules
给 make
,变量 KBUILD_BUILTIN
会依赖于内核配置选项 CONFIG_MODVERSIONS
。
下一步操作是引入下面的文件:
文件 Kbuild 或者又叫做 Kernel Build System
是一个用来管理构建内核及其模块的特殊框架。Kbuild
文件的语法与 Makefile 一样。文件 scripts/Kbuild.include 为 Kbuild
系统提供了一些常规的定义。因为我们包含了这个 Kbuild
文件,我们可以看到和不同工具关联的这些变量的定义,这些工具会在内核和模块编译过程中被使用(比如链接器、编译器、来自 binutils 的二进制工具包):
在这些定义好的变量后面,我们又定义了两个变量: USERINCLUDE
和 LINUXINCLUDE
。他们包含了头文件的路径(第一个是给用户用的,第二个是给内核用的):
以及给 C 编译器的标准标志:
这并不是最终确定的编译器标志,它们还可以在其他 Makefile 里面更新(比如 arch/
里面的 Kbuild )。变量定义完之后,全部会被导出供其他 Makefile 使用。
下面的两个变量 RCS_FIND_IGNORE
和 RCS_TAR_IGNORE
包含了被版本控制系统忽略的文件:
这就是全部了,我们已经完成了所有的准备工作,下一个点就是如何构建 vmlinux
。
###直面内核构建
现在我们已经完成了所有的准备工作,根 Makefile(注:内核根目录下的 Makefile )的下一步工作就是和编译内核相关的了。在这之前,我们不会在终端看到 make
命令输出的任何东西。但是现在编译的第一步开始了,这里我们需要从内核根 Makefile 的 598 行开始,这里可以看到目标 vmlinux
:
不要操心我们略过的从 export RCS_FIND_IGNORE.....
到 all: vmlinux.....
这一部分 Makefile 代码,他们只是负责根据各种配置文件(make *.config
)生成不同目标内核的,因为之前我就说了这一部分我们只讨论构建内核的通用途径。
目标 all:
是在命令行如果不指定具体目标时默认使用的目标。你可以看到这里包含了架构相关的 Makefile(在这里就指的是 arch/x86/Makefile )。从这一时刻起,我们会从这个 Makefile 继续进行下去。如我们所见,目标 all
依赖于根 Makefile 后面声明的 vmlinux
:
vmlinux
是 linux 内核的静态链接可执行文件格式。脚本 scripts/link-vmlinux.sh 把不同的编译好的子模块链接到一起形成了 vmlinux 。
第二个目标是 vmlinux-deps
,它的定义如下:
它是由内核代码下的每个顶级目录的 built-in.o
组成的。之后我们还会检查内核所有的目录,Kbuild
会编译各个目录下所有的对应 $(obj-y)
的源文件。接着调用 $(LD) -r
把这些文件合并到一个 build-in.o
文件里。此时我们还没有 vmlinux-deps
,所以目标 vmlinux
现在还不会被构建。对我而言 vmlinux-deps
包含下面的文件:
下一个可以被执行的目标如下:
就像我们看到的,vmlinux-dir
依赖于两部分: prepare
和 scripts
。第一个 prepare
定义在内核的根 Makefile
中,准备工作分成三个阶段:
第一个 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
:
第二个目标是 Makefile 里的 archscripts
:
我们可以看到 archscripts
是依赖于根 Makefile 里的 scripts_basic
。首先我们可以看出 scripts_basic
是按照 scripts/basic 的 Makefile 执行 make 的:
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
之后,终端的第一个输出就是 Kbuild
的结果:
当目标 script_basic
被执行,目标 archscripts
就会 make
arch/x86/tools 下的 Makefile 和目标 relocs
:
包含了重定位的信息的代码 relocs_32.c
和 relocs_64.c
将会被编译,这可以在 make
的输出中看到:
在编译完 relocs.c
之后会检查 version.h
:
我们可以在输出看到它:
以及在内核的根 Makefile 使用 arch/x86/include/generated/asm
的目标 asm-generic
来构建 generic
汇编头文件。在目标 asm-generic
之后,archprepare
就完成了,所以目标 prepare0
会接着被执行,如我上面所写:
注意 build
,它是定义在文件 scripts/Kbuild.include ,内容是这样的:
或者在我们的例子中,它就是当前源码目录路径- .
:
脚本 scripts/Makefile.build 通过参数 obj
给定的目录找到 Kbuild
文件,然后引入 Kbuild
文件:
并根据这个构建目标。我们这里 .
包含了生成 kernel/bounds.s
和 arch/x86/kernel/asm-offsets.s
的 Kbuild 文件。在此之后,目标 prepare
就完成了它的工作。 vmlinux-dirs
也依赖于第二个目标 scripts
,它会编译接下来的几个程序: filealias
, mk_elfconfig
, modpost
等等。之后, scripts/host-programs
就可以开始编译我们的目标 vmlinux-dirs
了。
首先,我们先来理解一下 vmlinux-dirs
都包含了那些东西。在我们的例子中它包含了下列内核目录的路径:
我们可以在内核的根 Makefile 里找到 vmlinux-dirs
的定义:
这里我们借助函数 patsubst
和 filter
去掉了每个目录路径里的符号 /
,并且把结果放到 vmlinux-dirs
里。所以我们就有了 vmlinux-dirs
里的目录列表,以及下面的代码:
符号 $@
在这里代表了 vmlinux-dirs
,这就表明程序会递归遍历从 vmlinux-dirs
以及它内部的全部目录(依赖于配置),并且在对应的目录下执行 make
命令。我们可以在输出看到结果:
每个目录下的源代码将会被编译并且链接到 built-io.o
里:
好了,所有的 built-in.o
都构建完了,现在我们回到目标 vmlinux
上。你应该还记得,目标 vmlinux
是在内核的根 Makefile 里。在链接 vmlinux
之前,系统会构建 samples , Documentation 等等,但是如上文所述,我不会在本文描述这些。
你可以看到,调用脚本 scripts/link-vmlinux.sh 的主要目的是把所有的 built-in.o
链接成一个静态可执行文件,和生成 System.map 。最后我们来看看下面的输出:
vmlinux
和 System.map
生成在内核源码树根目录下。
这就是全部了,vmlinux
构建好了,下一步就是创建 bzImage.
制作bzImage
bzImage
就是压缩了的 linux 内核镜像。我们可以在构建了 vmlinux
之后通过执行 make bzImage
获得 bzImage
。同时我们可以仅仅执行 make
而不带任何参数也可以生成 bzImage
,因为它是在 arch/x86/kernel/Makefile 里预定义的、默认生成的镜像:
让我们看看这个目标,它能帮助我们理解这个镜像是怎么构建的。我已经说过了 bzImage
是被定义在 arch/x86/kernel/Makefile ,定义如下:
在这里我们可以看到第一次为 boot 目录执行 make
,在我们的例子里是这样的:
现在的主要目标是编译目录 arch/x86/boot
和 arch/x86/boot/compressed
的代码,构建 setup.bin
和 vmlinux.bin
,最后用这两个文件生成 bzImage
。第一个目标是定义在 arch/x86/boot/Makefile 的 $(obj)/setup.elf
:
我们已经在目录 arch/x86/boot
有了链接脚本 setup.ld
,和扩展到 boot
目录下全部源代码的变量 SETUP_OBJS
。我们可以看看第一个输出:
下一个源码文件是 arch/x86/boot/header.S ,但是我们不能现在就编译它,因为这个目标依赖于下面两个头文件:
第一个头文件 voffset.h
是使用 sed
脚本生成的,包含用 nm
工具从 vmlinux
获取的两个地址:
这两个地址是内核的起始和结束地址。第二个头文件 zoffset.h
在 arch/x86/boot/compressed/Makefile 可以看出是依赖于目标 vmlinux
的:
目标 $(obj)/compressed/vmlinux
依赖于 vmlinux-objs-y
—— 说明需要编译目录 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
生成、然后编译:
这个汇编文件会包含经过计算得来的、压缩内核的偏移信息。处理完这个汇编文件,我们就可以看到 zoffset
生成了:
现在 zoffset.h
和 voffset.h
已经生成了, arch/x86/boot 里的源文件可以继续编译:
所有的源代码会被编译,他们最终会被链接到 setup.elf
:
或者:
最后的两件事是创建包含目录 arch/x86/boot/*
下的编译过的代码的 setup.bin
:
以及从 vmlinux
生成 vmlinux.bin
:
最最后,我们编译主机程序 arch/x86/boot/tools/build.c ,它将会用来把 setup.bin
和 vmlinux.bin
打包成 bzImage
:
实际上 bzImage
就是把 setup.bin
和 vmlinux.bin
连接到一起。最终我们会看到输出结果,就和那些用源码编译过内核的同行的结果一样:
全部结束。
结论
这就是本文的结尾部分。本文我们了解了编译内核的全部步骤:从执行 make
命令开始,到最后生成 bzImage
。我知道,linux 内核的 Makefile 和构建 linux 的过程第一眼看起来可能比较迷惑,但是这并不是很难。希望本文可以帮助你理解构建 linux 内核的整个流程。
链接
最后更新于