配置与编译内核用到的工具很多,在这里只对几个关键工具进行介绍,更多的内容请参考相关手册。
Make Make是一种帮助大型软件工程的编译工作实现自动化的编程语言。正确地使用Make可以大大减少因编译程序而花费的时间,因为它可以消除不必要的再编译。Make的基本设计思想是如果目标文件是在最近一次对源文件的修改之后编译的,它就是“新的”,不需要重新编译;如果最近一次对源文件的修改之后没有及时更新目标文件,那么该目标文件就是“旧的”,需要重新编译。为了理解Make如何执行一个任务,需要了解一些术语:
◆目标 需要执行的一个任务。多数情况下它就是用户要生成的文件的名字,但是它也可以仅是个任务的名字。
◆依赖关系 两个目标之间相互依存的关系。如果修改目标B会造成目标A的修改,那么就说目标A依赖于目标B,B是A的先决条件。
◆变量 一种存储临时信息的载体。Make中使用的变量应该加上括号,例如$(TEMP)。
◆命令 执行任务时使用的指令,可以是一条、多条,甚至没有。
◆规则 一条完整的规则具有以下格式:
目标(target) : 先决条件(prerequisites)
规则(command)
......
其中只有目标必须要有,其它成分可以没有。一条完整的规则描述了编译一个目标的方法和依赖关系,是Makefile中最重要的部分。
◆Makefile文件 描述如何生成一个或多个目标的文件。它列出目标依赖的各个文件,并提供正确编译这些目标所需要的规则。
接下来以2.4.23的kbuild为例,简要介绍一下内核的构建过程。首先,完整的内核构建过程由以下五种Makefile封装。
1.根目录Makefile
它是最重要的Makefile,定义所有与体系结构无关的变量和目标。它读取.config文件,并根据其信息最终生成vmlinux和modules。Make通过向下递归调用子目录中的Makefile来编译这两个目标。
2.配置文件.config
执行“make ”会在根目录下生成该配置文件,其内容记录了具体的配置选择,也可以将旧内核的配置文件放在这里。
3.arch/*/Makefile
这是与特定体系结构相关的Makefile。它包含在根目录下的Makefile中,为kbuild提供体系结构的特定信息。
4.子目录Makefiles
它们存在于每个子目录下,大约有几百个。它们接受来自上层Make传递下来的信息,并根据这些信息来构造一个需要编译的文件列表,并交由Rules.make处理。
5.Rules.make
几乎每个子目录Makefile都包含该Makefile。根据子目录Makefiles构建的文件列表,Make使用Rules.make定义的通用规则来编译所有来自列表的源文件。
kbuild的执行过程是:Make从根目录Makefile开始执行,从中获得与体系结构无关的变量和依赖关系,并同时从arch/*/Makefile中获得体系特定的变量等信息,这些信息扩展了根目录Makefile提供的变量。此时kbuild已经拥有构建内核需要的所有变量和目标。然后,Make进入子目录,把部分变量传递给子目录Makefile。子目录Makefile根据配置信息决定编译哪些源文件,从而构建出一个需要编译的文件列表。最后,Rules.make根据其定义的编译规则决定这些文件的编译方式。
需要注意的是,由于Make的向下递归特性和无序性,其执行过程并不完全遵守顺序逐行执行的规则,但无论Make的执行有多复杂,也只分为两个阶段。第一个阶段Make会读取所有变量和分析所有目标的依赖关系,并最终建立一棵依赖关系树。同时,所有的立即型变量(通过“:=”赋值)在这个过程中被扩展,就像C变量一样。而在这个阶段的最后,所有的延迟型变量才被扩展(通过“=”赋值)。这点需要格外注意。第二个阶段Make会根据依赖关系树执行命令。
因此,一个目标和其先决条件的规则定义的顺序是无所谓的,很可能一个目标的先决条件的规则定义在百行以后才出现。Make会耐心读完所有的Makefile后分析得出依赖关系树。
GCC GCC是GNU的免费编译程序,也是内核惟一指定使用的编译器。GCC在执行一个完整的编译任务时会经过以下步骤:
◆预处理 GCC会调用cpp程序来分析各种宏指令,如#define、#if、#include等。
◆编译 这一阶段根据输入文件产生汇编语言指令。由于通常情况下是立即调用汇编程序as,所以输出一般不保存在文件中,可以使用-S选项强制输出源程序的汇编版本。
◆汇编 这一阶段将汇编语言源程序作为输入,生成.o目标文件。
◆链接 这是最后一个阶段。该阶段中,各个.o模块被链接在一起构成可执行文件。
as
用户可以明确地要求使用as来直接处理汇编文件。as产生的目标文件可以分为文本段(.text)、数据段(.data)和未初始化数据段(.bss)。
ld
与as相似,用户可以明确地要求使用ld链接程序将几个模块组合成一个单独的可执行文件。其链接过程通常由一个叫ld链接脚本的文件来描述。该脚本使用Linker Command Language编写。使用“ld --verose”命令可以看到这个默认使用的ld链接脚本。
ar
ar是GNU的二进制文件处理程序,用于创建、修改及从归档文件中抽取文件。由它生成的.a归档文件实际上是一个包含许多可执行二进制代码子程序集合的库文件。
RPMBuild
使用“make rpm”可以把内核源代码制作成RPM包。在此之前,kbuild会执行“make spec”生成rpmbuild程序用到的spec文件,详见“man rpmbuild”。
中间件
根目录scripts下的各种脚本和C源文件都可以称作中间件。它们并不是内核组件的一部分,只是在kbuild执行过程中的辅助程序。以split-include为例,讲述配置文件的运作机理。
.config由关键字/值对组成,其内容类似于:
CONFIG_MPENTIUMIII=y
# CONFIG_MPENTIUM4 is not set
CONFIG_REISERFS_FS=m
这些信息在执行“make ”时自动生成。同时include/linux/autoconf.h依照.config的内容生成。它的格式类似于:
#define CONFIG_MPENTIUMIII 1
#undef CONFIG_MPENTIUM4
#undef CONFIG_REISERFS_FS
#define CONFIG_REISERFS_FS_MODULE 1
对比一下不难发现,include/linux/autoconf.h明确地洞悉了.config的意图:哪些组件不编译,哪些需要编译进内核,而哪些又要作为模块来编译?split-include根据include/linux/autoconf.h在include/config/下建立相关的目录和.h文件。每个.h文件只包括include/linux/autoconf.h中的某一行,比如在配置内核选项时支持NTFS文件系统,并把它编译进内核,在.config中就会生成“CONFIG_NTFS_FS=y”,相应地在include/linux/autoconf.h中会生成“#define CONFIG_NTFS_FS 1”一项。这样,所有与NTFS文件系统相关的C源文件都会包含include/config/ntfs/fs.h头文件。
如果以前编译过内核,并且没有使用过“make mrproper”,.config、include/linux/autoconf.h和include/linux/config/就不会被删除。这里涉及到新旧内核的配置问题。一个全新的内核代码是未经配置的。如果只在原内核的功能基础上增加对NTFS的支持,那么从头开始配置无疑是浪费时间。可以继续使用原内核的.config文件,而所有的配置信息不会有任何更改,并且可以直接在原配置的基础上增加新功能。
在复杂的情况下,保留的旧内核配置信息还要与新的配置信息进行比较:哪些旧信息需要覆盖,哪些需要保留?下面来看一下几种可能的情况:
旧值保存在include/config/下的.h文件中,新值保存在新生成的include/linux/autoconf.h文件中。split-include的代码不仅描述了如何处理这五种情况,还描述了include/config/下文件和子目录的生成过程。