Clang Static Analyzer
基于符号执行的源代码级静态分析工具
LLVM 项目的一部分 · 100% 开源 · 支持 C / C++ / Objective-C
1. 概述
Clang Static Analyzer(CSA)是 LLVM 项目中的一个源代码分析工具,能够在不运行程序的情况下,通过对源代码的深度语义分析找出 Bug。它实现了路径敏感(path-sensitive)的、过程间(inter-procedural)的符号执行(symbolic execution)。
🔍 路径敏感
沿着程序的每一条执行路径进行推理,追踪分支条件对程序状态的影响。与简单的语法检查器不同,CSA 真正「理解」if/else、循环、函数调用对程序的影响。
🔗 过程间分析
自动内联函数调用,跨越函数边界追踪 Bug。一个在辅助函数中产生的空指针解引用,即使发生在五层调用栈之下,也会被 CSA 捕获。
🧠 符号执行
用符号值代替具体值执行程序,并为每条路径维护约束条件。这允许 CSA 发现那些在具体测试中极少触发的深层 Bug。
🧩 可扩展架构
检查器(Checker)以插件形式注册到分析引擎。开发者可以编写自定义检查器,聚焦特定领域的 Bug 模式。
2. 架构总览
图1:Clang Static Analyzer 架构。Clang 前端将源码编译为 AST 和控制流图(CFG);分析引擎在 CFG 上执行符号执行,构建 ExplodedGraph;检查器订阅分析事件;Bug 报告附完整路径追溯。
分析流程的核心步骤:
- Clang 前端解析源码,生成 AST(抽象语法树)和 CFG(控制流图)。
- CoreEngine 沿 CFG 边进行符号执行,维护
ProgramState(包含符号存储、环境映射和检查器通用数据)。
- 每个程序点产生一个 ExplodedNode,全部节点构成 ExplodedGraph——即符号执行的状态空间。
- Checker 通过访问者模式注册回调(如
checkPreCall、checkBind、checkDeadSymbols),在符号执行的关键节点被唤醒。
- Checker 发现问题后调用
BugReporter,生成包含完整执行路径的 Bug 报告。
3. 检查器(Checkers)
检查器是 CSA 真正「找 Bug」的模块。它们被组织为多个包(package),每个包各有侧重。
3.1 默认检查器(Default Checkers)
| 包 | 典型检查器 | 检测的 Bug 类型 |
| core | core.NullDereference
core.DivideZero
core.StackAddressEscape
core.uninitialized.* | 空指针解引用、除零、栈地址逃逸、未初始化变量使用 |
| cplusplus | cplusplus.NewDeleteLeaks
cplusplus.Move
cplusplus.SelfAssignment | 内存泄漏、use-after-move、自赋值 |
| deadcode | deadcode.DeadStores | 死存储(写入后从未读取的值) |
| security | security.ArrayBound
security.FloatLoopCounter
security.insecureAPI.* | 数组越界、浮点循环计数器、gets/bcopy 等不安全 API |
| nullability | nullability.NullPassedToNonnull
nullability.NullReturnedFromNonnull | 将 null 传给 nonnull 参数、nonnull 函数返回 null |
| optin | optin.performance.Padding
optin.taint.TaintedAlloc
optin.cplusplus.UninitializedObject | 结构体填充浪费、污点数据导致的分配问题、C++对象未初始化 |
3.2 实验性检查器(Alpha Checkers)
⚠️ 实验性——这些检查器仍在开发中,默认关闭,可能产生更多误报或崩溃。使用时需显式启用。
| 检查器 | 用途 |
alpha.security.ArrayBoundV2 | 更精确的数组边界检查(V2 算法) |
alpha.unix.Stream | 检测 fopen/fclose 等流 API 的误用(double-close、use-after-close) |
alpha.unix.cstring.BufferOverlap | 检测 memcpy 等重叠缓冲区的未定义行为 |
alpha.core.CastSize | 检测因类型转换导致的截断 |
alpha.clone.CloneChecker | 检测代码克隆(copy-paste Bug) |
3.3 检查器选择指南
# 仅使用默认检查器(安全、适合CI)
scan-build make
# 启用所有核心检查器
scan-build --enable-checker core,deadcode,nullability make
# 启用所有检查器(含alpha)
scan-build --enable-all-checkers make
# 精细控制:启用安全包,禁用特定检查器
scan-build -enable-checker security -disable-checker security.insecureAPI.gets make
4. 使用方法
4.1 scan-build —— 最简入门
# 安装 clang 和 scan-build
sudo apt install clang clang-tools # Debian/Ubuntu
brew install llvm # macOS
# 分析一个项目
scan-build cmake -B build .
scan-build cmake --build build
# 查看 HTML 报告
scan-view /tmp/scan-build-2026-07-06-*/
4.2 直接调用 clang --analyze
# 单文件分析
clang --analyze -Xanalyzer -analyzer-checker=core file.c
# 启用多检查器
clang --analyze -Xanalyzer -analyzer-checker=core,security,deadcode file.c
# 输出为 plist 格式(供 Xcode 使用)
clang --analyze -Xanalyzer -analyzer-output=plist file.c
4.3 CodeChecker —— 大规模分析
# CodeChecker 提供 Web UI、增量分析、结果存储
pip install codechecker
# 创建编译数据库
CodeChecker log -b "make" -o compilation_database.json
# 运行分析
CodeChecker analyze compilation_database.json -o reports
# 启动 Web 查看器
CodeChecker server &
CodeChecker store reports --name my_project
# 打开 http://localhost:8001
4.4 抑制误报
// 方法1: 源码注解 —— 抑制特定检查器的警告
#ifdef __clang_analyzer__
#define SUPPRESS __attribute__((suppress))
#else
#define SUPPRESS
#endif
void example() {
int *p SUPPRESS = NULL; // 此处不报告 core.NullDereference
}
// 方法2: 断言引导分析器
#include <assert.h>
void safe_use(int *p) {
assert(p != NULL); // 分析器利用此断言,不再报告空指针
*p = 42;
}
5. 内部原理
5.1 符号执行引擎
Clang Static Analyzer 的核心是一种路径敏感、流敏感的符号执行。关键组件:
| 组件 | 职责 |
| CFG(控制流图) | 由 Clang AST 构建的有向图,节点为基本块和语句,边表示控制流转移。 |
| ExplodedGraph | 符号执行的状态空间图。每个节点 = (CFG位置, ProgramState)。 |
| CoreEngine | 驱动符号执行的主循环:从 WorkList 取出节点,评估 CFG 边的转移,产生新节点。 |
| ProgramState | 三元组:Store(内存模型)、Environment(变量→值的映射)、GenericDataMap(检查器私有数据)。 |
| StoreManager / RegionStore | 管理符号内存模型。RegionStore 以「区域」(Region)为单位建模内存,支持字段敏感推理。 |
| ConstraintManager | 管理路径约束。默认使用 RangeConstraintManager,基于范围的约束求解;可选集成 Z3 进行更精确的约束求解。 |
5.2 路径爆炸与缓解策略
符号执行面临的核心挑战是路径爆炸——随着分支和循环,执行路径指数增长。CSA 通过以下策略缓解:
- 函数摘要(Function Summary):为函数生成摘要,避免在调用点反复展开。
- 循环展开限制:循环体默认最多展开4次,之后宽化(widen)为未知状态。
- 路径合并(Path Merging):在控制流汇聚点合并相似状态。
- 节点缓存:对 (CFG位置, ProgramState) 对去重,避免重复分析。
- 分析器配置调优:
-analyzer-config max-nodes=150000 控制每个函数的探索上限。
5.3 跨翻译单元分析(CTU)
默认情况下,CSA 对每个翻译单元(.c 文件)独立分析。对于跨文件 Bug(如在一个文件中分配、在另一个文件中释放),这有局限。CTU 分析在调用外部函数时加载其函数摘要进行分析,极大地扩展了分析范围。
# 分两步启用 CTU
# 步骤1: 生成 AST 并收集外部定义
clang --analyze -Xanalyzer -analyzer-config experimental-enable-naive-ctu-analysis=true \
-Xanalyzer -analyzer-config ctu-dir=ctu-dir file.c
# 步骤2 (CodeChecker): 自动处理
CodeChecker analyze compile_commands.json --ctu
6. 编写自定义检查器
Clang Static Analyzer 的一个核心优势是其可扩展性。编写一个新检查器只需继承 Checker 基类并注册回调。
6.1 最小检查器示例
// 检查所有超过 64 个字符的函数名
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
using namespace clang;
using namespace ento;
namespace {
class LongFunctionNameChecker : public Checker<check::ASTDecl<FunctionDecl>> {
std::unique_ptr<BugType> BT;
public:
LongFunctionNameChecker()
: BT(std::make_unique<BugType>(this, "过长函数名", "代码风格")) {}
void checkASTDecl(const FunctionDecl *FD, AnalysisManager &Mgr,
BugReporter &BR) const {
if (FD->getName().size() <= 64) return;
PathDiagnosticLocation Loc =
PathDiagnosticLocation::create(FD, Mgr.getSourceManager());
BR.EmitBasicReport(FD, this, "过长函数名",
"代码风格",
"函数名超过64字符,建议缩短。",
Loc);
}
};
}
// 注册检查器
void ento::registerLongFunctionNameChecker(CheckerManager &Mgr) {
Mgr.registerChecker<LongFunctionNameChecker>();
}
6.2 常用的 Checker 回调
| 回调 | 触发时机 | 典型用途 |
check::PreCall | 每次函数调用前 | 检查参数合法性(如 malloc(0)) |
check::PostCall | 每次函数调用后 | 标记返回值(如 malloc 返回的指针可能为 null) |
check::Bind | 每次赋值时 | 追踪污点数据传播 |
check::PreStmt<ReturnStmt> | 每次 return 前 | 检查返回值是否违反合约 |
check::DeadSymbols | 符号不再可达时 | 检测资源泄漏(malloc 的指针丢失) |
check::EndFunction | 函数分析结束时 | 汇总分析结果 |
eval::Call | 遇到函数调用时 | 建模函数行为(内联效果) |
💡 提示:LLVM 源码中的 clang/lib/StaticAnalyzer/Checkers/ 目录包含 100+ 个检查器实现,是学习编写自定义检查器的最佳参考。
7. 与其他工具对比
| 维度 | Clang Static Analyzer | GCC -fanalyzer | Coverity | Cppcheck |
| 分析深度 | 路径敏感、过程间 | 路径敏感、过程间 | 路径敏感、过程间 | 路径不敏感 |
| 符号执行 | ✅ 完整 | ✅ 完整 | ✅ 完整 | ❌ 基于模式 |
| 误报率 | 中-低 | 中-低 | 低(商业优化) | 高(浅层匹配) |
| 可扩展性 | ✅ Checker API | ✅ GCC 插件 | ❌ 闭源 | ❌ 硬编码规则 |
| 开源 | ✅ Apache 2.0 | ✅ GPL | ❌ 商业 | ✅ GPL |
| 跨 TU 分析 | ✅ CTU(实验性) | ✅ -fanalyzer | ✅ 完整 | ❌ 不支持 |
| CI 集成 | ✅ scan-build / CodeChecker | ✅ -fanalyzer | ✅ 商业平台 | ✅ 命令行 |
与 LLVM 体系内其他工具的关系
🧹 Clang-Tidy
互补工具。Clang-Tidy 是语法级(AST 匹配器)的 lint 工具,不执行符号执行。CSA 做深层次的路径敏感分析。两者可在同一项目中同时使用。
🛡️ UBSan / ASan
互补工具。Sanitizer(UBSan、ASan)是动态检测工具,在运行时插桩检测 Bug。CSA 是静态的,不运行程序,两者结合可覆盖编译时+运行时缺陷。
🔧 Clangd (LSP)
间接关联。Clangd 使用同一套 Clang 前端基础设施,提供 IDE 级别的代码补全和诊断。CSA 也可以通过 clangd 集成显示分析警告。
8. 污点分析(Taint Analysis)
CSA 内置污点分析框架,可用于追踪来自不可信源(用户输入、网络数据等)的数据流,检测注入类漏洞。
// 需要启用实验性检查器
clang --analyze -Xanalyzer \
-analyzer-checker=optin.taint.TaintedAlloc,\
alpha.security.ArrayBoundV2 file.c
污点传播路径示例:
// CSA 能追踪以下完整路径并报告 Bug
int size = atoi(argv[1]); // 污点源:外部输入
int *buf = malloc(size * 4); // TaintedAlloc:污点数据控制分配大小
buf[size] = 0; // ArrayBoundV2:可能的越界写入
9. 最佳实践
1️⃣ CI 集成
将 scan-build 或 CodeChecker 加入 CI 管线,每次提交自动分析。
scan-build --status-bugs make
有 Bug 时返回非零退出码。
2️⃣ 渐进启用
从默认检查器开始,逐步启用 alpha 实验性检查器,评估误报率后决定是否长期使用。
3️⃣ 与编译器警告互补
启用 -Wall -Wextra 编译器警告 + CSA 深度分析,形成多层防线。还可以叠加 ASan/UBSan 在运行时检测。
4️⃣ 定期基线化
首次分析后记录 Bug 数量作为基线。之后每次分析只关注新增 Bug,避免被历史遗留问题淹没。
CodeChecker 内置此功能。
参考资源