
23 天前
想象一下,有一台机器能读懂自己的设计图,然后造出另一台一模一样的自己——这不是科幻,而是程序员沃伦·图米在GitHub上完成的现实:他用C语言的子集,写出了一个能编译自己的C编译器。
这个项目花了64个阶段才完成,从最基础的词法扫描,一步步迭代到支持指针、结构体、函数调用,甚至能生成ARM和摩托罗拉6809处理器的汇编代码。最神奇的是,它最终能把自己的源代码喂进去,吐出一个功能完全一致的新编译器。这背后的技术,就是自举编译器——一种能自我复制、自我进化的程序工具。但它到底是怎么打破「先有鸡还是先有蛋」的死循环的?
你可以把自举编译器的诞生,看成一场「从极简到完整」的接力赛。
第一步,得先有个「火种」——用汇编或者更简单的语言,写出一个只能处理C语言最小子集的「婴儿编译器」。它不用太聪明,能读懂变量、加减乘除和简单的if语句就行。这就像先造出一台只能拧螺丝的简易机床。

第二步,用这个「婴儿编译器」能读懂的C子集,写出一个功能更完整的编译器代码。再用「婴儿编译器」把这份代码编译成可执行程序——这就得到了「少年编译器」,它能处理更多语法,比如循环、函数。
接下来就是关键的自举时刻:用「少年编译器」去编译它自己的源代码。如果一切顺利,就能得到一个和自己功能完全一致的「成年编译器」。从此之后,它就可以自我迭代了——新版本的编译器代码,用旧版本编译就能生成,再也不需要依赖其他语言的工具。
这个过程的核心,是用语言的子集去构建语言的全集,再用全集反过来覆盖子集。就像用一把小锤子,敲出一把更大的锤子,最后用大锤子把小锤子换成和自己一样的大锤子。
沃伦的64个阶段,每一步都是在给这个「能复制自己的机器」添砖加瓦。
最基础的是词法扫描和语法分析——这相当于给编译器装上「眼睛」和「语法书」。词法扫描会把你写的代码拆成一个个最小的「单词」,比如int、=、5;语法分析则会按照C语言的规则,把这些单词拼成有意义的句子,判断你写的代码是不是符合语法。这两步就像老师批改作文,先看每个字对不对,再看句子通不通。
之后的阶段,就是一步步给编译器加功能:先支持变量,再支持循环,接着是函数、指针、结构体。每加一个功能,沃伦都要先在编译器代码里实现对这个功能的解析能力,再确保编译器能把包含这个功能的代码,正确转换成机器能懂的汇编指令。

到了第60阶段,项目迎来了关键的「三重测试」:用现有的编译器编译源代码得到版本A,再用版本A编译源代码得到版本B,最后比较版本A和版本B的二进制文件是否完全一致。如果一致,就证明这台机器真的能精准复制自己了。

更值得关注的是,这个编译器不仅能在x86平台运行,还能生成ARM和6809处理器的代码。这意味着它打破了平台的限制,能在不同的硬件上「复制」自己。
自举编译器听起来完美,但它也不是没有代价。
最明显的是启动门槛极高。你得先写出那个「婴儿编译器」,而这需要对汇编语言和硬件架构有深入理解——相当于你得先学会用原始工具造出第一把锤子。沃伦的项目能顺利推进,也借鉴了另一个开源编译器SubC的代码和思路。
还有一个隐藏的风险,就是肯·汤普逊在1984年提出的「信任信任攻击」:如果最初的「婴儿编译器」被植入了后门,它会在编译后续版本时,悄悄把后门也复制进去。即使你后来修改了源代码,只要用被污染的编译器编译,后门依然会存在。这种攻击像病毒一样潜伏在自举链里,很难被发现。
另外,自举也可能让语言设计陷入僵化。为了保证编译器能自举,语言的新特性必须先考虑编译器能不能用自身实现,这可能会限制语言的创新。比如有些复杂的语法糖,因为会让自举变得过于复杂,就可能被放弃。
沃伦最终停止了这个项目,转向开发新的编程语言alic,但这个64阶段的自举编译器,依然是编译器领域的一个经典实践。它不仅证明了自举的可行性,也为后来的学习者铺出了一条清晰的路径。
自举编译器的本质,其实是用代码构建代码的元能力——它让编程语言不再依赖外部工具,真正实现了自我闭环。就像生命从简单的有机分子,进化出能自我复制的DNA一样,自举编译器就是程序世界里的「自我复制基因」。
用语言构建语言,用程序生成程序。 这不仅是技术的突破,更是对「代码能做什么」的一次重新定义:当程序能复制自己、进化自己时,它就不再只是人类的工具,更成了人类延伸创造力的载体。
点击充电,成为大圆镜下一个视频选题!