工具链技术说明

本小节解释一下LFS编译方法的技术细节。没必要现在就把本节搞懂,因为真正安装过以后,会有更深的体会。以后遇到问题,可以回过头来参考本节的内容。

第五章的目的是提供一个合理的临时环境,以便我们在第六章能chroot进去,并在里面创建干净的LFS系统。为了做到这一点,我们希望与主系统关系越少越好,因此才创建一个自给自足的工具链。整个编译过程,既考虑要让新手遇到最少问题,又考虑了给读者提供最多的教育知识。也就是说,还有很多高级技术,可以用来创建这样的系统。

重要: 接着读下去以前,你需要确定一下工作平台的名称,也称作 target triplet.对多数人来说,都是i686-pc-linux-gnu. 确定工作平台的一个简单方法,是运行很多软件包都有的 config.guess 脚本。解压 Binutils 的源码,并运行./config.guess,看看结果说什么。

你还需要注意你的工作平台上动态连接器的名字,也被称为动态加载器,别与标准连接器ld搞混了,后者是binutils的一部份,而动态连接器是Glibc提供的,负责寻找和加载程序所需的共享库。对多数人来说,它的名字是ld-linux.so.2。在比较少见的平台上,它可能叫ld.so.1,在新的 64 位平台上,可能名字完全不同。你可以查看主系统中/lib目录的内容来确定动态连接器的名字。一个更保险的办法,是用下面的命令来检查主系统中的任何一个二进制程序:'readelf -l <name of binary> | grep interpreter'并观察结果。关于所有平台的参考在Glibc源码目录里的shlib-versions 文件中。

第五章编译方法能顺利进行的几个关键技术要点:

首先安装 Binutils 是因为 GCC 和 Glibc 在运行./configure 脚本的时候,会针对汇编器和连接器做种种测试,以决定打开或关闭某些功能。这里的重要性比你想像的大得多!配置不正确的GCC和Glibc会组成一个有点缺陷的工具链,这种缺陷可能开始还看不出来,到LFS的最后部份才显示出来,那前面的就都白做了。幸好我们还有测试套件,以免浪费太多时间。

Binutils 安装汇编器和连接器在两个目录下:/tools/bin/tools/$TARGET_TRIPLET/bin。一个路径下的工具很难连接到另一个路径下。另一个连接器的特性是库文件搜索顺序。用 ld 命令加上 --verbose 参数可以得到详细信息。比如:'ld --verbose | grep SEARCH' 会告诉你当前的搜索路径和它们的顺序。你可以看到什么文件被 ld 连接,只要编译一个简单程序被使用 --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的configure脚本不是靠搜索$PATH目录来决定使用什么工具的。在gcc的实际运行中,搜索路径可能用不到。你可以用下面的命令来看看 gcc会使用哪个标准连接器。'gcc -print-prog-name=ld'.在编译一个简单程序时向 gcc 传递 -v 参数,可以得到详细信息。例如: 'gcc -v dummy.c' 会告诉你关于预处理器,编译和汇编阶段,以及gcc的头文件搜索路径及顺序等的详细信息。

下一个软件包是 Glibc. 编译 Glibc 时最重要的是编译器,二进制工具和内核头文件。编译器通常没什么问题,因为Glibc总是使用$PATH目录里找到的 gcc。二进制工具和内核头文件就有点麻烦了,因此我们采用保险的方法,使用可用的configure开关来强制特定选择。在运行./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 <name of binary> | grep interpreter'来验证。通过修正 gcc的specs文件,我们确保从这里开始直到第五章结束,每个程序都使用 /tools/lib下的动态连接器。

因为要使用新的动态连接器,所以我们才要对第二遍的GCC打specs补丁。如果不这么做,GCC自身会含有对主系统的/lib目录的硬路径引用,这会有影响我们与主系统分离的目标。

在第二遍安装 Binutils时,我们可以利用--with-lib-path来控制ld的库搜索路径。从这里开始,核心工具链已经自给自足了。第五章的随后部份都会连接到/tools下的新Glibc上,这样就对了。

第六章进入chroot环境后,我们第一个安装的重要软件包是Glibc,前面我们说过,它具有自给自足的性质,只要把它安装到 /usr前缀下以后,我们再把工具链改变回缺省值,然后就能进行余下的LFS系统安装了。

静态连接的说明

除了特殊的任务外,许多程序还要执行很多常用和琐碎的操作,比如分配内存,搜索目录,打开和关闭文件,读写它们,操作字符串,模式匹配,数学计算等等。为了避免让每个程序都“重新发明轮子”(意为重复的创新),GNU 系统以库文件的方式提供这些基本功能和函数。任何一个 Linux 系统中最重要的库都是 glibc

将库文件中的函数连接到使用它们的程序中,有两种方法:静态连接或动态连接。当一个程序是静态连接时,它使用的函数会包含在可执行文件中,结果就是比较大的执行文件。当一个程序是动态连接时,可执行文件中包含的是针对连接器的引用,说明了要使用的库文件名称,以及使用的函数名称,结果就是执行文件要小多了。这个可执行文件在某种程度上比静态连接的要慢,因为在运行时连接要花一些时间。(还有第三种方法,是使用动态连接器的可编程接口,参见dlopen的man文档,以获得更多信息。)

动态连接是Linux上的缺省用法,有三个主要的优点。首先,你只需要在硬盘上有一份可执行的库文件代码,而不是在不同程序里有很多份相同的代码 -- 这样就节省了硬盘空间。第二,当几个程序同时使用同样的库函数,只需要有一份函数代码存在于内存中 -- 这样就节省了内存。第三,当库函数修正了一个 bug 后,或是做了某种程度的改进后,你只需要重新编译这个库文件,而不是重新编译所有使用已改进函数的那些程序。

为什么我们要在本章的前两个软件包里使用静态连接呢?有三方面的原因,历史原因,教育价值和技术考虑。历史上来说,以前的LFS第五章里使用静态连接来编译每一个程序。教育价值在于了解动态连接和静态连接的区别是非常有用的。从技术上说,我们可以得到独立于主系统的可执行文件。然而,应该注意,即使前两个软件包是动态连接的,还是能成功的编译LFS系统。