3.2 内核映像的构建过程

3.2.1 kbuild简介

虽然内核有自己的构建系统kbuild,但是kbuild并不是什么新的东西。我们可以把kbuild看作利用GNU Make组织的一套复杂的构建系统,虽然kbuild也在Make基础上作了适当的扩展,但是因为内核的复杂性,所以kbuild要比一般项目的Makefile的组织要复杂得多。虽然kbuild很复杂,但也是有章可循的,下面两点是理解kbuild的关键。

1.Makefile的包含

Makefile的包含是很多复杂的项目中常用的方法之一。通常的做法是将共同使用的变量或规则定义在一个文件中,在需要使用的Makefile中使用关键字"include"来包含这个文件。kbuild中多处使用了包含的方式,其中关键的两处我们需要特别指出。

(1)顶层Makefile包含平台相关的Makefile

为了Linux能够方便地支持多平台,kbuild必须方便添加对新平台的支持,同时上层的Makefile不需要做大的改动,甚至不需要改动。所以,kbuild将与平台无关的变量、规则等放到了顶层的Makefile中,平台相关的部分定义在各个平台的“顶层”Makefile中。所谓各个平台的“顶层”Makefile,即arch/$(SRCARCH)目录下的Makefile。在顶层的Makefile中包含平台的“顶层”Makefile,脚本如下所示:

3.2 内核映像的构建过程 - 图1

其中变量SRCARCH的值就是平台相关部分所在的目录,对于IA32架构,SRCARCH的值为x86。顶层Makefile包含了平台的“顶层”Makefile后,才组成了真正的Makefile。这也是为什么我们在顶层目录执行"make bzImage"这样的命令时,可以编译内核映像,却在顶层目录下的Makefile中找不到目标bzImage的原因,因为其在平台的“顶层”Makefile中。

(2)Makefile.build包含各个子目录下的Makefile

为了方便Linux开发者能够编写Makefile,kbuild考虑得不可谓不周到,在牺牲自己的同时(kbuild的实现非常烦琐),确实让Linux的开发者们享受了便捷。比如,kbuild将所有与编译过程相关的公共的规则和变量都提取到scripts目录下的Makefile.build中,而具体的子目录下的Makefile文件则可以写得非常简单和直接。

kbuild定义了若干变量,如obj-y、obj-m等,用于记录参与编译过程的文件。这些变量就像钩子或者回调函数,各个子目录Makefile只需为其赋值,设置参与编译的文件即可,其他事情都由Makefile.build处理。甚至最简单的Makefile可以简单到只有一行语句:

3.2 内核映像的构建过程 - 图2

在编译时,Makefile.build会指导make将要编译的子目录下的Makefile文件包含到Makefile.include中动态地组成完整的Makefile文件,脚本如下:

3.2 内核映像的构建过程 - 图3

理论上,要包含Makefile文件,一条include命令就够了,为什么这里实现得如此复杂?

一是因为src的值是相对于顶层目录的,所以在顶层目录执行make没有任何问题。但是如果make不是在顶层目录执行的,那么就需要使用绝对路径来定位编译的子目录了。这就是为什么既然有了src,还要定义变量kbuild-dir。src是kbuild中定义的一个变量,始终指向需要构建的目录,kbuild-dir则是加上了绝对路径的src。make使用内嵌函数filter来判断编译所在的目录的路径是否是以绝对路径表示,即以“/”开头,如果不是,则冠以$(srctree)。srctree记录内核顶层目录的绝对路径。以笔者的环境为例,srctree的值是/vita/build/linux-3.7.4。在一般情况下,构建都发生在顶层目录下,在子目录下构建是为内核开发人员提供的特性。

二是因为子目录下的"Makefile"文件毕竟不是一个真正意义上的Makefile,所以kbuild的设计者的初衷是希望使用Kbuild这个名字。所以,我们看到,在确定kbuild-file时,使用make的内嵌函数wildcard首先尝试匹配子目录下是否存在Kbuild这个文件。如果有则优先使用Kbuild,否则使用Makefile。但是事实上,在内核目录的绝大部分子目录下,人们还是更习惯使用Makefile这个名字。

2.使用指定Makefile的方式进行递归

通常,很多使用make进行构建的项目,当存在多级目录时,使用递归方式构建,例如:

3.2 内核映像的构建过程 - 图4

也就是说,在子目录下构建时,首先要切换当前工作目录到子目录,然后再启动一个make进程解释执行当前目录下的Makefile。在编译一些规模稍大一点的软件时,我们经常看到make不断通过cd命令切换目录,原因就在于此。

但是,kbuid并没有采用切换目录的方式。在kbuild中,make的当前工作目录永远是顶层目录,当编译子目录时,kbuild通过命令行选项-f将子目录的Makefile传递给make,从而达到编译子目录的目的。kbuild使用的典型方式如下:

3.2 内核映像的构建过程 - 图5

其中MAKE是make的内部变量,读者把它理解为make即可。变量build在Makefile.build中定义:

3.2 内核映像的构建过程 - 图6

只有当在子目录进行make时,变量KBUILD_SRC才会被设置为子目录,否则,在顶层目录进行make时,该变量值为空,所以make的内嵌函数if的返回值为else部分。但是因为if函数的else部分为空,所以该函数返回值为空。正如注释中所说,变量build相当于下面这段脚本的简写:

3.2 内核映像的构建过程 - 图7

我们进一步把上面的make命令展开:

3.2 内核映像的构建过程 - 图8

也就说,通过命令行参数-f,指定Makefile为scripts目录下的Makefile.build。而当make解释执行Makefile.build时,再将子目录中的Makefile包含到Makefile.include中来,动态地组成子目录的真正的Makefile。

既然通过指定Makefile的方式编译多级目录,而make又始终工作在顶层目录下,那么必然要在顶层工作目录中跟踪编译所在的子目录。为此,kbuild定义了两个变量:src和obj。其中,src始终指向需要构建的目录;obj指向构建的目标存放的目录。并约定,在引用源码树中业已存在的对象时使用变量src,引用编译时动态生成的对象使用变量obj。kbuild在脚本中小心地维护着这两个变量的值。实际上,因为构建的目标存放的目录与源文件经常在同一个目录下,所以大部分情况下这两个变量均指向同一个目录。

理解了kbuild中这两个变量的意义后,读者一定看明白了上述make命令中参数"obj=<subdir>"的意义,就是设置变量obj的值,记录编译所在的子目录。而在Makefile.build的一开头,变量src的值也被设置为$(obj)。

3.2 内核映像的构建过程 - 图9

3.2 内核映像的构建过程 - 图10

下面,我们就结合构建IA32架构下的内核映像bzImage,探讨内核映像的具体构建过程。