C89cc.sh —— 纯可移植 Shell 编写的独立 C89/ELF64 编译器
Hacker News 摘要原标题:C89cc.sh – standalone C89/ELF64 compiler in pure portable shell
c89cc.sh 是一个由 Alexandre Gomes Gaigalas 开发的独立 C89 编译器,完全使用可移植的 Shell 脚本编写。该项目托管在 GitHub 的 Gist 上,其主要功能是将符合 C89 标准的源代码解析并编译为适用于 x86-64 架构的 ELF64 可执行文件。
主要功能与用途
该脚本的核心是一个纯 Shell 实现的编译器,能够处理 C89 语法。它的主要运行方式是通过标准输入接收 C 代码,并将生成的二进制内容输出。用户可以通过运行 sh c89cc.sh < prog.c > a.out 来完成编译。此外,它还提供了一个 --no-libc 选项,用于在编译时跳过内置的 libc 库。整个项目采用 ISC 许可协议发布。
脚本核心结构
脚本的起始部分进行了严格的环境配置,以确保在不同环境下具有良好的兼容性:
• 环境初始化:通过 set -euf 开启严格模式,禁止未定义变量并禁用路径名扩展。设置 LC_ALL=C 以确保字符处理的一致性。
• 兼容性处理:脚本能够识别并适应多种 Shell,如 Bash、Zsh、ksh 和 mksh。例如,如果环境不支持 local 关键字,它会自动降级使用 typeset。
• 路径清理:脚本在开始时会清空 PATH 变量,这意味着它在运行过程中不依赖任何外部系统命令,体现了极高的独立性。
• 输出优化:内置了输出辅助函数,能根据系统环境自动选择性能最优的打印指令,优先级依次为 printf、print、最后是 echo。
核心模块详述
脚本内部由多个功能模块组成,共同完成从源码字符串到二进制文件的转换:
• 字符串处理模块:包含高效的字符串重复函数 _repeat(采用平方求幂法)、大小写转换函数 _ucase 和 _lcase_str,以及代替系统 cat 命令的多行读取函数 _readall。
• 解析器位置追踪:通过 _nlcount 函数实时计算消耗掉的字符串中的换行符,从而精准更新行号 _LN 和列号 _COL,用于错误报告。
• 抽象语法树(AST)引擎:
• 输入缓冲:脚本使用一个名为 CODE 的缓冲区。为了处理极长的输入行,它会将超过 128 个字符的行切分为数据块再进行处理。
• 字符操作:通过一系列 alias 宏实现的 ast_consume 和 ast_skip 等操作,可以移动缓冲区指针并提取字符。
• 栈管理:利用 ast_push 和 ast_pop 维护节点栈,构建嵌套的语法树结构。它还具备节点合并功能,可以简化无谓的一对一节点包裹。
• C89 解析状态机:解析器包含极其详尽的状态定义,涵盖了从文件主体、基本类型(如 int、char、void、long)、存储限定符(如 static、extern、typedef)到复杂结构(如 struct、union、enum)的所有语法。
• 运算符优先级处理:脚本定义了 12 个优先级等级,涵盖了从赋值、逻辑或到一元运算符的全部 C 语言运算符逻辑。它通过一种攀爬式算法处理表达式的优先级嵌套关系。
词法与语法分析细节
代码中定义了大量的状态转换逻辑。例如,状态 C45 用于处理字符串常量,C46 处理字符常量,C47 处理数字,C48 处理标识符。
它能够识别并跳过常见的 C 语言块注释 /* ... */,并对各种预处理行(如 # 开头的行)进行基础解析。通过对字符的逐个调度,解析器可以区分变量声明、函数定义、数组声明以及各种控制流语句(如 if、while、for、do-while、switch 等)。此外,针对常用的关键字,如 sizeof、return、break、continue 等,解析器都有专门的分支进行处理。
编译器的最后阶段会将解析出的抽象语法树转换为 ELF64 格式的数据。这个过程同样完全由 Shell 脚本内的位运算和二进制拼接逻辑完成,不借助任何如 gcc 或 as 之类的外部工具链。
原文:https://gist.github.com/alganet/2b89c4368f8d23d033961d8a3deb5c19