Freestanding C与交叉编译器的生成原理分析
0 问题由来
以前也用过C51写过简单的裸机程序,但是并没有认真的考虑过其与Linux环境下一般C语言程序的不同,只是想当然地认为C是跨平台的语言,并没有考虑过C语言的标准问题。
今天在编译GCC交叉编译器时,遇到了种种问题,不得不重新考虑C语言的实现标准,否则很难清晰的了解交叉编译器的编译过程。
1 C编译器的两种实现要求
C语言标准的正式文档中明确提出了C编译器的两种实现标准:
1.1 conforming freestanding implementation
所谓的Freestanding,C编译器只需提供C语言语句的编译,外加
<float.h> <limits.h> <stdarg.h> <stdint.h> <stdalign.h> <stdnoreturn.h>
等基本的类型与数值范围定义头文件。
可以看出,在这种级别的编译器下写程序时,基本的printf都是不能够直接使用的。
此种实现,主要用于裸机开发,比如OS开发,bootloader,以及C库本身的开发。
1.2 conforming hosted implementation
这是一种更加全面的实现,除了包含Freestanding要求的功能外,必须包含完整的标准C库实现。这也是大多数应用程序员能接触到的环境。由于<stdio.h>中的输入输出函数的实现需要OS系统调用的支持,Hosted编译器都是依赖于具体的操作系统而存在。
1.3 小结
可以看出,Hosted是比Freestanding更加全面的C实现要求,Hosted本身包含了Freestanding。例如Hosted的GCC实现,提供了-freestanding参数来作为Freestanding降级使用。
gcc -ffreestanding
C语言这种把语言本身和库独立对待的方式,使得它能够适应更广泛的开发环境。从单片机裸机开发到大型数据库系统,C语言的身影无处不在。
2 C语言的两种执行环境
与C语言的两种实现等级相对应,C语言的程序的执行环境也有两种,这里直接贴出C语言标准原文。
5.1.2 Execution environments Tw o execution environments are defined: freestanding and hosted. In both cases, program startup occurs when a designated C function is called by the execution environment. All objects with static storage duration shall be initialized (set to their initial values) before program startup. The manner and timing of such initialization are otherwise unspecified. Program termination returns control to the execution environment.
在Hosted环境下,C程序入口点必须是命名为main的函数,而且其函数原型必须符合标准。
而在Freestanding环境下,C程序的入口点没有指定的名称,由程序员自行决定。
3 C语言交叉编译器制作过程原理分析
3.1 为什么需要单独生成不同目标平台的编译器
理论上一个编译器可以通过命令行参数来编译生成各种不同平台的可执行程序。例如:
mycompiler -platform=arm-linux 1.c # 生成可以运行在ARM平台上的Linux程序 mycompiler -platform=mips-linux 1.c # 生成可以运行在MIPS平台的Linux程序 mycompiler -platform=i386-linux 1.c # 生成可以运行在Intel X86平台的Linux程序
然而实际上,这种做法的代价是十分巨大的,因为
- 不同平台差异巨大,代码转换逻辑完全不同,在一个可执行程序中包含所有平台转换代码,导致编译器程序本身体积巨大
- 不同平台的C库实现严重依赖于平台,在一个库中包含所有平台的实现代码,造成C库体积巨大,管理混乱。
- 汇编器、连接器也存在同样的问题
所以在现实中,虽然GCC,GLIBC,binutils这样的项目本身能够支持很多的软硬件平台,但是支持方式是通过配置来进行选择性的编译部分源代码来生成针对具体平台二进制工具。
3.2 交叉编译器生成步骤原理分析
理论上一个C交叉编译器的生成非常简单:
(0)编译生成交叉汇编器和交叉链接器
之所以会存在这个步骤是因为,像GCC这样的C编译器本身只负责把C语言代码编译为汇编代码。把汇编代码转换为二进制机器码,以及把二进制对象文件链接为可执行文件的功能需要其他程序来实现。对Unix世界来说,提供汇编链接功能的就是著名的binutils包。
gcc编译器在生成时,需要使用binutils来测试相关功能,以动态生成各种配置与代码。所以,在编译GCC之前,必须要有可用的binutils。
对于汇编程序员来说,binutils已经提供了一个完整的汇编开发环境,完全可以停留这里享受汇编程序设计的美好。
然而,对于C程序员来说,这才刚刚开始。
(1)编译生成Freestanding的交叉编译器;
这个交叉编译器完全可以用来编译裸机C程序,编译OS,编译BootLoader,对于嵌入式底层开发者来说,这样一个编译器就足够了,完全可以到此为止。
然而对于一个在OS环境下的应用开发者来说,就需要一个功能更完整的编译器,以及完整的标准C库,这就需要进入步骤(2)。
(2)用刚生成的交叉编译器编译生成标准C库;
Freestanding编译器可以用来编译标准C库,这个是很重要的。
此时虽然编译器和C库都有了,却存在两个缺陷:
- 编译器可执行文件本身并不知道对应的C库的路径,所以编译程序时就需要额外的命令行参数来提供这些信息。
- 编译器不能提供Hosted类型C程序初始化环境,因为Hosted环境的建立需要标准C库的支持,而Freestanding编译器在生成时还没有可用的标准C库。这就是一个“鸡生蛋蛋生鸡”的问题了。
为了解决上述问题,需要额外的一个步骤:
(3)重新编译生成Hosted的交叉编译器。
这次编译时,可以有(2)生成的标准C库的支持了,所以可以产生出一个完整的Hosted交叉编译器了。