Clang 静态分析器诞生于 2007 年,是 LLVM 项目中最具创新性的组件之一。与传统的编译时警告不同,静态分析器通过符号执行技术对程序进行深度分析,能够发现更加复杂和隐蔽的程序缺陷。本章将深入探讨 Clang 静态分析器的设计理念、核心技术和实践应用,帮助读者理解现代静态分析技术的前沿发展。
Clang 静态分析器的发展可以分为几个重要阶段:
2007-2008:初创期 Ted Kremenek 在 Apple 公司主导开发了最初的分析器框架。这一时期的主要目标是为 Objective-C 代码提供内存管理错误检测,特别是针对 retain/release 的引用计数问题。早期的设计决策奠定了路径敏感分析的基础架构。
2009-2011:扩展期 随着 C++ 支持的加入,分析器面临了更大的挑战。Anna Zaks 加入团队后,重点改进了符号执行引擎的效率和精度。这一时期引入了更多的 Checker,包括对标准库函数的建模。
2012-2015:成熟期 分析器开始被广泛应用于大型项目。Jordan Rose 领导了对 C++ 构造函数和析构函数的支持,使得分析器能够准确跟踪对象生命周期。同时,跨函数分析(interprocedural analysis)得到了初步实现。
2016-2020:优化期 Artem Dergachev 主导了一系列现代化改进,包括更好的循环处理、改进的内存模型和更精确的指针分析。Z3 SMT 求解器的集成使得约束求解能力大幅提升。
2021-今:智能化探索 社区开始探索机器学习技术在静态分析中的应用,包括使用统计模型指导路径探索、自动学习 API 使用模式等。
Clang 静态分析器的核心设计理念包括:
路径敏感性(Path Sensitivity):分析器跟踪程序的具体执行路径,而不是简单地合并所有可能的程序状态。这使得分析结果更加精确,减少了误报。
上下文敏感性(Context Sensitivity):函数调用时会考虑调用上下文,使得分析能够区分不同调用点的行为差异。
流敏感性(Flow Sensitivity):分析器理解程序的控制流,能够追踪变量值在不同程序点的变化。
可扩展性(Extensibility):通过 Checker 机制,用户可以轻松添加自定义的分析规则,无需修改核心引擎。
实用性优先(Practicality First):在精度和性能之间寻求平衡,优先保证在真实项目中的可用性。
┌─────────────────┐
│ 源代码输入 │
└────────┬────────┘
│
┌────────▼────────┐
│ Clang AST │
└────────┬────────┘
│
┌────────▼────────┐
│ CFG 构建 │
└────────┬────────┘
│
┌────────────▼────────────┐
│ 符号执行引擎 │
│ ┌──────────────────┐ │
│ │ ProgramState │ │
│ ├──────────────────┤ │
│ │ Environment │ │
│ ├──────────────────┤ │
│ │ Store │ │
│ ├──────────────────┤ │
│ │ GenericDataMap │ │
│ └──────────────────┘ │
└────────────┬────────────┘
│
┌────────▼────────┐
│ Checker 调度 │
└────────┬────────┘
│
┌────────▼────────┐
│ 缺陷报告 │
└─────────────────┘
静态分析器的工作流程始于 Clang 前端生成的 AST,通过构建控制流图(CFG)来表示程序的执行路径。符号执行引擎是整个系统的核心,它维护程序状态(ProgramState)并模拟程序的执行。Checker 在关键点被调用,检查特定的程序属性并报告潜在问题。
符号执行是 Clang 静态分析器的核心技术。与传统的具体执行不同,符号执行使用符号值来表示程序变量,通过收集路径约束来推理程序的可能行为。
Clang 静态分析器使用一套精心设计的符号值系统来表示程序中的未知值:
// 符号值的层次结构
SymExpr
├── SymbolData // 基础符号
│ ├── SymbolRegionValue // 内存区域的初始值
│ ├── SymbolConjured // 函数调用返回值等
│ ├── SymbolDerived // 派生符号
│ ├── SymbolExtent // 区域大小
│ └── SymbolMetadata // 元数据符号
└── SymIntExpr/IntSymExpr // 符号与常量的运算
└── SymSymExpr // 符号间的运算
每个符号都有唯一的标识符,分析器通过符号表达式树来表示复杂的计算。例如,对于代码 int z = x + y * 2;,如果 x 和 y 是未知的,分析器会创建符号表达式 $x + ($y * 2)。
内存模型是符号执行的关键组件,Clang 使用基于区域(Region)的内存模型:
MemRegion 层次结构:
├── MemSpaceRegion // 内存空间
│ ├── StackSpaceRegion // 栈空间
│ ├── HeapSpaceRegion // 堆空间
│ └── GlobalsSpaceRegion // 全局空间
├── SubRegion // 子区域
│ ├── AllocaRegion // alloca 分配
│ ├── SymbolicRegion // 符号指针指向的区域
│ ├── TypedRegion // 有类型的区域
│ │ ├── VarRegion // 变量
│ │ ├── FieldRegion // 结构体字段
│ │ └── ElementRegion // 数组元素
│ └── ...
这种设计允许分析器精确地追踪指针关系和别名信息。每个区域可以有一个父区域,形成树状结构,反映了 C/C++ 的内存布局。
ProgramState 是分析器的核心数据结构,它捕获了程序在某个执行点的完整状态:
class ProgramState {
Environment Env; // 表达式到值的映射
Store St; // 内存位置到值的映射
GenericDataMap GDM; // Checker 特定数据
ConstraintManager CM; // 路径约束
};
状态管理采用函数式编程风格,所有状态都是不可变的(immutable)。状态转换通过创建新状态实现,这种设计有几个优点:
约束管理器负责维护和求解路径约束。基础的 RangeConstraintManager 使用区间算术:
// 示例:处理条件分支 if (x > 10)
// True 分支:添加约束 x ∈ (10, +∞)
// False 分支:添加约束 x ∈ (-∞, 10]
对于更复杂的约束,可以启用 Z3 求解器:
// Z3 可以处理的复杂约束示例
// (x + y > 10) && (x - y < 5) && (x > 0)
// Z3 可以判断这组约束是否可满足
约束求解的效率直接影响分析性能。分析器使用多种优化技术:
路径敏感分析是 Clang 静态分析器区别于传统数据流分析的关键特性。它通过分别分析每条可能的执行路径来提供更精确的结果。
分析器使用工作列表(worklist)算法来系统地探索程序路径:
class CoreEngine {
// 工作列表存储待探索的程序点
WorkList WList;
void ExecuteWorkList() {
while (!WList.empty()) {
WorkListUnit Unit = WList.dequeue();
// 处理当前程序点
HandleBlockEdge(Unit.getNode(), Unit.getBlock());
// 生成后继状态
GenerateSuccessors(Unit);
}
}
};
工作列表的调度策略影响分析的效率和精度:
路径数量随着分支数量指数增长,这是符号执行的根本挑战。考虑一个有 n 个独立 if 语句的程序,理论上有 2^n 条路径。
分析器采用多种策略缓解路径爆炸:
1. 路径合并(Path Merging) 在某些程序点合并相似的路径状态:
if (condition) {
x = 1;
} else {
x = 2;
}
// 合并点:x = φ(1, 2)
// 使用符号值表示 x 可能是 1 或 2
2. 循环展开限制 限制循环展开次数,默认为 4 次:
// 配置选项
-analyzer-max-loop 4 // 最大循环展开次数
3. 函数内联限制 控制函数调用的内联深度:
// 分析选项
-analyzer-inline-max-stack-depth 5 // 最大内联深度
4. 状态缓存 缓存已分析的状态,避免重复计算:
class ExplodedGraph {
// 缓存已探索的 <程序点, 状态> 对
llvm::FoldingSet<ExplodedNode> Nodes;
ExplodedNode* getNode(ProgramPoint Loc,
ProgramStateRef State) {
// 检查缓存,避免重复创建节点
return Nodes.FindNodeOrInsertPos(Loc, State);
}
};
条件分支是路径敏感分析的核心:
void ProcessBranch(const Stmt *Condition,
ExplodedNode *Pred) {
ProgramStateRef State = Pred->getState();
SVal CondVal = State->getSVal(Condition);
// 假设条件为真
if (ProgramStateRef TrueState =
State->assume(CondVal, true)) {
// 探索 true 分支
GenerateNode(TrueBranch, TrueState);
}
// 假设条件为假
if (ProgramStateRef FalseState =
State->assume(CondVal, false)) {
// 探索 false 分支
GenerateNode(FalseBranch, FalseState);
}
}
循环是路径爆炸的主要来源。分析器使用多种技术处理循环:
1. 循环加宽(Loop Widening) 当循环迭代超过阈值时,将循环变量加宽到其类型的全范围:
for (int i = 0; i < n; ++i) {
// 前 4 次:i = 0, 1, 2, 3
// 之后:i = [0, INT_MAX]
}
2. 循环摘要(Loop Summarization) 预计算循环的效果,避免逐次迭代:
// 原始循环
for (int i = 0; i < 100; ++i) {
sum += i;
}
// 摘要:sum = sum_0 + 4950
3. 不变量推断 识别循环不变量,减少需要跟踪的状态:
int x = 10;
for (int i = 0; i < n; ++i) {
// x 是循环不变量
y = x + i; // 只需跟踪 i 的变化
}
函数调用的处理策略直接影响分析的精度和性能:
1. 内联策略 小函数直接内联分析:
// 适合内联的函数特征:
// - 代码行数少于阈值(默认 50 行)
// - 不包含循环
// - 非递归
2. 函数摘要 为常用函数预计算摘要:
// malloc 的摘要
void* malloc(size_t size) {
// 返回:新分配的堆区域符号
// 约束:size > 0 时返回非空
// size = 0 时返回可能为空
}
3. 调用上下文敏感性 区分不同调用点的上下文:
void foo(int *p) {
*p = 10;
}
void caller() {
int x, y;
foo(&x); // 上下文 1:p 指向 x
foo(&y); // 上下文 2:p 指向 y
// 分析器分别跟踪两个调用
}
Checker 是 Clang 静态分析器的扩展机制,允许开发者添加特定的程序验证规则。每个 Checker 关注特定类型的缺陷,通过注册回调函数在分析过程中的关键点进行检查。
Checker 通过实现特定的回调接口与分析引擎交互:
class Checker : public CheckerBase {
public:
// 前置条件检查
void checkPreCall(const CallEvent &Call,
CheckerContext &C) const;
// 后置条件检查
void checkPostCall(const CallEvent &Call,
CheckerContext &C) const;
// 语句检查
void checkPreStmt(const Stmt *S,
CheckerContext &C) const;
void checkPostStmt(const Stmt *S,
CheckerContext &C) const;
// 分支条件检查
void checkBranchCondition(const Stmt *Condition,
CheckerContext &C) const;
// 路径结束检查
void checkEndFunction(CheckerContext &C) const;
// 死亡符号检查
void checkDeadSymbols(SymbolReaper &SymReaper,
CheckerContext &C) const;
};
分析器在特定的程序点触发 Checker 回调:
1. 语句级回调
// 检查除零错误的示例
void DivZeroChecker::checkPreStmt(
const BinaryOperator *B,
CheckerContext &C) const {
if (B->getOpcode() != BO_Div &&
B->getOpcode() != BO_Rem)
return;
SVal Denominator = C.getState()->getSVal(B->getRHS());
// 检查分母是否可能为零
Optional<DefinedSVal> DV = Denominator.getAs<DefinedSVal>();
if (!DV)
return;
ConstraintManager &CM = C.getConstraintManager();
ProgramStateRef StateNotZero, StateZero;
std::tie(StateNotZero, StateZero) = CM.assumeDual(
C.getState(), *DV);
if (StateZero && !StateNotZero) {
// 确定会除零
reportBug("Division by zero", C);
}
}
2. 函数调用回调
void MallocChecker::checkPostCall(
const CallEvent &Call,
CheckerContext &C) const {
if (!Call.isGlobalCFunction("malloc"))
return;
// 获取分配大小
SVal Size = Call.getArgSVal(0);
// 创建新的堆区域
SVal RetVal = Call.getReturnValue();
ProgramStateRef State = C.getState();
// 记录分配信息
SymbolRef Sym = RetVal.getAsSymbol();
if (Sym) {
State = State->set<AllocatedMemory>(Sym,
AllocInfo(Size));
C.addTransition(State);
}
}
Checker 可以在 ProgramState 中存储自定义信息:
// 定义 Checker 特定的状态
REGISTER_MAP_WITH_PROGRAMSTATE(StreamMap,
SymbolRef,
StreamState)
class StreamChecker : public Checker {
void checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
if (Call.isGlobalCFunction("fopen")) {
SymbolRef FileDesc = Call.getReturnValue().getAsSymbol();
if (FileDesc) {
ProgramStateRef State = C.getState();
// 记录文件打开状态
State = State->set<StreamMap>(FileDesc,
StreamState::Opened);
C.addTransition(State);
}
}
}
};
1. 资源泄漏检测
class ResourceLeakChecker : public Checker {
// 跟踪资源分配
void checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
if (isResourceAllocation(Call)) {
trackResource(Call, C);
}
}
// 跟踪资源释放
void checkPreCall(const CallEvent &Call,
CheckerContext &C) const {
if (isResourceDeallocation(Call)) {
verifyAndReleaseResource(Call, C);
}
}
// 检查函数结束时的资源状态
void checkEndFunction(CheckerContext &C) const {
reportLeakedResources(C);
}
};
2. 污点分析
class TaintChecker : public Checker {
// 标记污点源
void checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
if (isTaintSource(Call)) {
ProgramStateRef State = C.getState();
SymbolRef Sym = Call.getReturnValue().getAsSymbol();
State = State->add<TaintedSymbols>(Sym);
C.addTransition(State);
}
}
// 检查污点传播
void checkPreStmt(const Stmt *S,
CheckerContext &C) const {
if (isCriticalUse(S)) {
checkTaintedData(S, C);
}
}
};
3. 类型状态检查
class TypeStateChecker : public Checker {
enum State { Uninitialized, Initialized, Moved };
void checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
// 跟踪对象状态转换
if (isConstructor(Call)) {
setState(Call.getCXXThisVal(), Initialized, C);
} else if (isMoveConstructor(Call)) {
setState(Call.getArgSVal(0), Moved, C);
}
}
void checkPreCall(const CallEvent &Call,
CheckerContext &C) const {
// 验证使用前的状态
if (requiresInitialized(Call)) {
verifyState(Call.getCXXThisVal(), Initialized, C);
}
}
};
多个 Checker 可能需要协作:
// Checker 依赖关系声明
def MallocChecker : Checker<"unix.Malloc">,
Dependencies<[CStringChecker]>,
HelpText<"Check for memory leaks">;
// 共享信息的方式
namespace {
// 使用命名空间避免冲突
struct AllocInfo {
enum Kind { Malloc, New, NewArray };
Kind K;
SVal Size;
};
}
// 通过 ProgramState 共享
REGISTER_MAP_WITH_PROGRAMSTATE(AllocationMap,
SymbolRef,
AllocInfo)
理解不同静态分析工具的设计权衡有助于选择合适的工具和改进分析技术。
Coverity 是商业静态分析领域的领导者,起源于斯坦福大学的研究项目。
技术特点:
与 Clang 的对比:
特性 Coverity Clang 静态分析器
规模 企业级(百万行) 中等规模
精度 高(低误报) 中等
速度 慢(小时级) 快(分钟级)
价格 昂贵 免费开源
定制化 有限 高度可定制
PVS-Studio 专注于 C/C++ 和 C# 的深度分析。
独特优势:
检测能力对比:
// PVS-Studio 能检测的微妙错误
if (ptr || ptr->field) { // V522: 逻辑错误
// 应该是 && 而不是 ||
}
// Clang 可能漏报的模式
memset(arr, 0, sizeof(arr)); // 当 arr 是指针时
Infer 使用分离逻辑(Separation Logic)进行分析。
核心创新:
分离逻辑示例:
// Infer 的堆抽象
{emp}
x = malloc();
{x ↦ _}
y = malloc();
{x ↦ _ * y ↦ _} // * 表示分离合取
free(x);
{y ↦ _}
| 场景 | 推荐工具 | 理由 |
|---|---|---|
| 开源项目 CI/CD | Clang 静态分析器 | 免费、易集成、快速 |
| 安全关键系统 | Coverity | 低误报、符合标准 |
| 大规模重构 | PVS-Studio | 详细建议、增量分析 |
| 移动应用 | Infer | 内存泄漏、并发错误 |
| 嵌入式系统 | PC-lint Plus | 标准合规性 |
| Web 应用 | Fortify | 安全漏洞检测 |
1. 分析技术
工具 核心技术
Clang SA 符号执行 + 路径敏感
Coverity 统计分析 + 模式匹配
PVS-Studio 语法模式 + 数据流
Infer 分离逻辑 + 抽象解释
Polyspace 抽象解释 + 形式验证
2. 性能特征
工具 分析速度 内存使用 可扩展性
Clang SA 快 中等 好
Coverity 慢 高 极好
PVS-Studio 中等 中等 好
Infer 快 低 极好
3. 误报率与漏报率权衡
实践中常常组合使用多个工具:
# CI/CD 管道示例
stages:
- quick_check:
- clang-tidy # 快速检查
- clang static analyzer # 基础缺陷
- deep_analysis:
- infer # 内存和并发
- pvs-studio # 代码质量
- security:
- coverity # 安全漏洞
SMT(Satisfiability Modulo Theories)求解器的集成代表了静态分析技术的前沿发展。Z3 是微软研究院开发的高性能 SMT 求解器,自 2017 年起被集成到 Clang 静态分析器中。
SMT 求解器扩展了 SAT 求解器,能够处理包含算术、数组、位向量等理论的约束:
SAT vs SMT 对比:
SAT 问题:(a ∨ b) ∧ (¬a ∨ c) ∧ (¬b ∨ ¬c)
SMT 问题:(x > 10) ∧ (y < x + 5) ∧ (y > 15)
SMT 求解器支持的理论:
启用 Z3 求解器:
# 编译时启用 Z3
cmake -DLLVM_ENABLE_Z3_SOLVER=ON ...
# 使用 Z3 进行分析
clang --analyze -Xanalyzer -analyzer-constraints=z3 source.c
集成架构:
class Z3ConstraintManager : public SimpleConstraintManager {
Z3_context Z3Ctx;
Z3_solver Solver;
ProgramStateRef assume(ProgramStateRef State,
SymbolRef Sym,
bool Assumption) {
// 将符号约束转换为 Z3 表达式
Z3_ast Constraint = symbolToZ3Expr(Sym);
// 添加约束到求解器
Z3_solver_push(Z3Ctx, Solver);
Z3_solver_assert(Z3Ctx, Solver, Constraint);
// 检查可满足性
Z3_lbool Result = Z3_solver_check(Z3Ctx, Solver);
if (Result == Z3_L_FALSE) {
// 约束不可满足,路径不可达
return nullptr;
}
// 更新程序状态
return State->set<ConstraintSet>(Sym, Assumption);
}
};
示例 1:复杂算术约束
void complexConstraints(int x, int y, int z) {
if (x + y > 10) {
if (y - z < 5) {
if (x + z == 15) {
// Z3 可以判断这个路径是否可达
// 约束系统:
// x + y > 10
// y - z < 5
// x + z = 15
// Z3 求解:x=8, y=7, z=7 是一个解
}
}
}
}
示例 2:数组边界检查
void arrayBounds(int arr[], int i, int j) {
if (i >= 0 && i < 10) {
if (j >= 0 && j < 10) {
if (i + j >= 10) {
arr[i + j] = 0; // Z3 检测潜在越界
// 约束:i ∈ [0,9], j ∈ [0,9], i+j ≥ 10
// Z3 发现:i+j 可能达到 18,超出边界
}
}
}
}
Z3 求解可能很昂贵,需要优化策略:
1. 增量求解
class IncrementalZ3Solver {
// 使用栈保存求解器状态
void pushContext() {
Z3_solver_push(Ctx, Solver);
}
void popContext() {
Z3_solver_pop(Ctx, Solver, 1);
}
// 重用之前的求解结果
bool checkWithCache(Z3_ast Constraint) {
if (Cache.contains(Constraint))
return Cache[Constraint];
bool Result = check(Constraint);
Cache[Constraint] = Result;
return Result;
}
};
2. 约束简化
// 在提交给 Z3 之前简化约束
Z3_ast simplifyConstraint(Z3_ast Expr) {
// 使用 Z3 的简化 API
Z3_ast Simplified = Z3_simplify(Ctx, Expr);
// 自定义简化规则
// x + 0 → x
// x * 1 → x
// x && true → x
return Simplified;
}
3. 超时控制
void configureZ3Timeout() {
Z3_params Params = Z3_mk_params(Ctx);
// 设置 100ms 超时
Z3_params_set_uint(Ctx, Params,
Z3_mk_string_symbol(Ctx, "timeout"),
100);
Z3_solver_set_params(Ctx, Solver, Params);
}
1. 不可判定性 某些理论组合是不可判定的:
// 非线性算术是不可判定的
if (x * y > 100 && y * z < 50 && x * z == 75) {
// Z3 可能无法在合理时间内求解
}
2. 状态空间爆炸
// 大量变量导致求解时间指数增长
void manyVariables(int a[], int n) {
for (int i = 0; i < n; i++) {
if (a[i] > a[i+1] + a[i+2]) {
// 每个循环迭代引入新变量
// n 次迭代产生 O(n) 个变量和约束
}
}
}
3. 浮点数精度
// 浮点数的精确建模很困难
float x = 0.1f;
if (x + x + x == 0.3f) {
// 由于浮点数表示误差,可能不相等
// Z3 需要特殊的浮点理论支持
}
求解器对比: | 求解器 | 特点 | 适用场景 | |——–|——|———-| | Z3 | 通用、功能全面 | 复杂约束、多理论组合 | | CVC4 | 学术背景、理论完备 | 形式验证 | | Yices | 轻量、快速 | 简单线性约束 | | Boolector | 位向量专长 | 硬件验证 | | MathSAT | 商业支持 | 工业应用 |
1. 机器学习引导 使用 ML 模型预测哪些约束值得求解:
# 伪代码:ML 引导的选择性求解
def shouldUseZ3(constraints):
features = extractFeatures(constraints)
probability = mlModel.predict(features)
return probability > threshold
2. 并行求解 利用多核处理器并行探索路径:
// 并行路径探索
parallel_for(paths.begin(), paths.end(),
[](Path &p) {
Z3Solver solver;
solver.checkPath(p);
});
3. 抽象解释结合 结合抽象解释减少需要精确求解的约束:
// 先用区间分析快速剪枝
if (intervalAnalysis.isInfeasible(path)) {
return; // 避免调用 Z3
}
// 只对可能可行的路径使用 Z3
z3Solver.checkPrecise(path);
通过具体案例理解 Clang 静态分析器的实际应用。
问题代码:
void processData(int size) {
char *buffer = (char*)malloc(size);
if (!buffer) {
return;
}
if (size > MAX_SIZE) {
// 忘记释放 buffer
return; // 内存泄漏
}
processBuffer(buffer);
free(buffer);
}
分析过程:
检测报告:
memory-leak.c:8:5: warning: Potential memory leak
return; // 内存泄漏
^~~~~~~
memory-leak.c:2:18: note: Memory allocated here
char *buffer = (char*)malloc(size);
^~~~~~~~~~~~~~~~~~~~
复杂的空指针场景:
struct Node {
int value;
struct Node *next;
};
void traverseList(struct Node *head, int target) {
struct Node *current = head;
struct Node *prev = NULL;
while (current != NULL) {
if (current->value == target) {
if (prev != NULL) {
prev->next = current->next;
} else {
head = current->next;
}
free(current);
current = prev->next; // 潜在空指针解引用
}
prev = current;
current = current->next;
}
}
分析关键点:
current->value == target && prev == NULLhead = current->next 后current = prev->next 其中 prev 仍为 NULL数据竞争示例:
class Counter {
int count;
bool locked;
public:
void increment() {
if (!locked) {
locked = true;
count++; // 竞争条件
locked = false;
}
}
int getValue() {
return count; // 无保护的读取
}
};
ThreadSanitizer 集成分析:
C++ 虚函数安全:
class Base {
public:
virtual void process() = 0;
};
class Derived : public Base {
int data;
public:
void process() override {
data = 42;
}
};
void unsafecast(Base *b) {
// 不安全的向下转型
Derived *d = static_cast<Derived*>(b);
d->process(); // 如果 b 不是 Derived,则未定义行为
}
分析策略:
Clang 静态分析器展示了现代程序分析技术的最新进展。通过符号执行、路径敏感分析和 SMT 求解器的结合,它能够发现传统编译器警告无法检测的复杂缺陷。主要要点包括:
关键公式:
练习 9.1:解释符号执行与具体执行的区别,并给出一个符号执行能检测而具体测试可能遗漏的 bug 示例。
提示:考虑输入空间覆盖的差异。
练习 9.2:描述 Clang 静态分析器的内存模型中,Region 层次结构如何表示以下 C 代码的内存布局:
struct Point { int x, y; };
struct Point arr[10];
int *p = &arr[5].x;
提示:考虑 ElementRegion 和 FieldRegion 的嵌套。
练习 9.3:编写一个简单的 Checker 伪代码,检测除零错误。需要处理哪些回调?如何判断除数可能为零?
提示:使用 checkPreStmt 回调和约束管理器。