GCC为什么需要编译两次才能完成自举

多年前安装LFS这个Linux发行版时候,GCC都需要进行多次编译进行自举,当时并为对其中的原因进行深入探索,再次看到LFS中的文档说明,就将其记录下来。

什么是编译器自举

在计算机科学中,自举是一种自生成编译器的技术——也就是,某个编程语言的编译器(或汇编器)是该语言编写的。最初的核心编译器(自举编译器)是由其他编程语言生成的(可以是使用汇编语言),之后的编译器版本则是使用该语言的最小子集编写而成。自生成编译器的编译问题被称为编译器设计的先有鸡还是先有蛋问题,而自举则是这个问题的解决方法。

自举的一般步骤

  • 步骤0:准备自举编译器的工作环境,选择自举编译器的编程语言和输出语言。在裸机(也就是没有任何语言的编译器)的情况下,源代码和输出代码需被编写为二进制机器代码,或者可以通过在目标机器之外的其他机器上交叉编译来创建。否则,该语言的自举编译器必选使用目标机器上存在的一种语言编写而成,并且将生成可以在目标机器上执行的东西,包括高级编程语言、汇编语言、对象文件、甚至机器代码。
  • 步骤1:生成自举编译器。这个编译器能够将自己的源代码编译成能在目标机器上运行的程序,之后的语言开发将会由这个自举编译器所支持的语言上拓展,进入步骤2。
  • 步骤2:使用自举编译器生成全功能编译器。通常是分阶段进行的,比如语言版本X的编译器能够支持语言版本X+1的功能,但自己不会使用这些功能。一旦这个编译器完成测试并可自行编译后,则现在语言版本X+1的功能可能会被编译器的后续版本使用。
  • 步骤3:使用步骤2的编译器生成全功能编译器。如果需要添加新的语言功能,则从步骤2重新开始。这时候开始,可以使用步骤3生成的编译器代替自举编译器来继续语言的开发。

自举的好处

  • 通过吃自己的狗粮的方式,对正在编译的语言进行测试。
  • 编译器开发人员和缺陷报告人员只需要知道当前编译的语言。
  • 编译器的开发可以在当前编译的高级编程语言上进行。
  • 对编译器后端的改进,不仅改进了通用程序,而且改进了编译器本身。
  • 这是一个全面的一致性检查,因为它应该能够重现自己的目标代码。

GCC为什么需要编译两次

根据LFS文档中的说明,我们定义以下术语:

build:一台我们进行代码构建的机器。
host: 我们运行构建成功得到程序的机器或系统。
target: 指我们的编译器编译出来的程序将要运行的机器,它可能和 host 或者 build 完全不同。

同时我们可以从LFS的文档中看到下面这个表

Stage Build Host Target Action
1 pc pc lfs pc环境中我们使用cc-pc构建了一个交叉编译器cc1
2 pc lfs lfs pc环境中我们使用cc1构建出一个lfs环境的编译器cc-lfs
3 lfs lfs lfs lfs环境中我们使用cc-lfs重构并验证cc-lfs的正确性

其中pc代表着我们已经启动或安装的系统,lfs代表着我们经过chroot后的环境。我们可以将pc环境理解为原始环境,lfs为全新环境,lfs内的软件都是可以脱离pc环境独立运行的。 cc-pc 是pc环境中自带的编译器,可构建出在pc环境中运行的程序。 cc1 是一个交叉编译器,它运行在pc环境中,但是构建出的目标程序是运行在lfs环境中的。 cc-lfs 是一个运行在lfs环境中的编译器,它可以构建lfs环境中运行的程序。

因为C语言以及GCC不单单是一个编译器,它还包含了很多辅助的工具和库。首先就是我们常见的GNU C库glibc。glibc必须要脱离pc环境,能在lfs环境中使用,因此它就需要使用交叉编译器cc1进行编译。但是GCC编译实现本身非常复杂,使用简单的汇编环境实现起来就非常复杂。同时GCC还有一个内部库叫做libgcc,它必须链接glibc才能具备全部的功能。同时标准的C++库(libstdc++)也是需要链接到glibc上的。因此解决这个问题的方法就是先编译出一个降级libgcc和交叉编译器cc1,这个cc1会缺少一些特性,如线程的支持和异常处理。然后我们可以使用这个降级的cc1构建出一个可以在lfs环境中使用且功能完整的glibc,然后进一步构建出功能完成的libstdc++。由于GCC的编译器是依赖libgcc的,而当前cc1所以来的libgcc缺少部分功能,此时即便我们有功能完整的glibc也无法构建出一个功能完成的libstdc++,因此我们需要对GCC进行二次构建。在二次构建的过程中我们构建了cc-lfs,我们会将libgcc连接到我们刚刚构建的glibc上,从而在后面我们切换到lfs环境中可以使用cc-lfs正确的构建libstdc++。