拓冰建站拓冰建站
首页 / 资讯中心 / 正文

编译器自举:自己编译自己的原理与实操验证

入门编译器开发时大部分教程都在教你“写一个能解析源码、生成汇编的程序”很少有人继续追问一句这个编译器本身也是程序那它又是由谁编译的呢这个问题看似钻牛角尖却是编译器领域最经典的“鸡生蛋”难题答案就是编译器自举。我刚开始接触这个概念时也一度以为“自己编译自己”只是某种玄学噱头直到亲手把一份编译器源码先后用两条工具链编译了两遍再对比二进制文件才真正理解了什么叫 self-hosting。这篇文章就从零开始把编译器自举这个概念拆开讲清楚并给出可验证的实操思路。1. 编译器自举到底是什么1.1 一个很容易被忽略的问题编译器也是程序我们平时用 GCC 编译 C 代码用 javac 编译 Java 代码用 Python 直接运行 .py 文件。久而久之很多人形成一种错觉编译器是天然的、系统自带的“翻译机器”。但编译器本质就是一个可执行程序它由无数行代码组成运行时也需要操作系统分配进程、加载文件、管理内存。既然它是程序就需要先通过某种方式变成可执行文件。那么问题来了第一台电脑上还没有编译器世界上第一个编译器是怎么来的更贴近日常的问题是假如你写了一个新语言 miniC并且用 C 语言写了一个 miniC 编译器那么现在想把这个编译器从“一个 C 程序”变成“一个可执行文件”你需要一个大 C 编译器如果哪天大 C 编译器不存在了你的 miniC 还能存活下去吗真正“活着”的语言通常都具备一种能力它的编译器能够编译自己写的源码。编译器一旦能做到这一点就不再依赖别人提供的“施舍”而是形成了一个自给自足的工具链。这个能力就叫做编译器自举。1.2 “自己编译自己”的严格定义编译器自举英文通常写作 compiler self-hosting也叫 bootstrap。它的定义并不复杂一个用 L 语言编写的编译器能够正确编译用 L 语言编写的源码并且这份源码就是该编译器自身的源码。也就是说L 语言写出的编译器能编译 L 语言写出的编译器这就是自举。举个例子GCC 的主体代码是 C 和 C 写的它又能编译 C/C 源码所以 GCC 可以拿自己去编译自己。Clang 同样如此。Go 语言在 1.5 版本之前编译器是用 C 写的从 1.5 开始Go 的编译器改成了 Go 语言自己实现所以 Go 的编译器也能编译自己的源码。这种能力不是偶然而是语言团队刻意追求的工程目标因为一旦实现自举语言就获得了独立性。要注意自举并不要求“编译器凭空产生二进制”。它描述的是语言生态的一种成熟状态而不是物理规则上的“无中生有”。第 3 章会详细展开这个过程的原理。1.3 别和“自举电容”“自举电路”搞混如果你搜“自举”很可能搜出一堆“自举电容工作原理”“自举电路设计”的文章看起来和编译器完全不搭边。这是因为“自举”这个词在多个领域都被使用了。编译领域的自举来自英文 bootstrap这个单词原意是靴子后面的“拔靴带”。有一个经典典故有人掉进沼泽靠拉着自己的靴带把自己拔出来。后来引申为“不依赖外部力量靠自身机制完成启动”。编译器把自身源码翻译成可执行文件就是“拉着自己的靴带把自己提起来”。而电子领域的“自举电路”指的是利用电容存储能量来抬升驱动电压让高侧开关管能可靠导通。两者英文不同、原理不同、应用场景也完全不同只是中文翻译撞了车。所以在学习时如果看到“自举电容”的网页内容直接绕过即可那讲的不是编译器。2. 为什么要让编译器自举2.1 摆脱对宿主语言和宿主编译器的依赖假设你设计了一门新语言 X用 C 写了它的编译器。那么 X 语言生态的命运就部分寄托在 C 编译器身上。如果 C 工具链在新操作系统上不再可用或者 C 编译器的新版本改变了某些行为你的 X 语言就会被“卡脖子”。自举之后X 语言的编译器完全由 X 语言自己编写只需一个最小的引导编译器过渡之后的所有迭代都可以由新版本自身完成。这就是独立性的价值。这种独立性在历史上反复被证明很重要。某个语言如果一直依赖另一种语言实现编译器那么生态越繁荣底层的“被依赖感”就越强而底层语言一旦发生较大的 ABI 变化或开发者停止维护上层语言就非常被动。自举相当于把根扎到自己的土壤里。2.2 用残酷的方式验证语言设计是否够用写一个“能算四则运算”的解释器很容易但写一个“能编译自身源码”的编译器则难得多。自举对语言表达能力是一场极高质量的检验。因为编译器源码要处理字符串、链表、哈希表、递归调用、内存分配、错误处理、模块化设计等问题如果你的语言在这些方面支持不到位写编译器时就会处处别扭。换句话说自举成功意味着这门语言有足够丰富的语法和运行时去编写一个复杂系统程序。这比任何 benchmark 都有说服力。很多语言开发者把“让语言可以写自己的编译器”当作一块试金石不是没有道理。2.3 让编译器开发进入“吃自己的狗粮”阶段软件开发领域有个说法叫 eating your own dog food也就是“自己吃自己的狗粮”。放下编译器开发者的身份把自己当作这门新语言的普通用户用这门语言去写编译器、写标准库、写工具你会立刻发现哪里语法别扭、哪里性能不足、哪里文档缺失。这些都是语言设计过程中极其宝贵的第一手反馈。如果编译器只是用 C 写开发者永远体会不到“作为用户使用自己语言”的感受。一旦新编译器由新语言自身实现后续开发新特性时开发者就能直接受益于自己做的 IDE、调试器、静态检查工具和格式化工具。开发体验也会大幅提升。2.4 建立可审计、可复现的信任链编译器位于整个软件生态的最底层。你写的所有代码最终都要经过编译器转换成机器指令因此编译器的正确性直接关系到整个系统的安全。如果编译器被恶意修改它编译出来的所有程序都可能被植入后门。Ken Thompson 在其成名演讲《Reflections on Trusting Trust》中就展示过一个著名攻击在一开始的 C 编译器中植入后门这个编译器再去编译其他版本的编译器后门就能一代代“遗传”下去并且源码里完全看不出异常。这是自举带来的一个极其重要的警示自举会建立一条信任链链条的起点必须被严格信任。现代编译器工程非常重视 reproducible build可复现构建即使不同编译器生成的二进制不一定相同也要保证同一工具链在同一输入下可以稳定复现一致结果。这也是为什么我们在验证自举时要对比二进制。3. 自举的基本原理3.1 从一个最小的编译器开始很多人以为编译器的自举是从零到一“瞬间”完成的实际完全不是。真实的开发过程非常类似“滚雪球”先用某种已有的语言编写一个很小的编译器这个编译器只支持该语言的一个子集然后用这个子集编译器去编译一个功能更完整的编译器源码再用新得到的编译器去编译下一个版本如此迭代下去。这个作为“起点”的小编译器通常被称为 stage0、bootstrap compiler 或引导编译器。比如你想让一门叫 miniC 的类 C 语言实现自举可以先不管指针、结构体、union 这些高级特性只用现有 C 编译器编写一个“只支持整数、变量、加减乘除和 if 语句”的 miniC 编译器。它编译范围很窄但足够让自己动起来之后每扩展一个特性都要保证新特性也可以被当前编译器解析和编译。这个过程层层递进最后才达到完整的自举。3.2 三层递进C0、C1、C2理解自举最好的方式是把编译器分代看。为了描述清楚我们假设目标语言是 L宿主平台上可用的编译器是 M0。第一阶段用已经存在的语言例如 C 语言编写一个最小编译器 C0。C0 能读取 L 语言的子集源码并生成可以在 M0 上运行的机器代码。C0 本身是 C 程序由操作系统现有的 C 编译器编译。第二阶段用 L 语言编写一个功能更完整的编译器 C1。C1 的源码是 L 语言写的所以不能直接运行。此时用 C0 去编译 C1 的源码得到可运行的 C1 可执行文件。这一步非常关键C1 是一个 L 语言开发的编译器但由 C0 加工成了可执行文件。第三阶段C1 可执行文件能编译 L 语言源码。于是把 C1 自己的源码再交给 C1 编译一遍会得到一个“由 L 编译器自己编译出来的 L 编译器”。此时就完成了初步自举。之后可以不断用 C1 编译新的 C2替换旧版本螺旋上升。这个流程的核心思想是你不需要凭空拥有一个 L 编译器只需要有一个能处理 L 子集的引导编译器剩下的工作都可以慢慢由语言自身承担。3.3 用 T 型图理解自举编译器领域常用 T 型图T-diagram直观表达“一个编译器到底在做什么”。简单来说一个编译器可以用三个要素描述运行在什么平台上能编译什么语言生成什么样的目标代码。画成文字示意大致如下源码语言 目标语言 ┌─────────┐ ┌─────────┐ │ L 源码 │ → │ 机器码 │ └─────────┘ └─────────┘ ↑ 由编译器处理 运行平台M自举过程可以理解为目标语言 L 要“追上”实现语言。第一代编译器是用 C 写的即“用 C 语言描述编译逻辑运行在 M 上生成 L 目标代码”到了全自举阶段就变成“用 L 语言描述编译逻辑运行在 M 上生成 L 目标代码”。两者之间的差别恰恰是那个被替换掉的实现语言。T 型图虽然画起来简单但能很好地解释为什么先前需要一层“引导”。4. 常见的自举实现路线4.1 路线一跨语言起步完成重写式自举这条路线是最常见的。以 Rust 为例最初版本的 rustc 编译器并不是用 Rust 写的而是用 OCaml 写的。用 OCaml 完成第一版后开发者再不断把 OCaml 实现移植成 Rust 实现直到新的 rustc 完全由 Rust 编写并可以用旧 rustc 编译新 rustc 的源码。Go 也是类似Go 1.5 之前编译器主要由 C 编写为了完成自举团队先用一些辅助工具把 C 写的编译器翻译成 Go再用 Go 1.4 的编译器来编译 Go 1.5 的编译器。这种路线的优点是可以利用成熟语言快速搭出第一版风险低缺点是存在一次“大重构”——把实现语言从宿主语言换成目标语言。重构期间需要保持编译结果一致工程量大。4.2 路线二用旧版本编译新版本逐步迭代自举一旦完成后续开发就进入非常舒服的迭代模式。假设当前可用的编译器是 V1.0它运行正常能编译自己的源码。现在你想开发 V1.1那么流程是先在 V1.0 源码仓库中修改代码得到 V1.1 源码然后用 V1.0 可执行文件编译 V1.1 源码得到 V1.1 可执行文件最后用 V1.1 可执行文件重新编译一遍 V1.1 源码检查两轮产物是否一致。每一版发布时都要保留“上一版本能编译下一版本”的能力。这就是经典的 bootstrap 迭代策略。这种策略看似简单实际非常稳健。它保证任何时刻都存在一个已经能运行的编译器同时新的编译器一旦编译成功就可以立即投入验证。很多主流编译器的开发流程本质上都是这种模式。4.3 路线三从“能编译子集”到“能编译全部特性”这条路线更强调语言特性的增长过程。写一个自举编译器时有一个很隐晦的坑编译器源码只能用目标语言已经实现的语法来写。如果你的语言还不支持数组编译器源码里就不能出现数组如果你还没实现结构体编译器源码里就不能用结构体。否则当前编译器就编译不了自己的源码。因此语言特性和编译器源码需要“并行生长”。可以先在语言里支持整数和加减乘除写一个只能处理整数运算的编译器再给语言加上字符串和数组编译器源码就用字符串和数组来重构词法分析器再支持结构体和指针如果你希望编译器最终用结构体管理语法树节点就必须先让语言具备结构体能力。这个过程非常考验语言设计者的全局规划也正因如此自举编译器常被认为是最有挑战性的编译器练习之一。5. 动手验证让一个编译器自己编译自己5.1 自举验证的通用流程理解了原理我们可以总结一个可执行的自举验证流程。假设我们有一个编译器项目它的源码写在文件compiler.c中这个编译器能编译标准 C 代码。我们可以通过四个步骤验证它是否具备自举能力用系统自带的编译器如 GCC编译compiler.c得到第一代可执行文件first_boot用first_boot去编译compiler.c得到第二代可执行文件second_boot用second_boot再次编译compiler.c得到第三代可执行文件third_boot对比second_boot与third_boot。如果两者一致说明这个工具链已经稳定自举成立。用命令表示大致如下# 1. 使用系统编译器生成第一代编译器 gcc compiler.c -o first_boot # 2. 使用第一代编译器编译自身的源码 ./first_boot compiler.c -o second_boot # 3. 使用第二代编译器再次编译同一份源码 ./second_boot compiler.c -o third_boot # 4. 比较两轮产物是否一致 cmp second_boot third_boot如果你的终端里cmp命令没有任何输出并且退出码是 0就说明second_boot和third_boot在二进制层面完全一致。可以再执行echo $?查看退出码。5.2 为什么比较的是 second_boot 和 third_boot很多初学者刚看到这个流程时会提出一个问题为什么不比较 first_boot 和 second_boot原因在于 first_boot 是系统编译器比如 GCC生成的而 second_boot 是第一个版本的编译器生成的。两者的“父编译器”不同即使输入源码相同二进制也可能因为编译器内部的代码生成策略、优化选项、链接顺序不同而产生差异这并不能说明失败。但 second_boot 和 third_boot 就不同了second_boot 编译compiler.c生成 third_boot而 third_boot 同样要编译compiler.c这一次生成指令的编译器和输入都是完全相同的。如果两次结果不一致说明编译器可能存在不确定性或者在某些边界情况下行为不一致。如果两次结果完全一致则说明这个编译器系统已经稳定可以进入自我更新循环。从工程角度理解这等价于“自己编译自己还要保证后代和自己是同一个样子”。这正是自举的含金量所在。5.3 用 Python 封装一次自举验证脚本如果你在上面流程的项目环境中可以把四个步骤写成一个脚本来执行。这里给出一个示意脚本它把命令顺序封装起来方便反复验证。注意实际命令要根据你选择的编译器项目调整参数这里重点是展示流程。#!/usr/bin/env python3 import subprocess import sys COMPILER_SRC compiler.c FIRST_BOOT first_boot SECOND_BOOT second_boot THIRD_BOOT third_boot def run(cmd: str) - None: print(f$ {cmd}) subprocess.run(cmd, shellTrue, checkTrue) def main(): run(fgcc {COMPILER_SRC} -o {FIRST_BOOT}) run(f./{FIRST_BOOT} {COMPILER_SRC} -o {SECOND_BOOT}) run(f./{SECOND_BOOT} {COMPILER_SRC} -o {THIRD_BOOT}) print(\n[1] compare second_boot and third_boot ...) result subprocess.run( fcmp {SECOND_BOOT} {THIRD_BOOT}, shellTrue, ) if result.returncode 0: print([OK] bootstrap verification passed: two binaries are identical.) else: print([FAIL] binaries differ, please check your compiler behavior.) sys.exit(1) if __name__ __main__: main()这段脚本的本质是把“生成第一代编译器、自编译、再次自编译、对比二进制”四步自动化。你在少数真实项目例如 TCC、chibicc 这类小型 C 编译器上可以尝试类似思路但需要先熟悉对应项目的构建方式再改写这里的命令。5.4 为什么很多项目不直接对比二进制也能自举你可能会发现很多真实项目的构建脚本并没有那么严格地对比二进制。原因主要有两点。第一二进制对比是一个非常苛刻的标准。编译器如果携带了时间戳、随机哈希、编译器绝对路径等元信息即使逻辑完全一致二进制也可能不同。因此很多项目只要求“同一源码用新编译器能重新编出自己并且通过全部测试”。测试通过比二进制一致更能说明功能正确性。第二对比两代产物是否一致本质上是在验证编译器的确定性。对于现代编译器来说非确定性问题通常被视为 bug因此也会刻意去避免。但在教学和轻量级项目中对比二进制仍然是理解自举最直观的方式。5.5 代码层面一个简化版编译器骨架为了更具体地感受“编译器源码也是一段普通代码”下面用 Python 展示一个极简解析器的骨架。它虽然不是一个真正的自举编译器但展示了编译器源码的基本形态读取源码、拆成 token、解析结构、生成输出。# file: mini_parser.py import re TOKEN_RE re.compile(r \s | (?PNUM\d) | (?PPLUS\) | (?PMINUS-) | (?PSTAR\*) | (?PLPAREN\() | (?PRPAREN\)) , re.VERBOSE) def tokenize(src: str): pos 0 while pos len(src): match TOKEN_RE.match(src, pos) if not match: raise SyntaxError(funexpected char at {pos}: {src[pos]!r}) pos match.end() if match.lastgroup is not None: yield (match.lastgroup, match.group())真正的编译器会在 tokenize 之后继续做语法分析和代码生成过程会比这个复杂很多。但重点在于这段源码一旦经过编译或解释它就能处理字符串和正则表达式而一个能处理字符串和表达式的语言已经具备编写小型编译器的基本条件。自举编译器项目正是从这些最小功能开始的。6. 真实世界中的自举案例6.1 Go 语言从 C 到 Go 的经典自举Go 语言的自举过程在研究编译器自举时非常值得参考。早期 Go 编译器 gc 是用 C 写的也就是说要编译 Go 程序必须先有一个能编译 C 代码的工具链。Go 团队为了摆脱这种依赖决定在 Go 1.5 版本中把编译器全部重写为 Go 代码。当时的问题在于Go 1.5 的编译器是用 Go 写的但当时还没有足够成熟的 Go 编译器能直接编译它。于是 1.4 成为了一个特殊版本这是最后一个用 C 编写的 Go 编译器。后续流程大致是先用 C 编译器构建 Go 1.4再用 Go 1.4 编译 Go 1.5 的 Go 源码得到 Go 1.5 编译器。从那个版本之后Go 再也不需要 C 编译器参与编译自身了。这就是一个典型的“旧版本编译新版本”的引导过程。6.2 Rust 语言从 OCaml 到 Rust 的重写式自举Rust 的 rustc 编译器最初使用 OCaml 实现。当时 Rust 语言尚处于设计早期用 OCaml 写编译器可以快速验证语法和语义设计。随着语言逐渐稳定社区开始用 Rust 重写 rustc最终形成现在完全使用 Rust 编写的 rustc。Rust 目前的引导策略是每一个新版本的 rustc 都由上一版本的 rustc 编译产生。因此发行版中会包含一个兼容旧版 Rust 的 bootstrap 套件用来从源码重建新编译器。这个过程比较长但已经成为 Rust 发布流程的一部分。它也说明一旦自举链建立起来语言生态就能“自我繁殖”。6.3 主流 C 编译器天生具有自举能力GCC、Clang、TCC 这类 C 编译器因为实现语言本身主要就是 C/C所以天然具备自举条件。如果你手头有 GCC 和一份 GCC 源码理论上可以用系统 GCC 编译 GCC 源码得到一份新的 GCC然后用新 GCC 再编译一次相同源码对比产物。很多发行版的构建流程正是如此。不过由于 GCC 体积庞大、构建配置复杂初学者实际操作时很容易迷路因此更推荐从小型 C 编译器入手。TCC 和 chibicc 都是很好的学习材料前者体积小且能完整编译 C 语言后者单文件结构清晰适合反复阅读源码。7. 常见问题与误区7.1 高频问题速查表问题现象常见原因解决思路编译器能编译自己但还是有 bug自举只说明编译器自身系统稳定不代表功能正确通过回归测试、对比测试验证编译结果的正确性第一次编译器是怎么来的编译器无法凭空诞生需要先用更底层的语言实现早期用汇编/机器码现代用已有语言实现引导编译器“用 C 编译器编译 C 编译器”是不是循环论证第一次编译依赖外部编译器不是循环论证用外部编译器生成第一版之后用第一版自举自举后二进制对比不一致编译器可能带时间戳、路径或存在非确定性行为关闭时间戳相关内容或改为比较测试结果编译器自举和自举电容有什么关系两者只是中文同名原理完全不同区分 bootstrap self-hosting 与 bootstrap capacitor7.2 自举成功是否说明编译器没有 bug绝对不能这样理解。自举成功只能说明这个编译器在“生成自身代码”这个特定场景下是稳定的不能说明它能正确编译所有 C 程序。一个编译器完全可能带着一个隐蔽 bug 完成自举因为那个 bug 影响的语法特性可能恰好不出现在编译器源码中或者 bug 在源码和编译产物中都以同样方式体现结果反而“稳定”地错下去。所以真实编译器项目会把自举和测试结合起来。除了自举还要准备大量测试用例包括编译器自身生成的汇编代码与参考编译器输出的一致性对比。自举只是起点不是终点。7.3 第一个编译器到底是怎么来的理解自举最常问的一句话就是既然编译器需要编译器编译那第一个编译器哪来的答案是第一个编译器不需要被编译它是直接由人使用更底层的工具写出来的。在计算机发展早期人们先用机器码和汇编语言编写很小的汇编器或编译器再通过手工汇编把程序变成机器码。后来随着编译器越来越复杂人们才开始用已存在的编译器作为引导去构建功能更强的新编译器。换句话说编译器历史不是一条永远向上的自举链条它有一个可追溯的起点这个起点是人用最低级的指令手工搭建出来的。自举解决的是“编译器如何维护自己”而不是“编译器如何凭空诞生”。7.4 编译器、编辑器与嵌入式工具链初学阶段很多人会把编辑器和编译器混为一谈。编辑器editor是写代码的软件比如 VS Code、Notepad它不负责把代码变成可执行程序编译器和解释器才负责语法分析和代码生成。像“编译器错误信息 cs1056: 意外的字符”这类报错通常是源码中引入了非法字符比如中文标点、不可见字符或者字符串引号不匹配。这是使用编译器时遇到的词法错误和编译器自举没有直接关系。嵌入式开发里经常提到的 Keil AC5/AC6 编译器也类似它们是针对 ARM 平台的具体工具链版本属于“选择哪款编译器”的工程问题而不是编译器开发中的自举问题。初学自举时不要被这些同名或相近的词干扰抓住核心自举关注的是编译器能否编译自身源码。8. 最佳实践与工程建议8.1 学习建议从解释器到自举编译器如果你是第一次接触编译器开发不建议直接挑战“写一个能自举的 C 编译器”。比较合理的学习路线是先写一个简单的表达式解释器然后实现一门前端小语言再把解释器逐步改造成独立编译器。等到你对词法分析、语法分析和代码生成都有基本概念后再去研究自举会轻松得多。具体到练习可以先设计一门极简语言只支持整数、加法、减法、变量和 print。用 Python 或 C 写出第一版编译器然后为这门语言增加字符串、数组和函数支持最后试着把编译器源码重写成目标语言。整个过程建议分阶段保存 git commit以便随时回退和对比。8.2 自举项目的工程管理经验做自举项目时最容易被忽视的是“编译器源码只能用当前已支持语法编写”这条约束。每给语言增加一个新特性都要问自己编译器源码是否已经开始依赖这个特性如果是就必须保证生成这个代码的老版本编译器能正确解析它。否则就会出现“新语法已经写进源码但老编译器根本读不懂”的尴尬局面。建议给自举项目编写自动化测试至少包含三类第一用系统编译器编译目标语言编译器源码并运行功能测试第二用目标语言编译器编译自身源码第三对比两轮编译产物或测试结果。每次提交代码后都执行这套流程可以有效防止自举能力在开发过程中“悄悄失效”。8.3 安全视角建立可信的自举链由于编译器的特殊地位一旦自举链起点被污染后续所有编译器都会继承这个污染而且很难发现。因此现代编译器开发应当把安全作为重要考量。至少做到两点第一从可信渠道获取引导编译器和源码验证源码的校验和或签名第二尽量保证构建过程可复现避免同一源码在不同时间打包出不同二进制。如果你参与开源编译器项目还应当关注上游版本更新和已知漏洞不要长期停留在某个存在安全问题的编译器版本上。8.4 编译器优化要从自举之后开始很多初学者一上来就研究各种复杂的编译优化其实没必要。自举阶段首先应该保证正确性而不是性能。先用极简方式生成明显正确的汇编代码完成可运行的自举闭环再逐步引入中间表示IR、寄存器分配、指令选择等优化技术。一个能自举但编译速度慢的编译器比一个编译速度快但经常出错的编译器更适合学习因为前者更容易定位错误。当你开始优化时也会发现自举带来的额外好处每次优化改动后都可以用当前编译器重新编译自身。编译自身的过程本身就是一次大型压力测试任何优化错误都会在这个测试中暴露出来。这种“自用自测”的方式是理解编译器优化重要且实用的入口。9. 结尾编译器自举是一个很容易被误解、却很值得深入理解的概念。它回答了“编译器由谁编译”的问题也展示了一个语言生态从依赖到独立的过程。理解自举不需要立刻去读几十万行 GCC 源码只要抓住引导编译器、分代编译、二进制对比这三个核心点就能建立起完整的认知框架。如果你正在学习编译原理不妨给自己定一个小目标写一门极简语言然后让这个语言的编译器编译自身源码。哪怕只支持整数和加法这个过程也会让你对编译器、语言设计和工程迭代的理解上升一个台阶。等这条路真正走通之后你会发现编译器的神秘感消失了剩下的都是可以被拆解、被验证、被维护的工程问题。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门