C89cc.sh —— 纯可移植 Shell 编写的独立 C89/ELF64 编译器

C89cc.sh —— 纯可移植 Shell 编写的独立 C89/ELF64 编译器

Hacker News 摘要

原标题:C89cc.sh – standalone C89/ELF64 compiler in pure portable shell

c89cc.sh 是一个由 Alexandre Gomes Gaigalas 开发的独立 C89 编译器,完全使用可移植的 Shell 脚本编写。该项目托管在 GitHubGist 上,其主要功能是将符合 C89 标准的源代码解析并编译为适用于 x86-64 架构的 ELF64 可执行文件。

主要功能与用途

该脚本的核心是一个纯 Shell 实现的编译器,能够处理 C89 语法。它的主要运行方式是通过标准输入接收 C 代码,并将生成的二进制内容输出。用户可以通过运行 sh c89cc.sh < prog.c > a.out 来完成编译。此外,它还提供了一个 --no-libc 选项,用于在编译时跳过内置的 libc 库。整个项目采用 ISC 许可协议发布。

脚本核心结构

脚本的起始部分进行了严格的环境配置,以确保在不同环境下具有良好的兼容性:

环境初始化:通过 set -euf 开启严格模式,禁止未定义变量并禁用路径名扩展。设置 LC_ALL=C 以确保字符处理的一致性。

兼容性处理:脚本能够识别并适应多种 Shell,如 BashZshkshmksh。例如,如果环境不支持 local 关键字,它会自动降级使用 typeset

路径清理:脚本在开始时会清空 PATH 变量,这意味着它在运行过程中不依赖任何外部系统命令,体现了极高的独立性。

输出优化:内置了输出辅助函数,能根据系统环境自动选择性能最优的打印指令,优先级依次为 printfprint、最后是 echo

核心模块详述

脚本内部由多个功能模块组成,共同完成从源码字符串到二进制文件的转换:

字符串处理模块:包含高效的字符串重复函数 _repeat(采用平方求幂法)、大小写转换函数 _ucase_lcase_str,以及代替系统 cat 命令的多行读取函数 _readall

解析器位置追踪:通过 _nlcount 函数实时计算消耗掉的字符串中的换行符,从而精准更新行号 _LN 和列号 _COL,用于错误报告。

抽象语法树(AST)引擎

输入缓冲:脚本使用一个名为 CODE 的缓冲区。为了处理极长的输入行,它会将超过 128 个字符的行切分为数据块再进行处理。

字符操作:通过一系列 alias 宏实现的 ast_consumeast_skip 等操作,可以移动缓冲区指针并提取字符。

栈管理:利用 ast_pushast_pop 维护节点栈,构建嵌套的语法树结构。它还具备节点合并功能,可以简化无谓的一对一节点包裹。

C89 解析状态机:解析器包含极其详尽的状态定义,涵盖了从文件主体、基本类型(如 intcharvoidlong)、存储限定符(如 staticexterntypedef)到复杂结构(如 structunionenum)的所有语法。

运算符优先级处理:脚本定义了 12 个优先级等级,涵盖了从赋值、逻辑或到一元运算符的全部 C 语言运算符逻辑。它通过一种攀爬式算法处理表达式的优先级嵌套关系。

词法与语法分析细节

代码中定义了大量的状态转换逻辑。例如,状态 C45 用于处理字符串常量,C46 处理字符常量,C47 处理数字,C48 处理标识符。

它能够识别并跳过常见的 C 语言块注释 /* ... */,并对各种预处理行(如 # 开头的行)进行基础解析。通过对字符的逐个调度,解析器可以区分变量声明、函数定义、数组声明以及各种控制流语句(如 ifwhilefordo-whileswitch 等)。此外,针对常用的关键字,如 sizeofreturnbreakcontinue 等,解析器都有专门的分支进行处理。

编译器的最后阶段会将解析出的抽象语法树转换为 ELF64 格式的数据。这个过程同样完全由 Shell 脚本内的位运算和二进制拼接逻辑完成,不借助任何如 gccas 之类的外部工具链。


原文:https://gist.github.com/alganet/2b89c4368f8d23d033961d8a3deb5c19

评论:https://news.ycombinator.com/item?id=47598413

Report Page