czhao-dev/llvm-c-compiler-toolchain
GitHub: czhao-dev/llvm-c-compiler-toolchain
一个从零构建的迷你 C 工具链,涵盖预处理、linter、静态分析、编译、链接和 Makefile 构建工具,用于学习和探索编译工具链的完整工作原理。
Stars: 1 | Forks: 0
# llvm-c-compiler-toolchain
[](https://github.com/czhao-dev/llvm-c-compiler-toolchain/actions/workflows/testing-suite.yml)
[](https://github.com/czhao-dev/llvm-c-compiler-toolchain/actions/workflows/c-preprocessor.yml)
[](https://github.com/czhao-dev/llvm-c-compiler-toolchain/actions/workflows/c-linter.yml)
[](https://github.com/czhao-dev/llvm-c-compiler-toolchain/actions/workflows/c-static-analyzer.yml)
[](https://github.com/czhao-dev/llvm-c-compiler-toolchain/actions/workflows/c-compiler.yml)
[](https://github.com/czhao-dev/llvm-c-compiler-toolchain/actions/workflows/c-linker.yml)
[](https://github.com/czhao-dev/llvm-c-compiler-toolchain/actions/workflows/build-tool.yml)
[](LICENSE)
一个从头开始、逐步构建的小型 C 工具链:包含预处理器、风格 linter、静态分析器、编译器、链接器和构建工具。
每个子项目都是独立且自包含的——拥有自己的语言、构建系统、测试和 README——但它们组合在一起涵盖了从源代码到最终构建的完整路径:检查代码、编译、构建。
## 架构
源代码从左到右流经 pipeline;`c-linter` 和
`c-static-analyzer` 作为护栏,在相同的预处理后的源代码上运行,而不是阻断流程;
`build-tool` 则是基于 Makefile 的编排层,
可以将这些阶段中的任何一个串联到构建规则中:
```
flowchart LR
SRC(["Source .c"]) --> PP["c-preprocessor
macro expansion, includes"] PP --> LINT["c-linter
style guardrails"] PP --> SA["c-static-analyzer
semantic guardrails"] PP --> CC["c-compiler
MiniC → LLVM IR → native"] CC --> LK["c-linker
ELF64 relocation + link"] LK --> EXE(["Executable"]) BT[["build-tool
Makefile-driven orchestration"]] -.-> PP BT -.-> CC BT -.-> LK ``` ## 项目 按逻辑 pipeline 顺序列出——首先是预处理,然后是两个源码级别的 护栏,接着是编译和链接,最后是 `build-tool`, 因为它是用来统筹其他工具的,而不处于线性流程中: | 项目 | 语言 | 描述 | |---|---|---| | [c-preprocessor](c-preprocessor/README.md) | C++20 | 一个最小化的 C 预处理器:`#include` 文件包含,具有 hide-set 安全递归展开的类对象 `#define`/`#undef` 宏,以及 `//`/`/* */` 注释剔除。类函数宏、条件编译和 `##`/`#` 明确不在目标范围内——它们都会被判定为严重错误,而不是静默无效。 | | [c-linter](c-linter/README.md) | C++20 | 一个针对 C 的风格/格式 linter:snake_case 命名、行长限制(80 列)和行尾空格检查、比较运算中的魔术数字检测,以及 K&R/Allman 大括号风格一致性检查。仅做报告——不自动修复,也不做语义检查(那是 c-static-analyzer 的工作)。 | | [c-static-analyzer](c-static-analyzer/README.md) | C++20 | 一个针对 C 代码的轻量级静态分析器。使用 tree-sitter 解析 `.c`/`.h` 文件(无需编译),并报告有关圈复杂度、未使用的变量、嵌套深度、缺失返回值、不可达代码以及(新增)使用未初始化变量的诊断信息。 | | [c-compiler](c-compiler/README.md) | C++20 / LLVM | **MiniC** —— 一个针对 C 静态类型子集的编译器。包含手写的词法分析器、递归下降解析器、语义分析器,以及生成原生二进制文件的 LLVM IR 代码生成器,并针对 clang 进行了交叉验证。 | | [c-linker](c-linker/README.md) | C++20 | 一个用于真实 ELF64 x86-64 目标文件的静态链接器:跨多个 `.o` 文件合并 `.text`/`.data` 段,解析符号(检测未定义符号和重复定义),应用 `Abs64`/`Pc32` 重定位修正,并输出一个真实、可运行的静态 ELF 可执行文件。不支持动态链接、归档解析或 LTO。 | | [build-tool](build-tool/README.md) | C++20 | 一个具备依赖图感知能力的构建工具,实现了核心的 GNU Make 语义。将 Makefile 解析为拓扑有序的计划,检查基于 mtime 的过期状态,并通过循环检测和 `-k`/`--keep-going` 支持串行执行构建规则。 | ## 核心亮点 **c-preprocessor** —— 一个四阶段 pipeline(注释剥离器 → 指令/include 行驱动器 → 分词器 → 基于 hide-set 的宏重扫描器),构建于 `libpp_core` 之上,在配置阶段不需要任何外部依赖(CLI11 作为 CLI 层的依赖被直接放在代码树中,而不是通过外部拉取)。`#include` 会相对于*包含它的*文件所在目录解析双引号路径,然后依次检索 `-I` 指定的目录;循环包含会被检测到并报告完整的链条,而菱形包含被特意不去重,因为不存在 include guard。递归宏展开(一个宏的替换内容可以引用另一个宏,例如 `TWO_PI` → `PI * 2` → `3 * 2`)通过标准的“蓝色油漆”(blue paint)hide-set 算法,在自引用和互相递归定义时能够正确终止——展开宏 `M` 所产生的每个 token 都会在其 hide set 中携带 `M`,因此如果某个标识符已经存在于其自身的 hide set 中,它将被按原样输出而不是无限循环,这与真实的 `cpp` 在处理如 `#define X X + 1` 这类输入时的行为完全一致,而不会直接报错。重定义宏时以最后一次为准(last-wins)且不产生任何诊断信息,这是相较于严格 C 语言要求的“相同重定义”规则的一种刻意简化,而 `#undef` 则自然而然地获得了正确的后期绑定特性,因为宏主体是作为原始的、未展开的 token 存储的。注释剥离会用相同数量的换行符替换多行块注释,因此贯穿其中的诊断行号依然能保持准确。类函数宏、条件编译(`#ifdef`/`#if`)以及 `##`/`#` 明确不在目标范围内——每一项都会触发严重的 `file:line` 编译错误,而不是成为静默无效的操作,并且 CLI 本身的退出码(`0` 成功,`1` 预处理/I/O 错误,`2` 用法错误)遵循该工具链中所有其他工具相同的惯例。全部 7 个测试套件均通过,包括针对多文件示例的逐字节黄金输出比对,以及在子进程级别对 CLI 的全面测试。 **c-linter** —— 五条规则(`CL001`–`CL005`),涵盖 snake_case 命名、行长限制(默认 80 列)、行尾空格、比较运算中的魔术数字以及 K&R/Allman 大括号风格一致性检查,构建于一个小型手写的词法分析器之上,该分析器特意不与 `c-compiler` 共享,从而根据本仓库的惯例保持子项目的独立性。词法分析器在设计上是宽容的——未闭合的注释/字面量和未建模的标点符号会回落到通用的 token 类型中,而不是直接报错,因为 linter 必须处理它无法完全建模的真实世界中可能存在破损的 C 代码——而且关键字识别也刻意保持最小化:只有 `if`/`while` 是独立的 token,其他所有关键字都被词法分析为普通的标识符,这是安全的,因为所有真实的 C 关键字都是小写的,且不包含嵌入的大写字母,所以 CL001 绝不会在它们身上误报。命名检查(CL001)是纯粹的 token 级别检查,没有符号表,因此命名不当的标识符的每一次*出现*都会被标记,而不仅仅是它的声明;魔术数字检测(CL004)豁免了 `0`、`1` 和 `-1` 这些常见的哨兵值,并且只从比较运算符开始向前查找,而不向后查找;大括号风格检查(CL005)通过嵌套括号匹配 `if`/`while` 条件的右括号,并将其与后续大括号的位置进行比对。行长和行尾空格检查甚至在分词发生之前就作为纯文本处理流程运行。全部 8 个测试套件均通过。仅做报告——不自动修复,不追踪缩进,也不做语义检查(该边界归属于 `c-static-analyzer`)——具有对 CI 友好的退出码(`0`/`1`/`2`)。 **c-static-analyzer** —— 六条规则(`SA001`–`SA006`),涵盖圈复杂度、未使用的变量、控制流嵌套深度、非穷尽的返回路径、`return`/`break`/`continue`/`goto` 之后的不可达代码,以及在变量被写入之前读取局部变量的情况,直接基于 tree-sitter 的 C API 和 `tree-sitter-c` 语法构建(通过 CMake `FetchContent` 获取并编译为普通的 C 静态库,刻意绕过了该语法自带的构建系统,而是直接编译其预生成的 `parser.c`——这是唯一一个带有获取依赖的子项目)。最后三条规则(`SA004`–`SA006`)运行在按函数体(if/else、带有正确无限循环检测的 while/do-while/for、switch/case fallthrough、break/continue、goto/labels)构建的真实控制流图(CFG)上,而不是临时的 AST 模式匹配:缺失返回值是关于 exit 块的可达性,不可达代码是从 entry 块无法到达的任何块,而未初始化变量检查则是从每个声明处开始的“可能未初始化”的前向数据流分析——这与真实编译器的未初始化变量警告采取的形式相同,能够捕获单次文本处理在结构上无法捕捉的对分支敏感的情况(例如,仅在部分路径上初始化的变量)。文件发现功能默认会跳过常见的非项目目录(`.git`、`build`、`dist`、`vendor`、`third_party` 等),并且行为可以通过 CLI 标志或检索到的 `.c-static-analyzer.toml` 文件进行配置(规则选择、复杂度/嵌套阈值、排除 glob),CLI 标志的优先级始终高于配置文件。配置发现机制会从扫描的工作目录开始向上遍历祖先目录,找到的第一个文件即为最终结果(即使该文件解析失败,也会应用默认设置,而不会继续向上查找);无法读取的输入文件会产生一个合成的、未注册的 `SA000` 诊断信息,而不是终止整个扫描。按照设计,添加一条新规则只需要修改两个文件(一个实现 `Rule` 接口的头文件,外加一行注册代码)。13 个测试套件均通过,包括一个独立的 CFG 构建套件、针对专门设计以同时触发所有规则的测试用例进行的逐字节黄金输出比对,以及一个验证退出码契约的子进程 CLI 测试(`0` 正常,`1` 发现问题,`2` 用法错误)。 **MiniC 编译器 (c-compiler)** —— 一个完整的四阶段 pipeline(词法分析器 → 递归下降解析器 → 语义分析器 → LLVM IR 生成器),编译为隐藏在轻量级 CLI 背后的 `libminic_core.a`,带有 `--emit-tokens`/`--emit-ast`/`--emit-ir` 标志以公开每个阶段的输出供检查。该语言的子集涵盖 `int`/`float`/`char`/`void`、指针(包括取地址/解引用和空指针比较)、带有指针退化(pointer decay)的定长数组、具有一等整体值赋值能力的命名 struct/union/enum、完整的算术/比较/逻辑/按位/三元/复合赋值运算符集合,以及 `if`/`while`/`for`/`do`-`while`/带有真实 fallthrough 的 `switch`/`goto`/labels —— 递归函数也能正常工作,因为 IR 生成器能够正确解析前向引用。语义分析器能以精确的 `file:line:col` 诊断信息捕获未声明的标识符、类型不匹配、参数数量错误以及返回类型不匹配(采用单行 `file:line:col: error: message` 格式——没有源代码片段或插入符号渲染;该功能在 `c-lint`/`c-static-analyzer` 中是通过 `--show-source` 按需启用的,请参阅[测试与性能](#testing--performance))。全部五个测试套件均通过,并且所有九个示例程序(fibonacci、fizzbuzz、gcd、pointer swap、array sum、struct point、bit ops、control flow、sum of squares)编译后产生的输出与 `clang` 编译相同源代码的输出在字节上完全一致。一项有针对性的正确审查发现并修复了四个真实的 bug——赋值诊断顺序错误、写入失败时的临时文件泄漏、缺失的浮点指数词法分析 (`1e5`),以及针对 `char` 的一元负号运算在 sema/codegen 阶段的类型分歧——每个 bug 都通过回归用例进行了验证。`-O1`/`-O2`/`-O3` 运行 LLVM 真实的新 pass manager pipeline(`mem2reg`、`instcombine`、`simplifycfg`、`tailcallelim`、循环展开);`-O2` 使得 `fibonacci(40)` 相比 `-O0` 获得了实测 1.4 倍的加速,并将其尾递归转换为循环。语言覆盖范围本身是通过五个按依赖顺序排列的层级扩展的——首先是指针,然后是数组(从指针退化而来),接着是 struct/union/enum(重用 pointer/GEP 机制),然后是扩展的运算符集合,最后是剩余的控制流形式——每个阶段落地时都带有各自的测试和示例程序;数值转换 (`(int)`/`(float)`/`(char)`) 紧随其后。`sizeof`、存储限定符(`const`/`static`/`extern`)、指针/聚合类型转换以及函数原型/指针/真正的可变参数仍不受支持。`volatile` 明确不在目标范围内(涉及并发的范围蔓延),并会被解析器通过针对性的诊断信息拒绝,而不是被静默接受。 **c-linker** —— 通过将指针强制转换为少量放在本地的 ELF64 结构子集来解析真实的 ELF64 x86-64 可重定位目标文件(`ET_REL`,与 `clang -c` 产生的输出相同,macOS 系统没有提供 ``,因此该链接器自行定义了它实际需要读写的少数几个字段),而不是凭空发明一种玩具格式。对于输入文件中的每个 section,仅读取 `.text`、`.data`、`.symtab`/`.strtab` 和 `.rela.text`/`.rela.data` —— 其他所有内容(`.rodata`、`.bss`、`.eh_frame`、`.debug_*` 等)均通过名称进行查找,如果未找到则被静默丢弃。整个 pipeline 会读取每个目标文件,在构建全局符号表的同时标记重复定义的冲突,检查每个重定位引用的符号是否确实在某个地方得到了解析,按顺序合并所有输入文件的 `.text` 和 `.data` 段并带有逐文件的内存对齐填充,使用真实的 x86-64 psABI 公式原地修补 `Abs64` (`write64(S+A)`) 和 `Pc32`/`Plt32` (`write32(S+A-P)`) 重定位位置,并输出一个真实的、最小化的、可加载的静态 ELF 可执行文件——包含两个页面对齐的 `PT_LOAD` segment,运行时不需要 section header——可在 x86-64 Linux 上直接执行,并已通过 `readelf` 独立验证。不支持动态链接(无 PLT/GOT)、归档 (`.a`) 解析、链接时优化或 `STB_WEAK` 符号。每个诊断信息都是 `Severity::Error`——静态链接要么完全成功,要么完全失败,没有警告级别——而 CLI 用法错误(缺少 `-o`、无输入、无法识别的标志)属于单独的路径,始终以 `2` 退出,有别于链接诊断退出的错误码 `1`。全部 6 个测试套件均通过,每一个都使用了由 `clang --target=x86_64-unknown-linux-gnu` 针对独立测试夹具即时编译的真实 `.o` 文件,而不是提交入库的二进制文件,其中包含一项重定位测试,该测试针对真实编译的重定位手工验证了修补数学公式,以及一个虚构的超范围用例,它产生了一个干净的 `RelocationOverflow` 错误,而不是导致字节损坏。
**build-tool** —— 将 Makefile 解析为规则(显式先决条件、制表符缩进的配方、在规则之前或之后的 `.PHONY` 声明、行内注释),通过单次递归的深度优先遍历将它们解析为依赖图,这既能检测循环,又能顺带产生有效的拓扑排序(节点的先决条件总是在节点本身之前被解析,因此不需要单独的调度过程),通过 mtime 过期检查跳过最新的目标,并以快速失败或 `-k`/`--keep-going` 语义串行运行未完成的配方。记忆化保证了菱形结构中的共享先决条件只会被构建一次。`-j` 并行处理是刻意不在目标范围内的:它只影响挂钟构建速度,而不影响正确性,因此这个小工具牺牲了它,换来了一个简单得多的单线程执行器,没有线程池或工作窃取队列。变量展开(`$(VAR)`、自动变量)、模式规则(`%.o: %.c`)和内置的 Make 函数同样不在范围内,此外还有 `include` 指令、`ifdef`/`ifeq` 条件判断、`VPATH`、仅限顺序的先决条件(`|`)、静态模式规则、双冒号规则和特定于目标的变量——`-n`(试运行)和 `-f`(Makefile 路径覆盖)仅在命令行被识别,这样它们就可以通过明确的错误被拒绝,而不是被静默地误读为目标名称。这精确地实现了 Make 的核心心智模型(目标、先决条件、配方、过期状态),而不是大范围地近似实现。三个套件(Makefile 解析、依赖规划和涵盖干净的重运行和触发式重新构建的完整二进制端到端调用)中的 22 个测试均通过,它是位于 pipeline 其余部分之上的编排层——Makefile 可以将其他五个工具中的任何一个作为配方串联起来。
## 文档
每个子项目自己的 `docs/` 目录是该 pipeline 阶段的权威参考——上面的段落只是进行了总结,而这些文档明确规定了支持和不支持的内容,包括语法、算法和诊断格式:
| 项目 | 权威规范 | 附加文档 |
|---|---|---|
| [c-preprocessor](c-preprocessor/README.md) | [docs/SPEC.md](c-preprocessor/docs/SPEC.md) —— 指令语法、hide-set 宏语义、`#include` 解析、CLI/错误格式参考 | — |
| [c-linter](c-linter/README.md) | [docs/SPEC.md](c-linter/docs/SPEC.md) —— CL001–CL005 规则语义、词法分析器宽容策略、豁免理由 | — |
| [c-static-analyzer](c-static-analyzer/README.md) | [docs/SPEC.md](c-static-analyzer/docs/SPEC.md) —— SA001–SA006 规则语义、配置文件发现、glob 匹配规则 | — |
| [c-compiler](c-compiler/README.md) | [docs/language_spec.md](c-compiler/docs/language_spec.md) —— 完整的 BNF 语法、类型系统、隐式转换表 | [docs/ir_walkthrough.md](c-compiler/docs/ir_walkthrough.md) —— 针对两个示例程序的带有注释的 `-O0`/`-O2` LLVM IR |
| [c-linker](c-linker/README.md) | [docs/SPEC.md](c-linker/docs/SPEC.md) —— ELF64 段/符号模型、重定位修补公式、诊断/退出码表 | — |
| [build-tool](build-tool/README.md) | [docs/SPEC.md](build-tool/docs/SPEC.md) —— Makefile 语法子集、过期/循环/快速失败语义、明确的非目标范围 | — |
## 测试与性能
每个子项目自己的 `tests/` 保持在针对该工具的单元/集成测试范围内
(参见其 README 的测试部分)。一个单独的顶层
[testing/](testing/README.md) 目录存放了本质上属于横切关注点并跨越多个子项目的套件:
- **差异化测试** —— 使用 `minic` 和 `clang` 编译相同的 MiniC 源代码,
运行这两个二进制文件,并针对运算符优先级、控制流、指针和
多函数程序,逐字节比较 stdout 和退出代码。
- **负面/快照测试** —— 针对
专门设计用来触发其护栏的测试夹具运行 `c-lint` 和 `c-static-analyzer`
(snake_case 命名违规、被新的 `SA006` 规则捕获的初始化前读取),并将输出与冻结的黄金测试夹具进行比较。
- **基准测试** —— 由 [hyperfine](https://github.com/sharkdp/hyperfine) 驱动,针对三种算法(埃拉托斯特尼筛法、递归 Fibonacci、最坏情况冒泡排序)在 `minic` 和 `clang` 之间进行编译时和执行时的比较。
- **延伸集成冒烟测试** —— 使用仓库自带的 `c-link` 链接一个程序,在整个链接步骤中完全不需要使用 clang。
`c-lint` 和 `c-static-analyzer` 还增加了一个 `--show-source` 标志
(按需开启,默认输出不变),该标志会在任何诊断信息下打印出违规的源代码行
及其下方的插入符号。
在按照[入门指南](#getting-started)构建了相关子项目之后,可以在本地运行这些套件:
```
python3 testing/differential/run_differential_tests.py
python3 testing/invalid/run_negative_tests.py
python3 testing/benchmarks/run_benchmarks.py --output testing/benchmarks/results.json # requires hyperfine
```
有关详细分解,请参阅 [testing/README.md](testing/README.md),
包括此次探索发现并记录而不是修复的两个 `c-compiler` 现有真实 bug:针对任何带有 `int **` 参数的函数的 `-O2` 代码生成 bug
([testing/differential/cases/03_pointers.mc](testing/differential/cases/03_pointers.mc)),
以及嵌套在另一个循环中的循环,在超过中等迭代次数后会在运行时崩溃,并且在 `-O2` 编译时会发生无限挂起
([testing/benchmarks/programs/sieve.mc](testing/benchmarks/programs/sieve.mc))。
### 基准测试结果
**主要发现:**
- **在 `-O2` 级别,所有三个程序的执行速度都与 clang 非常接近**
(在 ±8% 以内)——这是符合预期的,因为 `-O1`–`-O3` 运行的是 LLVM
真实的全新 pass manager pipeline(`mem2reg`、`instcombine`、
`simplifycfg`、尾调用消除、循环展开),而不是
手写的近似替代品。
- **在 `-O0` 级别情况则比较复杂**:未优化的 `bubble_sort` 运行速度
慢了 73%,而未优化的 `fibonacci` 的运行速度却比 clang 自己的 `-O0`
输出*快*了 20%——这提醒我们,“没有优化器”并不意味着
“代码生成方式相同”,这一点值得进一步深入研究,
而不是简单地用平均值敷衍过去。
- **在这款硬件上,编译速度通常比 clang 慢**,而
不是更快——在 `bubble_sort -O2` 上最高慢了约 19 倍。这些是真实的测量数据,而不是编译器项目 README 中通常喜欢宣称的
“快了 N 倍”,因为手写的数据在代码发生更改时很容易变得
过时。
- **构建此基准测试套件暴露了 `c-compiler` 中两个真实的现有 bug**,
而以前没有任何测试发现它们(上面描述的 `-O2`/`int **` 和
嵌套循环 bug),这些已在发现时被记录下来——完整报告请参阅
[testing/README.md](testing/README.md)。发现真实的 bug 正是测试基础设施的意义所在;
完美的健康报告反而会是相对无趣的结果。
#### 执行时间 —— 算法挑战
| 算法 | 优化 | minic | clang | 差值 |
|---|---|---:|---:|---|
| 冒泡排序 (5,000 个元素,逆序) | `-O0` | 56.8 ms | 32.8 ms | 慢 73% |
| 冒泡排序 (5,000 个元素,逆序) | `-O2` | 8.7 ms | 9.4 ms | **快 7%** |
| Fibonacci(39),朴素递归 | `-O0` | 219.3 ms | 275.4 ms | **快 20%** |
| Fibonacci(39),朴素递归 | `-O2` | 157.6 ms | 161.6 ms | **快 2%** |
| 埃拉托斯特尼筛法 (N=40,000) | `-O0` | 2.7 ms | 2.5 ms | 慢 8% |
| 埃拉托斯特尼筛法 (N=40,000) | `-O2` | — | 1.4 ms | 已知 bug,见下文 |
#### 编译时间 —— 工具链竞速
| 算法 | 优化 | minic | clang | 差值 |
|---|---|---:|---:|---|
| 冒泡排序 | `-O0` | 51.5 ms | 32.2 ms | 慢 1.6 倍 |
| 冒泡排序 | `-O2` | 710.5 ms | 37.3 ms | 慢 190 倍 |
| Fibonacci(39) | `-O0` | 41.9 ms | 31.6 ms | 慢 1.3 倍 |
| Fibonacci(39) | `-O2` | 46.1 ms | 34.8 ms | 慢 1.3 倍 |
| 埃拉托斯特尼筛法 | `-O0` | 157.1 ms | 31.8 ms | 慢 4.9 倍 |
| 埃拉托斯特尼筛法 | `-O2` | — (挂起) | 41.6 ms | 已知 bug,见下文 |


在 `sieve -O2` 处缺失的 `minic` 条形图/数据行是上文提到的编译时挂起
bug,这里将其呈现为 `N/A` 而不是静默丢弃——
`run_benchmarks.py` 使用外部超时限制了每一次测量,
并将该组合记录为失败,而不是导致整个套件挂起。
**方法论:** 每个数据点都是一个
[hyperfine](https://github.com/sharkdp/hyperfine) 的平均值——执行方面进行了 10 次
测量运行(3 次预热),编译方面进行了 3 次
测量运行(1 次预热);确切的调用方式请参见 `testing/benchmarks/run_benchmarks.py`。
测量环境为 Apple M3 (macOS 26.5.1)、Apple clang 21.0.0、
hyperfine 1.20.0——这只是单台机器的快照,并非严格的多重试
统计研究,因此应将这些百分比视作方向性参考,
而不是精确到两位有效数字的准确值。
这些数据和图表是手动重新生成的
(`python3 testing/benchmarks/run_benchmarks.py --output
testing/benchmarks/results.json && python3
testing/benchmarks/plot.py testing/benchmarks/results.json`),并
随普通的代码更改一起提交,而不是由 CI 自动提交;有关
全新运行的数据,请参阅
[testing-suite workflow](https://github.com/czhao-dev/llvm-c-compiler-toolchain/actions/workflows/testing-suite.yml) 的任务摘要和上传的制品。
## 入门指南
每个项目都是独立构建的——详情请参阅其各自的 README。
```
# c-preprocessor (C++20/CMake,在 configure 阶段无外部依赖;CLI11 vendored in-tree)
cd c-preprocessor && ./scripts/configure.sh && cmake --build build
ctest --test-dir build --output-on-failure
# c-linter (C++20/CMake,在 configure 阶段无外部依赖;CLI11 vendored in-tree)
cd c-linter && ./scripts/configure.sh && cmake --build build
ctest --test-dir build --output-on-failure
# C 静态分析器 (C++20/CMake,通过 FetchContent 获取 tree-sitter + tree-sitter-c;CLI11 vendored in-tree)
cd c-static-analyzer && ./scripts/configure.sh && cmake --build build
ctest --test-dir build --output-on-failure
# MiniC 编译器 (C++20/CMake,需要 LLVM 17+;CLI11 vendored in-tree)
cd c-compiler && ./scripts/configure.sh && cmake --build build
ctest --test-dir build --output-on-failure
# c-linker (C++20/CMake,构建 test/example fixtures 需要在 PATH 中存在 clang;CLI11 vendored in-tree)
cd c-linker && ./scripts/configure.sh && cmake --build build
ctest --test-dir build --output-on-failure
# build-tool (C++20/CMake,在 configure 阶段无外部依赖;CLI11 vendored in-tree)
cd build-tool && ./scripts/configure.sh && cmake --build build
ctest --test-dir build --output-on-failure
```
## 参考文献
每个子项目都在自己的 README 中引用了特定于其 pipeline 阶段的来源
——请参阅
[c-preprocessor](c-preprocessor/README.md#references)、
[c-linter](c-linter/README.md#references)、
[c-static-analyzer](c-static-analyzer/README.md#references)、
[c-compiler](c-compiler/README.md#references) 和
[c-linker](c-linker/README.md#references)。在工具链
级别:
- ISO/IEC 9899:2018. *Programming Languages — C* (C17 标准) ——
这些子项目所实现的各个部分(预处理、编译和
翻译单元语义)的权威参考。
- Kernighan, Brian W. 和 Ritchie, Dennis M. *The C Programming Language*
(第 2 版). Prentice Hall, 1988. —— 该工具链端到端处理的
语言的权威描述。
- Aho, Lam, Sethi, Ullman. *Compilers: Principles, Techniques, and Tools*
(第 2 版,“龙书”). Addison-Wesley, 2006. —— 工具链 pipeline
(预处理、编译、分析、构建)整体形态的
标准参考。
- Levine, John R. *Linkers and Loaders*. Morgan Kaufmann, 2000. ——
`c-linker`(该工具链 pipeline 的最后阶段)的主要参考:
符号解析、段合并和重定位处理。
## 许可证
每个子项目均使用 MIT 授权;请参阅本目录及各子项目中的 `LICENSE` 文件。
macro expansion, includes"] PP --> LINT["c-linter
style guardrails"] PP --> SA["c-static-analyzer
semantic guardrails"] PP --> CC["c-compiler
MiniC → LLVM IR → native"] CC --> LK["c-linker
ELF64 relocation + link"] LK --> EXE(["Executable"]) BT[["build-tool
Makefile-driven orchestration"]] -.-> PP BT -.-> CC BT -.-> LK ``` ## 项目 按逻辑 pipeline 顺序列出——首先是预处理,然后是两个源码级别的 护栏,接着是编译和链接,最后是 `build-tool`, 因为它是用来统筹其他工具的,而不处于线性流程中: | 项目 | 语言 | 描述 | |---|---|---| | [c-preprocessor](c-preprocessor/README.md) | C++20 | 一个最小化的 C 预处理器:`#include` 文件包含,具有 hide-set 安全递归展开的类对象 `#define`/`#undef` 宏,以及 `//`/`/* */` 注释剔除。类函数宏、条件编译和 `##`/`#` 明确不在目标范围内——它们都会被判定为严重错误,而不是静默无效。 | | [c-linter](c-linter/README.md) | C++20 | 一个针对 C 的风格/格式 linter:snake_case 命名、行长限制(80 列)和行尾空格检查、比较运算中的魔术数字检测,以及 K&R/Allman 大括号风格一致性检查。仅做报告——不自动修复,也不做语义检查(那是 c-static-analyzer 的工作)。 | | [c-static-analyzer](c-static-analyzer/README.md) | C++20 | 一个针对 C 代码的轻量级静态分析器。使用 tree-sitter 解析 `.c`/`.h` 文件(无需编译),并报告有关圈复杂度、未使用的变量、嵌套深度、缺失返回值、不可达代码以及(新增)使用未初始化变量的诊断信息。 | | [c-compiler](c-compiler/README.md) | C++20 / LLVM | **MiniC** —— 一个针对 C 静态类型子集的编译器。包含手写的词法分析器、递归下降解析器、语义分析器,以及生成原生二进制文件的 LLVM IR 代码生成器,并针对 clang 进行了交叉验证。 | | [c-linker](c-linker/README.md) | C++20 | 一个用于真实 ELF64 x86-64 目标文件的静态链接器:跨多个 `.o` 文件合并 `.text`/`.data` 段,解析符号(检测未定义符号和重复定义),应用 `Abs64`/`Pc32` 重定位修正,并输出一个真实、可运行的静态 ELF 可执行文件。不支持动态链接、归档解析或 LTO。 | | [build-tool](build-tool/README.md) | C++20 | 一个具备依赖图感知能力的构建工具,实现了核心的 GNU Make 语义。将 Makefile 解析为拓扑有序的计划,检查基于 mtime 的过期状态,并通过循环检测和 `-k`/`--keep-going` 支持串行执行构建规则。 | ## 核心亮点 **c-preprocessor** —— 一个四阶段 pipeline(注释剥离器 → 指令/include 行驱动器 → 分词器 → 基于 hide-set 的宏重扫描器),构建于 `libpp_core` 之上,在配置阶段不需要任何外部依赖(CLI11 作为 CLI 层的依赖被直接放在代码树中,而不是通过外部拉取)。`#include` 会相对于*包含它的*文件所在目录解析双引号路径,然后依次检索 `-I` 指定的目录;循环包含会被检测到并报告完整的链条,而菱形包含被特意不去重,因为不存在 include guard。递归宏展开(一个宏的替换内容可以引用另一个宏,例如 `TWO_PI` → `PI * 2` → `3 * 2`)通过标准的“蓝色油漆”(blue paint)hide-set 算法,在自引用和互相递归定义时能够正确终止——展开宏 `M` 所产生的每个 token 都会在其 hide set 中携带 `M`,因此如果某个标识符已经存在于其自身的 hide set 中,它将被按原样输出而不是无限循环,这与真实的 `cpp` 在处理如 `#define X X + 1` 这类输入时的行为完全一致,而不会直接报错。重定义宏时以最后一次为准(last-wins)且不产生任何诊断信息,这是相较于严格 C 语言要求的“相同重定义”规则的一种刻意简化,而 `#undef` 则自然而然地获得了正确的后期绑定特性,因为宏主体是作为原始的、未展开的 token 存储的。注释剥离会用相同数量的换行符替换多行块注释,因此贯穿其中的诊断行号依然能保持准确。类函数宏、条件编译(`#ifdef`/`#if`)以及 `##`/`#` 明确不在目标范围内——每一项都会触发严重的 `file:line` 编译错误,而不是成为静默无效的操作,并且 CLI 本身的退出码(`0` 成功,`1` 预处理/I/O 错误,`2` 用法错误)遵循该工具链中所有其他工具相同的惯例。全部 7 个测试套件均通过,包括针对多文件示例的逐字节黄金输出比对,以及在子进程级别对 CLI 的全面测试。 **c-linter** —— 五条规则(`CL001`–`CL005`),涵盖 snake_case 命名、行长限制(默认 80 列)、行尾空格、比较运算中的魔术数字以及 K&R/Allman 大括号风格一致性检查,构建于一个小型手写的词法分析器之上,该分析器特意不与 `c-compiler` 共享,从而根据本仓库的惯例保持子项目的独立性。词法分析器在设计上是宽容的——未闭合的注释/字面量和未建模的标点符号会回落到通用的 token 类型中,而不是直接报错,因为 linter 必须处理它无法完全建模的真实世界中可能存在破损的 C 代码——而且关键字识别也刻意保持最小化:只有 `if`/`while` 是独立的 token,其他所有关键字都被词法分析为普通的标识符,这是安全的,因为所有真实的 C 关键字都是小写的,且不包含嵌入的大写字母,所以 CL001 绝不会在它们身上误报。命名检查(CL001)是纯粹的 token 级别检查,没有符号表,因此命名不当的标识符的每一次*出现*都会被标记,而不仅仅是它的声明;魔术数字检测(CL004)豁免了 `0`、`1` 和 `-1` 这些常见的哨兵值,并且只从比较运算符开始向前查找,而不向后查找;大括号风格检查(CL005)通过嵌套括号匹配 `if`/`while` 条件的右括号,并将其与后续大括号的位置进行比对。行长和行尾空格检查甚至在分词发生之前就作为纯文本处理流程运行。全部 8 个测试套件均通过。仅做报告——不自动修复,不追踪缩进,也不做语义检查(该边界归属于 `c-static-analyzer`)——具有对 CI 友好的退出码(`0`/`1`/`2`)。 **c-static-analyzer** —— 六条规则(`SA001`–`SA006`),涵盖圈复杂度、未使用的变量、控制流嵌套深度、非穷尽的返回路径、`return`/`break`/`continue`/`goto` 之后的不可达代码,以及在变量被写入之前读取局部变量的情况,直接基于 tree-sitter 的 C API 和 `tree-sitter-c` 语法构建(通过 CMake `FetchContent` 获取并编译为普通的 C 静态库,刻意绕过了该语法自带的构建系统,而是直接编译其预生成的 `parser.c`——这是唯一一个带有获取依赖的子项目)。最后三条规则(`SA004`–`SA006`)运行在按函数体(if/else、带有正确无限循环检测的 while/do-while/for、switch/case fallthrough、break/continue、goto/labels)构建的真实控制流图(CFG)上,而不是临时的 AST 模式匹配:缺失返回值是关于 exit 块的可达性,不可达代码是从 entry 块无法到达的任何块,而未初始化变量检查则是从每个声明处开始的“可能未初始化”的前向数据流分析——这与真实编译器的未初始化变量警告采取的形式相同,能够捕获单次文本处理在结构上无法捕捉的对分支敏感的情况(例如,仅在部分路径上初始化的变量)。文件发现功能默认会跳过常见的非项目目录(`.git`、`build`、`dist`、`vendor`、`third_party` 等),并且行为可以通过 CLI 标志或检索到的 `.c-static-analyzer.toml` 文件进行配置(规则选择、复杂度/嵌套阈值、排除 glob),CLI 标志的优先级始终高于配置文件。配置发现机制会从扫描的工作目录开始向上遍历祖先目录,找到的第一个文件即为最终结果(即使该文件解析失败,也会应用默认设置,而不会继续向上查找);无法读取的输入文件会产生一个合成的、未注册的 `SA000` 诊断信息,而不是终止整个扫描。按照设计,添加一条新规则只需要修改两个文件(一个实现 `Rule` 接口的头文件,外加一行注册代码)。13 个测试套件均通过,包括一个独立的 CFG 构建套件、针对专门设计以同时触发所有规则的测试用例进行的逐字节黄金输出比对,以及一个验证退出码契约的子进程 CLI 测试(`0` 正常,`1` 发现问题,`2` 用法错误)。 **MiniC 编译器 (c-compiler)** —— 一个完整的四阶段 pipeline(词法分析器 → 递归下降解析器 → 语义分析器 → LLVM IR 生成器),编译为隐藏在轻量级 CLI 背后的 `libminic_core.a`,带有 `--emit-tokens`/`--emit-ast`/`--emit-ir` 标志以公开每个阶段的输出供检查。该语言的子集涵盖 `int`/`float`/`char`/`void`、指针(包括取地址/解引用和空指针比较)、带有指针退化(pointer decay)的定长数组、具有一等整体值赋值能力的命名 struct/union/enum、完整的算术/比较/逻辑/按位/三元/复合赋值运算符集合,以及 `if`/`while`/`for`/`do`-`while`/带有真实 fallthrough 的 `switch`/`goto`/labels —— 递归函数也能正常工作,因为 IR 生成器能够正确解析前向引用。语义分析器能以精确的 `file:line:col` 诊断信息捕获未声明的标识符、类型不匹配、参数数量错误以及返回类型不匹配(采用单行 `file:line:col: error: message` 格式——没有源代码片段或插入符号渲染;该功能在 `c-lint`/`c-static-analyzer` 中是通过 `--show-source` 按需启用的,请参阅[测试与性能](#testing--performance))。全部五个测试套件均通过,并且所有九个示例程序(fibonacci、fizzbuzz、gcd、pointer swap、array sum、struct point、bit ops、control flow、sum of squares)编译后产生的输出与 `clang` 编译相同源代码的输出在字节上完全一致。一项有针对性的正确审查发现并修复了四个真实的 bug——赋值诊断顺序错误、写入失败时的临时文件泄漏、缺失的浮点指数词法分析 (`1e5`),以及针对 `char` 的一元负号运算在 sema/codegen 阶段的类型分歧——每个 bug 都通过回归用例进行了验证。`-O1`/`-O2`/`-O3` 运行 LLVM 真实的新 pass manager pipeline(`mem2reg`、`instcombine`、`simplifycfg`、`tailcallelim`、循环展开);`-O2` 使得 `fibonacci(40)` 相比 `-O0` 获得了实测 1.4 倍的加速,并将其尾递归转换为循环。语言覆盖范围本身是通过五个按依赖顺序排列的层级扩展的——首先是指针,然后是数组(从指针退化而来),接着是 struct/union/enum(重用 pointer/GEP 机制),然后是扩展的运算符集合,最后是剩余的控制流形式——每个阶段落地时都带有各自的测试和示例程序;数值转换 (`(int)`/`(float)`/`(char)`) 紧随其后。`sizeof`、存储限定符(`const`/`static`/`extern`)、指针/聚合类型转换以及函数原型/指针/真正的可变参数仍不受支持。`volatile` 明确不在目标范围内(涉及并发的范围蔓延),并会被解析器通过针对性的诊断信息拒绝,而不是被静默接受。 **c-linker** —— 通过将指针强制转换为少量放在本地的 ELF64 结构子集来解析真实的 ELF64 x86-64 可重定位目标文件(`ET_REL`,与 `clang -c` 产生的输出相同,macOS 系统没有提供 `
标签:Bash脚本, LLVM, 云安全监控, 开发工具链, 编译器, 静态分析