交叉编译工具链构建原理
这是与弗朗西斯科·图尔科(Francesco Turco)讨论的结果。
弗朗西斯科为初学者提供了一个很好的教程(死链,Wayback机器没有存档版本),以及一个示例,从x86_64 Debian主机为ARM目标构建工具链的分步过程。
谢谢弗朗西斯科发起这个活动!
我想要一个交叉编译器!你说的这个工具链是什么?
交叉编译器实际上是不同工具的集合,这些工具被设置为紧密地协同工作。这些工具以某种级联方式链接在一起,其中一个工具的输出成为另一个工具的输入,最终产生在机器上运行的实际二进制代码。因此,我们称这种安排为“工具链”。当工具链要为运行它的机器之外的机器生成代码时,这被称为交叉工具链。
那么,工具链中的组件是什么?
在工具链中起作用的组件首先是编译器本身。编译器将源代码(用C、C++等语言)转换成汇编代码。选择的编译器是GNU编译器集合,众所周知的gcc
。
汇编程序解释汇编代码以生成目标代码。这是由二进制实用程序完成的,如GNU binutils。
一旦生成了不同的目标代码文件,它们就会聚集在一起形成最终的可执行二进制文件。这称为链接,通过使用链接器来实现。GNU binutils还附带了一个链接器。
到目前为止,我们得到了一个完整的工具链,能够将源代码转化为实际的可执行代码。根据目标上运行的操作系统,我们还需要C库。C库提供了一个执行基本任务的标准抽象层(例如分配内存、在终端上打印输出、管理文件访问……)。有许多C库,每个都针对不同的系统。对于Linux桌面,有glibc
或eglibc
甚至uClibc
,对于嵌入式Linux,您可以选择eglibc
或uClibc
,而对于没有操作系统的系统,您可以使用newlib
、dietlibc
,甚至根本不使用
。还有一些其他的C库,但它们没有被广泛使用,并且/或者是针对非常特殊的需求(例如,klibc
是C库的一个非常小的子集,旨在构建受约束的初始ramdisks)。
在Linux下,C库需要知道内核的API,以决定存在什么特性,如果需要的话,为缺失的特性包含什么模拟。该API由内核头文件提供。注意:这是特定于Linux的(可能还有极少数其他操作系统),其他操作系统上的C库不需要内核头文件。
现在,所有这些组件是如何连接在一起的?
到目前为止,已经涵盖了所有主要组件,但它们需要按照特定的顺序构建。从我们最终要使用的编译器开始,我们可以看到依赖关系
是什么。我们称该编译器为最终编译器
。
- 最终的编译器需要C库来知道如何使用它,但是:
- 构建C库需要编译器
A需要B,B需要A .这是典型的先有鸡还是先有蛋的问题……这可以通过构建一个精简的编译器来解决
,该编译器不需要C库,但能够构建C库
。我们称之为引导
、初始
或核心编译器
。这是新的依赖列表:
- 最终的编译器需要C库来知道如何使用它
- 构建C库需要核心编译器,但是:
- 核心编译器需要C库头文件和启动文件来知道如何使用C库
B需要C,C需要B .又是先有鸡还是先有蛋。为了解决这个问题,我们需要构建一个只安装头文件和启动文件的C库
。启动文件
(也称为C运行时
或CRT
)是gcc需要在NPTL系统上启用线程本地存储(TLS)的极少数文件
。所以现在我们有:
- 最终的编译器需要C库来知道如何使用它
- 构建C库需要一个核心编译器
- 核心编译器需要C库头文件和启动文件来了解如何使用C库,但是:
- 构建启动文件需要编译器
天啊… C需要D,D又需要C。因此我们需要构建一个更简单的编译器,它不需要头文件,但需要启动文件。这个编译器也是一个引导、初始或核心编译器。为了区分两个核心编译器,我们称之为一个核心通道1(core pass 1
),而前者为一个核心通道2(core pass 2
)。依赖性列表变成:
- 最终的编译器需要C库来知道如何使用它
- 构建C库需要编译器
- 核心通道2编译器需要C库头文件和启动文件来了解如何使用C库
- 构建启动文件需要编译器
- 我们需要一个核心通道1编译器
正如我们前面所说的,C库也需要内核头文件
。对内核头没有要求,所以在这种情况下故事结束了:
- 最终的编译器需要C库来知道如何使用它
- 构建C库需要一个核心编译器
- 核心通道2编译器需要C库头文件和启动文件来了解如何使用C库
- 构建启动文件需要编译器和内核头文件
- 我们需要一个核心通道1编译器
我们需要增加一些新的要求。当我们为目标编译代码时,我们需要汇编程序和链接程序。当然,这样的代码是从C库构建的,所以我们需要在C库启动文件之前构建binutils,以及完整的C库本身。此外,gcc中的一些代码也将转而在目标上运行。幸运的是,对binutils没有要求。因此,我们的依赖链如下:
- 最终的编译器需要C库来知道如何使用它,还需要binutils
- 构建C库需要核心通道2编译器和binutils
- 核心通道2编译器需要C库头文件和启动文件,以了解如何使用C库和binutils
- 构建启动文件需要编译器、内核头文件和binutils
- 核心通道1编译器需要binutils
依次构建组件:
- binutils
- 核心通道1编译器
- 内核头文件
- c库头文件和启动文件
- 核心通道2编译器
- 完整的C库
- 最终编译器
是啊!:-)但是我们结束了吗?
事实上,不是的,仍然有缺失的依赖项。就工具本身而言,我们不需要任何其他东西。
但是gcc有一些先决条件。它依靠一些外部库来执行一些重要的任务(比如处理常数中的复数……)。构建这些库有几种选择。首先,人们可能会认为依靠Linux发行版来提供这些库。唉,直到最近,它们才被广泛使用。因此,如果发行版不是太新的话,我们很有可能必须构建这些库(我们将在下面进行构建)。受影响的库包括:
- GNU多精度算术库——GMP(GNU Multiple Precision Arithmetic Library);
- 具有正确舍入的多精度浮点计算的C库——MPFR(Multiple-Precision Floating-point-computations with correct Rounding);
- 复数算术的C语言库——MPC。
这些库的依赖关系如下:
- MPC需要GMP和MPFR
- MPFR需要GMP
- GMP没有先决条件
因此,构建顺序变为:
- GMP
- MPFR
- MPC
- binutils
- 核心通道1编译器
- 内核头文件
- C库头文件和启动文件
- 核心通道2编译器
- 完整的C库
- 最终编译器
是啊!或者更多?
这足以构建一个功能工具链。所以如果你现在已经受够了,你可以到此为止。或者如果你很好奇,你可以继续阅读。
gcc还可以利用其他一些外部库。这些额外的可选库
用于启用gcc中的高级功能
,如循环优化(GRAPHITE)
和链路时间优化(LTO, Link Time Optimisation)
。如果要使用这些库,您需要另外三个库:
要启用GRAPHITE,根据GCC版本,可能需要以下一项或多项:
- PPL, Parma多面体库;
- ISL, 整数集库;
- CLooG/PPL, 使用PPL后端的Chunky循环生成器;
- CLooG, 使用ISL后端的Chunky循环生成器。
要启用LTO:-ELF对象文件访问库,libelf
这些库的依赖关系如下:
- PPL要求GMP
- CLooG/PPL需要GMP和PPL或ISL之一;
- ISL没有先决条件;
- libelf没有先决条件。
列表现在看起来像这样:
- GMP
- MPFR
- MPC
- CLooG/PPL(如果需要)
- ISL(如果需要)
- libelf(如果需要)
- binutils
- 核心通道1编译器
- 内核头文件
- C库头文件和启动文件
- 核心通道2编译器
- 完整的C库
- 最终编译器
这个列表现在已经完成了!哇哦!或者是?
但是为什么crosstool-NG的步骤更多呢?
从理论的角度来看,已经制定的十三个步骤是必要的步骤。然而在现实中,还是有一些小的不同。crosstool-NG中的额外步骤有三个不同的原因。
第一,GNU binutils不支持某些类型的输出。用binutils生成平面二进制文件是不可能的,所以我们必须使用另一个添加了这种支持的组件: elf2flt
。elf2flt还需要zlib
压缩库-如果我们正在构建加拿大的或跨本地的工具链,我们可能无法使用主机的zlib。
第二,工具链的本地化需要一些主机操作系统上的附加库: gettext
和 libiconv
。
第三,crosstool-NG还可以构建一些额外的调试实用程序在目标上运行。这是我们构建的地方,例如,cross-gdb
、gdbserver
和原生gdb
(最后两个在目标上运行,第一个在工具链所在的机器上运行)。其他工具(strace
、ltrace
、DUMA
和dmalloc
)与工具链完全无关,但在开发时非常有用,因此作为好东西包含在内(它们很容易构建,所以没问题;更复杂的东西不值得包含在crosstool-NG中)。