最近在调研大量c代码,人工效率比较低,感觉能用c编译器,实现自动生成代码、自动检查代码…都需要一个基础:c编译器。
https://bellard.org/tcc/
这个是我目前准备使用的编译器,因为"With libtcc, you can use TCC as a backend for dynamic code generation."
查阅tcc相关资料时,把我引到了8cc(9cc chibicc)等工程,凭感觉,可以从更简单的chibicc入手。
https://github.com/rui314/chibicc
文档:https://www.sigbus.info/compilerbook#
作者说他的代码是配合书写的,代码提交历史记录就是逐步的开发过程(功能逐步增加),作者建议按照commit记录来学习。
我计划先翻译文档,然后根据代码记录逐步描述chibicc的开发过程。
第一条commit记录:https://github.com/rui314/chibicc/commits/main/?after=90d1f7f199cc55b13c7fdb5839d1409806633fdb+314
https://github.com/rui314/8cc
除了流行的编译器书籍,如《龙书》之外,我发现以下书籍/文档对于开发C编译器非常有用。请注意,标准草案版本与正式版本非常接近。您实际上可以将它们用作标准文档。
LCC: 一个可重定位的C编译器:设计与实现 http://www.amazon.com/dp/0805316701,https://github.com/drh/lcc
TCC:Tiny C Compiler http://bellard.org/tcc/,http://repo.or.cz/w/tinycc.git/tree
C99标准最终草案 http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf
C11标准最终草案 http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
Dave Prosser的C预处理算法 http://www.spinellis.gr/blog/20060626/
x86-64 ABI http://www.x86-64.org/documentation/abi.pdf
https://github.com/rui314/chibicc/blob/main/README.md
(旧版已移至 historical/old 分支。这是 2020 年 9 月上传的新版本。)
chibicc 是又一个实现了大多数 C11 特性的小型 C 编译器。尽管它可能仍然属于“玩具编译器”类别,就像其他小型编译器一样,但 chibicc 可以编译几个真实世界的程序,包括 Git、SQLite、libpng 和 chibicc 本身,而无需对这些程序进行修改。生成的这些程序的可执行文件通过了各自的测试套件。因此,chibicc 实际上支持广泛的 C11 特性,并且能够正确编译数十万行真实世界的 C 代码。
chibicc 被开发为我目前正在撰写的一本关于 C 编译器和低级编程的参考实现。这本书采用了渐进式的方法来涵盖广泛的主题;在第一章中,读者将实现一个只接受一个数字作为“语言”的“编译器”,然后在书的每个部分中逐步添加功能,直到编译器接受的语言与 C11 规范指定的语言相匹配。我从 Abdulaziz Ghuloum 的论文中采用了这种渐进式方法。
该项目的每个提交都对应于书中的一个章节。出于这个目的,不仅项目的最终状态,而且每个提交都经过了精心编写,考虑到了可读性。读者应该能够通过阅读本项目的一个或几个提交来了解如何实现 C 语言特性。例如,这是 while、[]、?: 和线程局部变量是如何实现的。如果你有充足的空闲时间,从第一个提交开始阅读可能会很有趣。
如果你喜欢这个项目,请考虑在书籍发布时购买一本!我在这里发布源代码是为了让人们提前获得它,因为我计划在发布书籍后以一种开放且自由的许可证进行发布。如果我不收费提供源代码,对我来说将其保持私有并没有太多意义。我希望在 2021 年出版这本书。你可以在这里注册,以便在网上免费提供一章内容或书籍发布时收到通知。
我把 chibicc 的发音读作 chee bee cee cee。“chibi”在日语中意为“迷你”或“小”。“cc”代表 C 编译器。
chibicc 几乎支持 C11 的所有强制性特性以及大多数可选特性,还支持一些 GCC 语言扩展。
在小型编译器中经常缺少但被 chibicc 支持的特性包括(但不限于):
预处理器
float、double 和 long double(x87 80 位浮点数)
位域
alloca()
可变长度数组
复合字面量
线程局部变量
原子变量
共享符号
指定初始化项
L、u、U 和 u8 字符串字面量
根据 x86-64 SystemV ABI 指定的以结构体作为值传递或返回的函数
chibicc 不支持复数、K&R 风格的函数原型和 GCC 风格的内联汇编。故意不包含双字符和三字符。
当在源代码中发现错误时,chibicc 会输出简单但友好的错误消息。
没有优化步骤。chibicc 生成的代码非常糟糕,可能比 GCC 的输出慢两倍或更多。一旦前端完成,我计划添加一个优化步骤。
我正在使用 Ubuntu 20.04 的 x86-64 作为开发平台。我做了一些小的更改,使 chibicc 在 Ubuntu 18.04、Fedora 32 和 Gentoo 2.6 上工作,但目前可移植性不是我的目标。除了 Ubuntu 20.04,它可能在其他系统上工作,也可能不工作。
chibicc 包含以下几个阶段:
词法分析:词法分析器接受一个字符串作为输入,将其分解为一个标记列表并返回。
预处理:预处理器接受一个标记列表作为输入,并输出一个新的宏扩展标记列表。它在展开宏的同时解释预处理指令。
解析:递归下降解析器从预处理器的输出中构建抽象语法树。它还为每个 AST 节点添加了类型。
代码生成:代码生成器为给定的 AST 节点生成汇编文本。
当我发现这个编译器中的一个 bug 时,我会回到引入 bug 的原始提交,并重新编写提交历史,仿佛从一开始就没有这样的 bug。这是修复 bug 的一种不寻常的方式,但作为一本书的一部分,保持每个提交都没有 bug 是很重要的。
因此,我不接受此存储库中的拉取请求。如果您发现了一个 bug,您可以向我发送一个拉取请求,但我很可能会阅读您的补丁,然后将其应用到我之前的提交中,通过重写历史。我会在某个地方致谢您的名字,但您的更改将由我在提交到此存储库之前进行重写。
另外,请假设我会偶尔将我的本地仓库强制推送到这个公共仓库以重写历史。如果您克隆了这个项目并在其上进行了本地提交,那么当我强制推送新的提交时,您的更改将不得不手动进行重新基础操作。
chibicc 的核心价值在于其简洁性和源代码的可读性。为了实现这个目标,在编写代码时,我小心翼翼地避免过于巧妙。让我解释一下这是什么意思。
通常,当你习惯了代码库时,你会受到诱惑,想要使用更多的抽象和聪明的技巧来改进代码。但这种改进并不总是提高第一次读者的可读性,实际上有时会降低可读性。我尽量避免这种陷阱。我写这段代码不是为了自己,而是为了第一次读者。
如果你看一下源代码,你会发现一些看起来很蠢的代码片段。这些是有意这样写的(但在某些地方我可能确实遗漏了一些东西)。以下是一些显著的例子:
递归下降解析器包含许多看起来相似的函数,用于处理看起来相似的生成式语法规则。你可能会被诱惑去通过使用高阶函数或宏来减少重复,但我认为那太复杂了。最好是允许一些小的重复。
chibicc 不会过于努力节省内存。例如,在分词器开始工作之前,整个输入源文件会首先被读入内存。
如果我们知道 n 不会太大,那么慢速算法是可以接受的。例如,在预处理器中,我们使用链表作为集合,因此成员检查的时间复杂度是 O(n),其中 n 是集合的大小。但这没关系,因为我们知道 n 通常是非常小的。即使 n 可能很大,我仍然坚持使用简单的慢速算法,直到通过基准测试证明它是瓶颈为止。
每种 AST 节点类型只使用 Node 结构的几个成员。其他未使用的 Node 成员在运行时只是内存的浪费。我们可以使用联合来节省内存,但我决定简单地将所有内容放在同一个结构中。我相信这种效率损失是可以忽略不计的。即使这很重要,我们随时都可以更改代码以使用联合。我想避免过早优化。
chibicc 总是使用 calloc 分配堆内存,它是 malloc 的一种变体,用零清除内存。calloc 稍微慢一些,但应该是可以忽略的。
最后但并非最不重要的是,chibicc 使用 calloc 分配内存,但从不调用 free。分配的堆内存在进程退出之前不会被释放。我确信这种内存管理策略(或者说缺乏策略)看起来非常奇怪,但对于像编译器这样的短暂程序是有道理的。例如,D 编程语言的编译器 DMD 也出于同样的原因使用相同的内存管理方案 [1]。
我是植山瑠偉。我是 8cc 的创作者,这是一个业余爱好的 C 编译器,也是 LLVM lld 链接器当前版本的原始创作者,它是各种操作系统和大型构建系统使用的生产级链接器。
tcc:由法布里斯·贝拉尔德编写的一个小型 C 编译器。我从这个编译器中学到了很多,但是 tcc 和 chibicc 的设计是不同的。特别是,tcc 是一种单通道编译器,而 chibicc 是一种多通道编译器。
https://bellard.org/tcc/
lcc:另一个小型 C 编译器。创作者写了一本关于 lcc 内部的书,我发现这是一个很好的资源,可以了解编译器是如何实现的。
https://github.com/drh/lcc
https://sites.google.com/site/lccretargetablecompiler/
《逐步构建编译器的方法》
https://blog.csdn.net/weixin_43172531/article/details/136125185
罗布·派克的编程五定律
https://blog.csdn.net/weixin_43172531/article/details/136125027
[1] https://www.drdobbs.com/cpp/increasing-compiler-speed-by-over-75/240158941
DMD 对内存分配的方式有点隐秘。由于编译器是短暂的程序,速度至关重要,因此 DMD 只是不停地分配内存,从不释放。