2.2.2 构建工具链的过程
针对我们的具体情况,目标系统与宿主系统都是基于IA32体系架构的,所以一种方式是利用宿主的编译工具链来构建一套独立于宿主系统的自包含的本地编译工具链;另外一种方式就是构建一套交叉编译工具链。在本章中,我们采用交叉编译的方式构建工具链,主要原因是:
❑Linux的主要应用的场景之一是嵌入式领域,嵌入式设备中存在多种不同的体系架构,受限于嵌入式设备的性能和内存等,像编译链接这种工作都在工作站或PC上进行。因此,使用交叉编译的方法,对读者进行嵌入式开发更有益处。
❑再者,采用交叉编译的方式相对更有助于读者理解链接过程及文件系统的组织。所以,虽然我们的宿主系统和目标系统都是基于IA32的,但是我们利用交叉编译的方式构建编译工具链。
如果读者没有嵌入式开发的相关经验,也不必担心,交叉编译与本地编译本质上并无区别。交叉编译就是在目标机器与宿主机器体系结构不同时使用的编译方法。无论是本地编译还是交叉编译,工具链的各个组件均是运行在工作站或者PC上,只不过对于本地编译,我们编译出的程序运行在本地系统上,或者至少是运行在与宿主系统相同的体系架构的机器上。而对于交叉编译,编译出的程序是运行在目标机器上的。
如图2-7所示,如果目标机器与宿主系统相同均为IA32架构,那么就使用宿主机器上的本地编译工具链,编译出的二进制代码对应的也是IA架构的指令。如果目标机器是其他的体系结构的,比如ARM,那么就需要使用宿主系统上的交叉编译工具链,编译出的二进制代码对应的也是目标体系架构的指令,如ARM指令。
图 2-7 本地编译和交叉编译
GNU将编译器和C库分开放在两个软件包里,好处是比较灵活,在工具链中可以选择不同的C库,比如Glibc、uClibc等。但是,也带来了编译器和C库的循环依赖问题:编译C库需要C编译器,但是C编译器也依赖C库。虽然理论上编译器不应该依赖C库,C编译器只负责将源代码翻译为汇编代码,但是事实并非如此:
❑C编译器需要知道C库的某些特性,以此来决定支持哪些特性。所以,为了支持某些特性,C编译器依赖C库的头文件。
❑C++的库和编译器需要C库支持,比如异常处理部分和栈回溯部分。
❑GCC不仅包含编译器,还包含一些库,这些库通常依赖C库。
❑C编译器本身也会使用C库的一些函数。
但是,幸运的是,C99标准定义了两种运行环境,一种是"hosted environment",针对的是具有操作系统的环境,程序一般是运行在操作系统之上的,因此这个操作系统不仅是内核,还包括外围的C库,对于程序来说,就是一个"hosted environment"。另外一种是"freestanding environment",就是程序不需要额外环境的支持,直接运行在裸机(bare metal)上,比如Linux内核,以及一些运行在没有操作系统的裸板上的程序,不再依赖操作系统内核和C库,所有的功能都在单个程序的内部实现。
针对这两种运行环境,C99标准分别定义了两种实现:一种称为"hosted implementation",支持完整的C标准,包括语言标准以及库标准;另外一种是"freestanding implementation",这种实现方式支持完整的语言标准,但是只要求支持部分库标准。C99标准要求"hosted implementation"支持"freestanding implementation",通常是通过向编译器传递参数来控制编译器采用哪种方式进行编译。
通常"hosted implementation"的实现包含编译器(比如GCC)和C库(比如Glibc)。而"freestanding implementation"的实现通常只包含编译器,如GCC,最多再加上一个简单的库,比如典型的newlib。但是如果没有newlib的支持,GCC自己也可以自给自足。
"freestanding implementation"的实现,恰恰解决了我们提到的GCC和Glibc的循环依赖问题。我们可以先编译一个仅支持"freestanding implementation"的GCC,因为在这种情况下,不需要C库的支持。但是"freestanding implementation"的GCC却可以编译Glibc,因为Glibc也是一个自包含的,完全自给自足。事实上,Glibc中也有小部分地方使用了GCC的代码,但是这不会带来依赖的麻烦,因为GCC一定是在Glibc之前编译的。
在编译目标系统的C库,甚至是编译GCC中包含的目标系统上的库时,都需要链接器,因此,Binutils是编译器和C库共同依赖的。索性Binutils几乎没有任何依赖,只需要利用宿主系统的工具链构建一套交叉Binutils即可。
另外值得一提的是内核头文件。在Linux系统上,在编译C库前需要安装目标系统的内核头文件,从某种意义上讲,内核头文件就是C库和内核之间的一个协议(Protocol)。而且,C库会根据内核头文件检查内核提供了哪些特性,以及需要在C库层面模拟哪些内核没有提供的服务。
综上所述,我们可以按照如下步骤构建工具链:
1)构建交叉Binutils,包括汇编器as、链接器ld等。
2)构建临时的交叉编译器(仅支持freestanding)。
3)安装目标系统的内核头文件。
4)构建目标系统的C库。
5)构建完整的交叉编译器(支持hosted和freestanding)。
最后提醒读者注意一点,上面的目标平台也是IA32的,并且使用的C库是Glibc。如果是其他平台的,或者是用了不同的C库,编译过程可能会略有差异,比如为了使最终编译C库的编译器具有更多的特性,有的编译过程将使用freestanding编译器编译一个简化版的hosted编译器,然后用这个hosted的GCC再编译Glibc等。但是,无论如何,交叉编译的关键还是构建freestanding的编译器。freestanding的编译器解决了鸡和蛋的问题(即GCC和Glibc的循环依赖),一旦解决了鸡和蛋的问题后,其他的问题就都迎刃而解。