DependableC 的作者 Eskil Steenberg Hald 在网站里反复强调一句话:
“只要你不触发未定义行为,C 就是世界上最安全的语言。”
道理都懂,但人眼会犯错。本文把 DependableC 的 “人类守则” 翻译成 “机器守则”—— 用编译器插件在提交前就把内存与并发缺陷钉死。整套工具链只依赖 Clang/LLVM 现有 API,不造新轮子,方便你在现有 CI 里 3 分钟落地。
1. 为什么 DependableC 需要插件化静态分析
DependableC 的核心是 “最小可移植子集”:
- 语法层面:只承认 C89 + 少量 C99(如
//注释),禁止 VLAs、复数、 Annex K。 - 语义层面:零容忍 UB—— 不允许任何 Effective Type 违规、指针算术越界、数据竞争。
人工代码审查无法规模化,尤其面对祖传宏魔法。把规则搬进编译器插件,好处:
- 路径敏感:CSA(Clang Static Analyzer)引擎已帮你做完抽象解释,直接复用。
- 零误报可配置:通过
.dependablec_suppress文件给特定函数 / 宏打标,避免 “狼来了”。 - 增量检查:基于编译数据库(
compile_commands.json)只扫改动的 TU,大型仓库 10 秒级反馈。
2. 插件架构总览:AST→CSA→Taint→Report 四步流水线
┌--------------┐
AST 前端 ----▶│DependableC │---┐
(Clang Plugin)│Checkers │ │
└--------------┘ ▼
CSA 引擎 ------------------▶ 路径敏感分析
┌--------------┐ ▼
Taint 模块--▶│标签传播 │---┘
└--------------┘
┌--------------┐
Report 生成--▶│SARIF/终端 │
└--------------┘
- AST 前端:注册
DependableCChecker继承Checker<check::PreCall, check::PostCall, check::DeadSymbols>。 - CSA 引擎:利用 CoreEngine 的 ExplodedGraph,自动做跨函数路径剪枝。
- Taint 模块:对
malloc返回的MemRegion打标签,后续遇到free时校验标签是否匹配。 - Report 生成:默认输出 SARIF 2.1,GitHub/CodeQL 可直接识别;终端模式给本地开发用。
3. 内存缺陷规则:把 UB 变成「机器可判」
| 规则 | 触发场景 | 实现要点 | 参数示例 |
|---|---|---|---|
| double-free | free(p); free(p); |
给 p 绑定 Freed 标签,二次调用时报错 |
标签存活域与 SymbolRef 生命周期绑定 |
| use-after-free | free(p); *p = 0; |
在 MemRegion 上标记 AfterFree,任何 load/store 路径到达即告警 |
支持配置 “白名单”:仅报必须路径(must-reach) |
| NULL 误用 | int x = *NULL; |
对 0 常量指针解引用立即报错 |
兼容平台特定 NULL != 0 的情况,需读 TargetInfo::getNullPointerValue() |
| effective-type 违规 | int *pi = malloc(4); *pi = 1; float *pf = (float*)pi; float f = *pf; |
在 malloc 返回的 GenericMemSpace 记录 “首次写入类型”,后续读取类型不匹配即报错 |
提供 strict-alias=0 模式只警告 “明显违规” |
所有规则均提供 CheckOptions 入口,可在 compile_commands.json 里通过 -analyzer-config 动态开关:
{
"command": "clang -c foo.c -Xclang -analyzer-config -Xclang dependablec:strict-alias=1"
}
4. 并发缺陷规则:让 “数据竞争” 在编译期现形
DependableC 禁止任何 POSIX 线程 API,只允许 C11 thrd_* 与 atomic_—— 但现实中老代码全是 pthread。插件策略:
- 识别竞争对:对全局变量
g的任意两个访问,若至少一个为写,且中间无mtx_lock(&m)路径,即判竞争。 - 顺序违规:把
mtx_lock/unlock建模为 acquire/release 边,若出现lock(&m2); lock(&m1); unlock(&m2);这类交叉顺序,报 “潜在死锁”。 - 混用 atomic/non-atomic:对同一变量,若存在
atomic_load(&x)又与x++并发,立即报错。
实现技巧:利用 Clang 的 ThreadSafety 分析框架,把 pthread_mutex_t 映射到 `CAPABILITY (