欢迎光临!
若无相欠,怎会相见

在现代系统上构建和使用已有 29 年历史的编译器

https://miyuki.github.io/2017/10/04/gcc-archaeology-1.html

在这篇文章中,我将分享我在现代系统上构建和使用 GNU C 编译器的最早版本之一——GCC 1.27(1988 年发布)的经验。

环境

在我的实验中,我使用了一个基于 Debian 8 的 LXC 容器(为什么不是 9?因为我在 Debian 9 发布之前就开始写这篇文章了)。我决定使用 i386 容器(而不是 amd64 容器)来节省工作量;我敢肯定,在与路径和符号链接共舞之后,amd64 的一切也会好起来的)。

Debian 8 将 GCC 4.9.2 作为主机编译器、Binutils 2.25 和 Glibc 2.19 发布。

获取源码

GCC 的旧版本可从 gcc.gnu.org 的官方服务器获得。第一个可用版本是 0.9 版。不幸的是,这个版本对我们来说不是很有趣,因为它不支持 i386 架构(或任何兼容的架构)。

GNU C 编译器的 1.27 版(请注意,当时它被称为 GNU C 编译器而不是 GNU 编译器集合)是第一个支持 i386 架构的可用版本。它于 1988 年 9 月 5 日发布(当时 Linux 还不存在)。

构建和测试

与现代版本不同,GCC 1.27 不包含任何庞大的配置脚本,并且配置是手动完成的。尽管如此,它非常简单且有据可查。事实上,它只涉及创建 4 个符号链接。

令人惊讶的是,C 标准的兼容性在 GNU 工具链中得到了很好的维护。此外,基本的 Glibc 标头也向后兼容旧编译器。由于这一事实,在修补了十几行(~92000 行)代码后,可以使用现代编译器编译 GCC 1.27。它们中的大多数都与 C 库中的更改有关,还有一些是由于现代 C 编译器中实现的更严格的 C 语法规则(参见 gcc-1.27.patch)。

另一个问题是缺少一个名为 syms.h 的标头,它显然定义了一些用于以 SDB 格式生成调试信息的常量。缺少标头也就不足为奇了:在 Linux 上,SDB 很久以前就被 DWARF 格式所取代。我设法在麻省理工学院网站上找到了这些标题:syms.h。该 URL 表明这些文件与 IBM AIX OS 有关。

标头和规格

在现代系统上使源代码可编译并不足以获得一个有效的编译器。您可能知道,GNU 工具链包括其他工具,例如汇编程序和编译器与之交互的链接器。幸运的是,生成的汇编代码的语法与现代版本的 GNU 汇编程序完全兼容(调试信息除外)。至于链接器,需要对所谓的链接器规范(即 GCC 驱动程序使用的命令行选项)进行一些调整。

应用这些修复后,我们现在有一个能够生成有效 elf 二进制文件的编译器。

引导

快速提醒:引导编译器意味着使用自身编译它。有关更多详细信息,请参阅“Bootstrapping (compilers)”维基百科文章。

bootstrap 的第一个小问题来自 glibc 标头:现代版本的 glibc 假定编译器支持 64 位整数类型,而 GCC 1.27 则不然。罪魁祸首是 /usr/include/bits/byteswap.h .通过向编译器传递标志 -D_BITS_BYTESWAP_H ,可以很容易地禁用它的包含。

现在,尝试引导编译器会导致在编译与 C 预处理器相关的文件 cccp.c 时失败:编译器崩溃并出现分段错误。在深入研究了这次失败的原因之后,我设法使用 C-Reduce 生成了一个最小的失败测试用例:

	  struct file_buf { } fn1(), a;
	  fn2() { a = fn1(); }

编译器在尝试取消引用空指针时崩溃,同时将表达 a = fn1() 式从 AST 转换为其中间表示形式 (RTL)。显然,i386 后端在代码中存在一个错误,该错误处理对按值返回结构的函数的调用。

事实证明,修复这个错误相对简单。添加 GCC 1.31 中存在的单个检查可以修复错误,并且引导程序现在成功。

顺便说一句,这可能意味着以前没有人尝试过在 x86 上引导 GCC 1.27。

此外,我设法执行了引导比较,即构建:

  • 阶段 1 编译器,即由主机编译器编译的 GCC 1.27 (GCC 4.9.2)
  • 第 2 阶段编译器,即由第 1 阶段编译器编译的 GCC 1.27
  • 第 3 阶段编译器,即由 2 阶段编译器编译的 GCC 1.27

正如我所料,第 2 阶段和第 3 阶段是相同的。

四处游玩 (Playing around)

我试图找到一些代码(除了 GCC 本身)来编译和使用。请记住:我们需要用所谓的 K&R C 编写代码(因为当时还不存在 ANSI C89 标准)。

例如,我使用了一个程序来生成 Mandelbrot 集的 ASCII 图像(不幸的是,我未能找出此代码的作者是谁)。

以下是代码,格式化后可读性更好:

	  main (n)
	  {
	    float r, i, R, I, b;
	    for (i = -1; i < 1; i += .06, puts (""))
	      for (r = -2; I = i, (R = r) < 1; r += .03, putchar (n + 31))
	        for (n = 0; b = I * I, 26 > n++ && R * R + b < 4;
	          I = 2 * R * I + i, R = R * R - b + r);
	  }

如您所见,它涉及相当复杂的控制流和浮点运算。GCC 1.27 编译它没有错误,并且输出与现代版本的 GCC 编译的相同代码的输出相匹配。

GCC 1.27 包含许多现代编译器的典型功能,例如:

  • Compiler warnings 编译器警告
  • Optimizations 优化
  • Debug information output 调试信息输出
  • Instrumentation for code profiling 代码分析检测
  • Command-line flags controlling all of the above 控制上述所有内容的命令行标志

探索 GCC 源代码

另一件令我惊讶的事情是,这些旧版本的 GCC 的许多想法甚至大部分代码至今仍在使用。

编译器通过可切换的头文件支持多个后端(即, config-i386v.h 用于 i386 System V, config-sparc.h 用于 Sparc Sun)。计算机描述 .md 文件描述 CPU 指令模式。在构建阶段,它们被解析并转换为 C 源文件,稍后与编译器链接。当然,在过去的几十年里,DSL .md 已经发展,但原理保持不变。此外,LLVM 编译器基础结构也使用类似的技术

两种无处不在的数据类型 tree 分别 rtl 用于在编译器前端和后端表示程序,仍然发挥其作用。

当然,这并不意味着当前版本的 GCC 停留在八十年代。尽管有一些相似之处,但主要增强功能的数量确实很大,我不会费心列出它们(对于包含一些基准、图表和信息图表的帖子来说,这是一个很好的主题)。

旧版本的 GCC 没有 bug 跟踪器站点,因此,错误和增强请求列表保存在一个名为 PROBLEMS .从 1.27 版开始,它包含 27 个条目,编号上有许多空白。实际上,最后一项的编号为 122。这可能意味着在此版本之前已修复剩余的 95 个问题。

 

 

赞(0) 打赏
转载请注明:飘零博客 » 在现代系统上构建和使用已有 29 年历史的编译器
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

欢迎光临