Hotdry.
compiler-design

把 DependableC 变成编译器插件:C 内存与并发缺陷静态检测工具链

基于 DependableC 保守子集,给出可落地的编译器插件架构、检测规则与工程化参数,让 CI 在 3 分钟内拦住 UB。

DependableC 的作者 Eskil Steenberg Hald 在网站里反复强调一句话:

“只要你不触发未定义行为,C 就是世界上最安全的语言。”

道理都懂,但人眼会犯错。本文把 DependableC 的 “人类守则” 翻译成 “机器守则”—— 用编译器插件在提交前就把内存与并发缺陷钉死。整套工具链只依赖 Clang/LLVM 现有 API,不造新轮子,方便你在现有 CI 里 3 分钟落地。

1. 为什么 DependableC 需要插件化静态分析

DependableC 的核心是 “最小可移植子集”:

  • 语法层面:只承认 C89 + 少量 C99(如 // 注释),禁止 VLAs、复数、 Annex K。
  • 语义层面:零容忍 UB—— 不允许任何 Effective Type 违规、指针算术越界、数据竞争。

人工代码审查无法规模化,尤其面对祖传宏魔法。把规则搬进编译器插件,好处:

  1. 路径敏感:CSA(Clang Static Analyzer)引擎已帮你做完抽象解释,直接复用。
  2. 零误报可配置:通过 .dependablec_suppress 文件给特定函数 / 宏打标,避免 “狼来了”。
  3. 增量检查:基于编译数据库(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。插件策略:

  1. 识别竞争对:对全局变量 g 的任意两个访问,若至少一个为写,且中间无 mtx_lock(&m) 路径,即判竞争。
  2. 顺序违规:把 mtx_lock/unlock 建模为 acquire/release 边,若出现 lock(&m2); lock(&m1); unlock(&m2); 这类交叉顺序,报 “潜在死锁”。
  3. 混用 atomic/non-atomic:对同一变量,若存在 atomic_load(&x) 又与 x++ 并发,立即报错。

实现技巧:利用 Clang 的 ThreadSafety 分析框架,把 pthread_mutex_t 映射到 `CAPABILITY (

查看归档