最早的 C 编译器确实是用汇编语言编写的,因为汇编语言可以直接控制硬件,生成机器码,而当时并没有现成的高级语言编译器可供使用。后来,用汇编实现的 C 编译器被用来编译用 C 语言重写的新版编译器,这个过程被称为自举或引导编译。下面详细说明其中的原理和论据:
1. 初始阶段:汇编器的作用
-
汇编器的优势:
汇编语言能直接生成目标机器码,操作简单且依赖于底层硬件。早期的计算机系统中,汇编器是最直接且可靠的工具,因此用汇编编写编译器可以确保编译器能正常运行。 -
无循环依赖问题:
当时的汇编器是已经存在且稳定的工具,用汇编编写的 C 编译器不依赖于 C 编译器本身。这就为后续使用 C 编写编译器提供了一个“基石”。
2. 自举(Bootstrapping)的过程
-
第一步:用汇编编译器生成 C 编译器
最初的 C 编译器完全由汇编代码实现。它能把 C 语言代码转换成机器码。由于这个版本是用汇编实现的,所以不存在依赖自身的情况。 - 第二步:用初始 C 编译器编译新版编译器
当人们希望将编译器用 C 语言重写时,就使用最初用汇编实现的 C 编译器来编译新的用 C 写的编译器源代码。这个过程叫做“自举”。- 关键点在于:最初的编译器已经存在,是独立于 C 语言实现的,它作为一个可靠的工具来编译 C 语言的源代码。
- 第三步:自举迭代
编译出的新版编译器经过验证后,就可以用它来编译自身的新版本,这样逐步更新、优化,但始终有一个可用的“旧版本”作为基础,这个过程不会导致真正的循环依赖,因为:- 每个新版本都是由一个旧版本编译出来的,而旧版本本身已经存在并且是可运行的。
- 如果新版本有问题,可以回退到旧版本,形成一个稳定的迭代链。
3. 为什么不会导致循环依赖?
-
有明确的起点:
循环依赖问题通常指两个或多个组件互相依赖而无法初始化。但在编译器自举中,依赖链的起点是最初用汇编语言实现的编译器,这个版本独立存在,并不依赖于任何 C 编译器。 - 分阶段构建:
自举是一个分阶段的过程。最初的汇编版编译器作为种子(bootstrapper),之后每次构建新版编译器时,总是依赖于前一个已经编译好的版本,而不是直接互相依赖。- 例如,新版 C 编译器由旧版编译器生成,而旧版编译器本身并不需要新版本就能运行,从而避免了循环依赖。
- 验证和对比机制:
在自举过程中,常常会有多个版本的编译器相互编译,通过对比编译结果来验证新版本的正确性。如果新版本和旧版本的输出一致,就说明新编译器可以正确地“自举”,这也是验证编译器质量的一种方法。
4. 实际论据和原理总结
-
实际操作案例:
历史上,C 语言的第一个编译器就是用汇编写的(例如,Dennis Ritchie 在 PDP-11 上的实现)。随后,C 语言被广泛使用,许多 C 编译器逐步用 C 语言重写,再用自身编译自身,这正是自举的成功实践。 -
理论依据:
自举依赖于已有的、独立于目标语言实现的工具(在这个例子中是汇编器),以及一种分阶段的构建策略。只要有一个最初的可用版本,就不存在“先有鸡还是先有蛋”的问题,而是通过迭代不断改进和验证编译器的正确性和性能。 -
防止循环论证:
由于每个编译器版本都是基于一个稳定的前一版本构建的,并且都有一个明确的构建起点(汇编版编译器),因此不会出现循环论证或依赖的问题。这种方法确保了编译器能够自我完善,而不会陷入一个无穷依赖的死循环。
总之,最初用汇编实现的 C 编译器为 C 编译器的自举提供了一个坚实的基础,通过分阶段构建和验证机制,自举过程既能实现用 C 语言重写编译器,又能避免循环依赖的问题。这种方法不仅提高了编译器的可维护性和优化能力,也成为现代编译器自举的标准实践。