5. 构建临时编译环境
5.1. 简介
本章介绍如何编译和安装一个小的 Linux 系统。这个系统将仅包含必要的工具,用于构建第六章中最终的 LFS 系统。
构建这个小系统分两步进行,第一步是构建一个新的不依赖于宿主系统的工具链(编译器、汇编器、连接器、库文件以及一些有用的软件),第二个步骤是利用这个工具链去构建其它基本的工具。
本章中编译的文件将安装在 $LFS/tools 目录下,这样可以与下一章将要安装的软件以及宿主系统区分开来。这些软件包编译出来是起临时作用的,我们不希望这些软件和即将建立的 LFS 系统混杂在一起。
重要
在运行每一个软件包的编译指令之前,都需要用 lfs 用户解开这个软件包,并用 cd 命令进入软件包解开后的目录。编译指令假定您使用的是 bash shell 。
[译者注]举例来说,对于第一个软件包 Binutils-2.16.1 ,在执行书上的第一个命令
mkdir -v ../binutils-build
之前,必须先执行下列解包和切换目录的命令:
tar -xvjf $LFS/sources/binutils-2.16.1.tar.bz2 -C $LFS/sources/
cd $LFS/sources/binutils-2.16.1/
其他软件包以此类推。
某些软件包在编译之前需要打上补丁,但仅仅是需要补丁来解决某个问题的时候。一个补丁可能本章和下一章都会用到,也可能只在其中一章用到。因此,当某个软件包存在一个补丁,而在编译时并未让你使用这个补丁时,不要以为是我们忘记了。事实上,那个补丁只需要在另外一次编译中被用到。而应用某个补丁的时候,也可能会出现某些关于 offset 或 fuzz 的警告信息,不要理会,这个补丁仍然会被成功的应用。
大多数软件包的编译过程中,屏幕上可能会滚过一些警告信息,这些是正常且可以被安全忽略的。这些警告就像显示的那样:警告的是不标准,却不是不正确的 C 或 C++ 语法。C 标准常常会变,而某些软件包仍然使用老的标准,这不是问题,仅仅导致一些警告信息而已。
重要
在安装完每个软件包之后,删除其源代码和编译目录,除非另有特别说明。删除源文件可以节省磁盘空间,并且可以在下次需要安装同一个软件包的时候不会出现配置错误。
[译者注]举例来说,对于第一个软件包 Binutils-2.16.1 ,在执行完书上的最后一个命令
cp -v ld/ld-new /tools/bin
之后,可以使用下列命令删除其源文件和编译目录:
rm -fr $LFS/sources/binutils-2.16.1/
rm -fr $LFS/sources/binutils-build/
其他的软件包以此类推。
最后一次检查 LFS 环境变量是否设置正确:
echo $LFS
请确认输出显示的是挂载 LFS 分区的路径,在我们的例子中是 /mnt/lfs 。
5.2. 工具链技术说明
本节阐述了整个构建方法的一些基本原理和技术细节,您不必马上就理解本节中的所有内容。在您实际操作之后,就会了解大多数的东西,您可以在任何时候回顾本节。
第五章的总体目标是提供一个临时环境,您可以 chroot 到这个环境,在里面构建一个第六章中的干净、没有问题的目标 LFS 系统。为了尽量的与宿主系统分开,我们创建了一个自包含、自依赖的工具链。要注意的是,这个创建过程被设计为尽量减少新手犯错误的可能,同时尽可能多的提供教育价值。
重要
在继续之前,要先知道工作平台的名称,就是所谓的"target triplet"(目标三元组),许多时候,target triplet 可能是 i686-pc-linux-gnu 。要确定 target triplet 的名称,有一个简单的方法就是运行许多源码包里都有的 config.guess 脚本。解开 Binutils 的源码包,然后运行脚本 ./config.guess 并注意输出的内容。
工作平台的动态连接器名称也需要知道,这里指的是动态加载器(不要与 Binutils 里的标准连接器 ld 混淆了)。动态连接器由 Glibc 提供,用来找到并加载一个程序运行时所需的共享库,在做好程序运行的准备之后,运行这个程序。动态连接器的名称通常是 ld-linux.so.2 ,在不怎么流行的平台上则可能是 ld.so.1 ,而在新的 64 位平台上更可能是别的完全不同的名称。查看宿主系统的 /lib 目录可以确定动态连接器的名称。确定这个名称还有一个必杀技,就是在宿主系统上随便找一个二进制文件,运行 readelf -l <二进制文件名> | grep interpreter 并查看输出的内容。涵盖所有平台的权威参考请查看 Glibc 源码根目录里的 shlib-versions 文件。
第五章中构建方法是如何工作的一些技术要点:
· 这个过程在原理上与交叉编译类似,通过把工具安装在同一个目录(使用相同的"prefix")中以便协同工作,还利用了一点 GNU 的"魔法"。
· 小心处理标准连接器的库文件搜索路径,确保程序仅连接到指定的库上。
· 小心处理 gcc 的 specs 文件,告诉编译器要使用哪个动态连接器。
首先安装的是 Binutils ,因为 GCC 和 Glibc 的 configure 脚本要在汇编器和连接器上执行各种各样的特性测试,以确定软件的哪些功能要启用,哪些功能要禁用。这样做比你想像的还要重要,配置不正确的 GCC 或者 Glibc 会导致工具链出现微妙的错误,这样的错误造成的影响可能直到整个系统快要编译完成的时候才显现出来。测试程序通常会在其它的许多工作进行之前给出错误警告(以避免其后的无效劳动)。
Binutils 的汇编器和连接器安装在两个位置:/tools/bin 和 /tools/$TARGET_TRIPLET/bin ,一个位置的程序是另外一个位置的硬链接。连接器的一个重要方面是它的库搜索顺序,将 --verbose 选项传递给 ld 可以获得详细的信息。例如,输入命令:ld --verbose | grep SEARCH 将显示当前搜索路径和顺序。要显示 ld 连接的是哪些文件,可以编译一个伪(dummy)程序并把 --verbose 选项传递给连接器。举个例子,输入 gcc dummy.c -Wl,--verbose 2>&1 | grep succeeded 将显示所有连接成功的文件。
第二个安装的软件包是 GCC 。下面是运行 configure 脚本时,输出内容的一个示例:
checking what assembler to use...
/tools/i686-pc-linux-gnu/bin/as
checking what linker to use... /tools/i686-pc-linux-gnu/bin/ld
基于上面提到过的原因,这是重要的步骤,它同时证明了 GCC 的配置脚本并不是搜索 PATH 里的目录来寻找要使用哪个工具的,而且,在 gcc 的实际操作中,相同的搜索路径不一定会被使用。要知道 gcc 会使用哪个标准连接器,请运行 gcc -print-prog-name=ld 命令。
在编译一个伪程序的时候,给 gcc 命令传递 -v 选项可以获得详细的信息。举个例子:gcc -v dummy.c 将显示在预处理、编译和汇编各个阶段的详细信息,包括 gcc 文件包含的搜索路径及其顺序。
接下来安装的软件包是 Glibc 。编译 Glibc 的时候,最需要注意的地方是编译器、二进制工具(Binutils)和内核头文件。编译器一般不是问题,因为 Glibc 总是使用在 PATH 目录里找到的 gcc 。二进制工具和内核头文件则有点复杂,因此,为慎重起见,明确使用配置开关(选项)来强制进行正确的选择。在运行 configure 之后,请检查 config.make 文件的内容(位于 glibc-build 目录下),查看所有重要的细节。注意,CC="gcc -B/tools/bin/"的作用是控制要使用哪个二进制工具;而 -nostdinc 和 -isystem 选项则是控制编译器的文件包含搜索路径。这些选项表明了 Glibc 软件包的一个重要特征:根据其编译方法,它是非常自给自足的,而且一般不依赖于工具链的默认值。
装完 Glibc 之后,需要做一些调整使得只在 /tools 目录里搜索和连接。安装一个调整好的 ld ,将它的固化搜索路径限制在 /tools/lib 目录。然后修改 gcc 的 specs 文件以指向 /tools/lib 目录里新的动态连接器。最后这一步在整个过程中至关重要,像上面提到的,指向动态连接器的固化路径被嵌入到每个 ELF 可执行文件里。可以通过 readelf -l <二进制文件名> | grep interpreter 命令来检查。修改 gcc 的 specs 文件以确保本章后面编译的每一个程序都使用位于 /tools/lib 目录里新的动态连接器。
需要使用新动态连接器也是第二遍编译 GCC 需要打 Specs 补丁的原因。不这样做的结果是 GCC 会把宿主系统 /lib 目录下动态连接器的名字嵌入进来,这样有悖于与宿主系统隔离的目标。
第二遍编译 Binutils 的过程中,我们利用 --with-lib-path 选项来控制 ld 的库搜索路径。如前面所指出的,核心工具链是自包含和自依赖的,所以第五章余下的软件包的编译将依赖于 /tools 下新的 Glibc 。
在第六章进入虚根环境后,第一个安装的主要软件包就是 Glibc ,原因是上面所提到的 Glibc 自给自足的特性。一旦 Glibc 安装到 /usr 目录后,马上改变工具链的默认值,然后构建目标 LFS 系统的其它部分。
5.3. Binutils-2.16.1 - 第一遍
Binutils 是一组开发工具,包括连接器、汇编器和其他用于目标文件和档案的工具。
预计编译时间: 1 SBU
所需磁盘空间: 189 MB
5.3.1. 安装 Binutils
首先安装的第一个软件包是 Binutils ,这非常重要,因为 Glibc 和 GCC 会针对可用的连接器和汇编器进行多种测试,以决定是否打开某些特性。
Binutils 的文档推荐用一个新建的目录来编译它,而不是在源码目录中:
mkdir -v ../binutils-build
cd ../binutils-build
注意
如果你想使用本书余下部份列出的 SBU 值,那么现在就要测量一下编译本软件包的时间。你可以用类似下面这样的 time 命令:time { ./configure ... && make && make install; } 。
为编译 Binutils 做准备:
../binutils-2.16.1/configure --prefix=/tools --disable-nls
配置选项的含义:
--prefix=/tools
这个参数告诉 configure 脚本,应该把 Binutils 软件包中的程序安装到 /tools 目录中。
--disable-nls
这个参数禁止了国际化(通常简称i18n),静态程序不需要国际化的特性。
接下来编译它:
make
现在编译完成了。通常我们会运行测试套件,但是目前测试套件(Tcl, Expect, DejaGNU)尚未安装。而且在这里运行测试也没什么用处,因为第一遍安装的程序很快就会被第二遍的程序所覆盖。
安装软件包:
make install
接下来为后面"调整工具链"步骤准备连接器:
make -C ld clean
make -C ld LIB_PATH=/tools/lib
cp -v ld/ld-new /tools/bin
make 参数的含义:
-C ld clean
告诉 make 程序删除所有 ld 子目录中编译生成的文件。
-C ld LIB_PATH=/tools/lib
这个选项重新编译 ld 子目录中的所有文件。在命令行中指定 Makefile 的 LIB_PATH 变量值,使它明确指向临时工具目录,以覆盖默认值。这个变量的值指定了连接器的默认库搜索路径,它在这一章的稍后部分会用到。
关于这个软件包的详细资料位于节 6.11.2, Binutils 的内容
5.4. GCC-4.0.3 - 第一遍
GCC 软件包包含 GNU 编译器集合,其中有 C 和 C++ 编译器。
预计编译时间: 8.2 SBU
所需磁盘空间: 514 MB
5.4.1. 安装 GCC
GCC 的安装指南推荐用一个新建的目录来编译它,而不是在源码目录中:
mkdir -v ../gcc-build
cd ../gcc-build
为编译 GCC 做准备:
../gcc-4.0.3/configure --prefix=/tools \
--with-local-prefix=/tools --disable-nls --enable-shared \
--enable-languages=c
配置选项的含义:
--with-local-prefix=/tools
这个参数的目的是把 /usr/local/include 目录从 gcc 的 include 搜索路径里删除。这并不是绝对必要,但我们想尽量减小宿主系统的影响,所以才这样做。
--enable-shared
这个参数咋一看有点违反直觉。但只有加上它,才能编译出 libgcc_s.so.1 和 libgcc_eh.a 。Glibc(下一个软件包)的配置脚本只有在找到 libgcc_eh.a 时才能确保产生正确的结果。
--enable-languages=c
只编译 GCC 软件包中的 C 编译器。我们在本章里不需要其它编译器。
接下来编译它:
make bootstrap
make 参数的含义:
bootstrap
使用这个参数的目的不仅仅是编译 GCC ,而是重复编译它几次。它用第一次编译生成的程序来第二次编译自己,然后又用第二次编译生成的程序来第三次编译自己,最后比较第二次和第三次编译的结果,以确保编译器能毫无差错的编译自身,这通常表明编译是正确的。
编译现在完成了,通常我们会在这里运行测试套件,但是正如前面说过的,测试套件目前尚未安装,而且在这里运行测试没什么用处,因为第一遍安装的程序很快就会被第二遍的程序所覆盖。
安装软件包:
make install
最后,我们创建一个必要的符号连接。因为许多程序和脚本试图运行 cc 而不是 gcc ,这样做是为了让程序能在多种 Unix 平台上运行,并保持一致性。并不是每个人都安装 GNU C 编译器。只运行 cc 而不是 gcc 可以把选择 C 编译器的自由留给系统管理员,我们这里将指向 gcc :
ln -vs gcc /tools/bin/cc
关于这个软件包的详细资料位于节 6.12.2, GCC 的内容
5.5. Linux-Libc-Headers-2.6.12.0
Linux-Libc-Headers 包含了"纯净的"内核头文件。
预计编译时间: 少于 0.1 SBU
所需磁盘空间: 27 MB
5.5.1. 安装 Linux-Libc-Headers
多年来的公共惯例是使用 /usr/include 目录下"原始的"内核头文件(直接来自于内核源码包),但是近年来,内核开发者强烈要求不要这样做,因此诞生了 Linux-Libc-Headers 项目,其目标是维护一个API(应用程序编程接口)版本稳定的 Linux 头文件。关于内核头文件变迁的历史,这里有一篇《[八卦故事]内核头文件传奇》,可以当作课外读物 :)
安装这些头文件:
cp -Rv include/asm-i386 /tools/include/asm
cp -Rv include/linux /tools/include
如果您的机器不是 i386 兼容架构的,请相应的调整第一条命令。
关于这个软件包的详细资料位于节 6.7.2, Linux-Libc-Headers 的内容
5.6. Glibc-2.3.6
Glibc 包含了主要的C库。这个库提供了基本例程,用于分配内存、搜索目录、打开关闭文件、读写文件、字串处理、模式匹配、数学计算等等。
预计编译时间: 6 SBU
所需磁盘空间: 325 MB
5.6.1. 安装 Glibc
Glibc 文档推荐在源码目录之外的一个专门的编译目录下进行编译:
mkdir -v ../glibc-build
cd ../glibc-build
接下来为编译 Glibc 做准备:
../glibc-2.3.6/configure --prefix=/tools \
--disable-profile --enable-add-ons \
--enable-kernel=2.6.0 --with-binutils=/tools/bin \
--without-gd --with-headers=/tools/include \
--without-selinux
配置选项的含义:
--disable-profile
它关掉了 profiling 信息相关的库文件编译。如果你打算做 profiling ,就省掉这个参数。
--enable-add-ons
这个指示 Glibc 使用附加的 NPTL 包作为线程库。
--enable-kernel=2.6.0
这个告诉 Glibc 编译支持 2.6.x 内核的库。
--with-binutils=/tools/bin
这个参数并不是必需的。但它们能保证在编译 Glibc 时不会用错 Binutils 程序。
--without-gd
这个参数保证不生成 memusagestat 程序,这个程序会顽固地连接到宿主系统的库文件(libgd, libpng, libz 等等)。
--with-headers=/tools/include
这个参数指示 Glibc 按照前面刚刚安装到 tools 目录中的内核头文件编译自己,从而精确的知道内核的特性以根据这些特性对自己进行最佳化编译。
--without-selinux
当从一个含有 SELinux 特性的宿主系统(如 Fedora Core 3)编译时,Glibc 将会将 SELinux 支持编译进来。由于 LFS 工具链并不包含 SELinux 支持,所以一个含有 SELinux 特性的 Glibc 将会导致许多操作失败。所以这里明确禁用它。
在这个阶段你可能会看到下面的警告:
configure: WARNING:
*** These auxiliary programs are missing or
*** incompatible versions: msgfmt
*** some features will be disabled.
*** Check the INSTALL file for required versions.
抱怨说缺少或有不兼容的 msgfmt 程序,这没有什么大问题,不过有时候可能会在运行测试套件的时候出问题。msgfmt 程序是宿主系统 Gettext 应当提供的一部分。如果担心宿主系统的 msgfmt 有兼容性问题,你可以升级宿主系统的 Gettext ,也可以忽略这个问题而不去管它。
编译软件包:
make
Glibc GCC Binutils 三者是整个工具链的核心,因此如何对其进行定制就显得很重要。这里有一篇文章《Glibc 安装指南》,可以在漫长的编译过程中作为参考资料读一读。
现在编译完成了。正如前面说过的,在这里运行测试没什么用处,但是如果你坚持要测试的话可以运行下列命令:
make check
关于测试失败重要性的讨论请参考这里:节 6.9, "Glibc-2.3.6."
在这一章里,Glibc 的测试套件高度依赖于宿主系统的工具和环境,尤其是内核。因为这个原因,有时错误很难避免,但是无需太在意。第六章里面的 Glibc 才是我们最后所使用的,那里的 Glibc 需要通过绝大多数测试。但要注意的是,即使在第六章里,有的失败还是会出现,比如 math 测试。
当遇到一个错误时,记录下来,再用 make check 继续。测试套件会从出错的地方继续进行。你也可以用 make -k check 来一次把测试做完。但如果你这样做的话,就要把屏幕输出记录到文件里( make -k check > ck_log ),以便最后检查到底出了多少错,哪些测试出错了。
在安装 Glibc 的过程中,它会警告缺少 /tools/etc/ld.so.conf 文件。其实这没什么关系,不过下面的命令能修正它:
mkdir -v /tools/etc
touch /tools/etc/ld.so.conf
安装软件包:
make install
不同的国家和文化,使用不同的习俗来交流。这样的习俗很多,从比较简单的时间和日期格式,到非常复杂的语言发音。GNU 程序的"internationalization"(国际化,又称"i18n",18表示中间的18个字母)是以 locale 来实现的。
注意
如果刚才没有运行测试套件,那么现在就没有必要安装 locale 。在下一章里面我们将会安装。如果你一定要安装 locale ,请参考节 6.9, "Glibc-2.3.6."的内容。
关于这个软件包的详细资料位于节 6.9.4, Glibc 的内容