此页正在建设中...

Checker 开发者手册

静态分析器引擎对程序执行路径敏感探索,并且依赖于一组 checkers 来实现用于检测和构造特定错误报告的逻辑。任何感兴趣实现自己的 checker,可以看《24小时构建 Checker》(幻灯片 视频),并参考该页面有关编写 checker 的其他信息。静态分析器是 Clang 项目的一部分,因此请查阅 Hacking on ClangLLVM 程序员手册了解开发人员指南,并将您的问题和建议发送到 cfe-dev 邮件列表

入门

静态分析器概述

分析器核心是给定程序的符号执行。所有输入值都用符号值表示; 此外,引擎基于输入符号和路径推导出程序中所有表达式的值。执行是路径敏感的,并且探索通过程序的每个可能的路径。被探索的执行轨迹用 ExplodedGraph 对象表示。图的每个节点是 ExplodedNode,它由一个 ProgramPoint 和一个 ProgramState 组成。

ProgramPoint 表示程序(或CFG)中的对应位置。ProgramPoint 还用于记录有关何时/如何添加状态的附加信息。例如,PostPurgeDeadSymbolsKind 状态是清除死符号的结果——等于分析器的垃圾回收。

ProgramState 表示程序的抽象状态。它包括:

与 Checkers 交互

Checkers 不仅仅是分析器核心变化的被动接收器——它们通过 GenericDataMap 主动参与 ProgramState 构造,GenericDataMap 可用于存储 checker-defined 的部分状态。每次分析器引擎探索一个新的语句,它通知每个已注册的 checker 监听该语句,给它一个报告错误或修改状态的机会。(作为经验法则,checker 本身应该是无状态的。)以预定义的顺序一个接一个地调用 checkers; 因此,调用所有 checkers 添加一个链到 ExplodedGraph

表示值

在符号执行期间,SVal 对象用于表示表达式的语义评估。它们可以表示诸如具体整数、符号值或内存位置(它们是内存区域)之类的内容。它们是“值”、符号和其他方面的 union。如果值不是符号,通常意味着没有要跟踪的符号信息。例如,如果值是一个整数,例如42,它将是一个 ConcreteInt,并且 checker 通常不需要跟踪具体数字的任何状态。在某些情况下,SVal 不是一个符号,但它真的应该是一个符号值。这发生在分析器不能推测某事物时。一个例子是浮点数。在这种情况下,SVal 将评估为 UnknownVal。这表示分析器的推理能力范围之外的情况。SVals是值对象,它们的值可以使用.dump()方法查看。通常它们封装持久对象,如符号或区域。

SymExpr(符号)意在表示抽象的,但是命名的符号值。符号表示实际(不可变)值。我们可能不知道它的具体值是什么,但是当我们分析路径时,我们可以将约束与该值相关联。例如,我们可能记录一个符号的值大于0,等。

MemRegion 类似于一个符号。它用于提供如何描述抽象内存的词典。区域可以在其他区域的顶部,提供分层的方法来表示内存。例如,堆栈上的结构对象可以由 VarRegion 表示,但是作为 VarRegion 的子区域的 FieldRegion 可以用于表示与该对象的特定字段相关联的存储器。那么我们如何表示符号内存区域呢?这就是 SymbolicRegion 的目的。它是一个具有关联符号的 MemRegion。由于符号是唯一的并且具有唯一的名称; 该符号命名该区域。

让我们看看分析器如何处理以下示例中的表达式:

  int foo(int x) {
     int y = x * 2;
     int z = x;
     ...
  }
  

让我们看看如何评估 x * 2。当 x 被求值时,我们首先构造一个表示 x 的左值的 SVal,在这种情况下,它是一个引用了 xMemRegionSVal。然后,当我们进行左值到右值转换时,我们得到一个新的 SVal,它引用当前绑定x 的值。这个值是符号;它是任何 x 绑定到函数的开始。这个符号我们称为 $0。类似地,我们评估 2 的表达式,并获得引用具体数字 2SVal。当我们计算 x * 2 时,我们取子表达式的两个 SVals,并创建一个新的 SVal 来表示它们的乘法(在这种情况下是一个新的符号表达式,我们可以称为 $1)。当我们评估 y 的赋值时,我们再次计算它的左值(一个 MemRegion),然后将 RHS(引用符号值 $1)的 SVal 绑定到符号存储中的 MemRegion。 第二行是类似的。当我们再次评估 x,同理,并创建一个引用符号 $0SVal。注意,两个 SVals 可能引用相同的底层值。

总而言之,MemRegions 是内存块的唯一名称。符号是抽象符号值的唯一名称。一些 MemRegions 表示存储器的抽象符号块,因此也基于符号。SVals 只是对值的引用,并且可以引用 MemRegions、符号或具体值(例如,数字1)。

一个 Checker 的想法

这里有几个问题,你应该考虑在评估你的 checker:

开发 checker,需要做出两个关键决定:

Checker 注册

所有 checker 实现文件都位于 clang/lib/StaticAnalyzer/Checkers 文件夹中。下面的步骤描述了如何检查流 API 的滥用的 checker SimpleStreamChecker 是否已注册到分析器。新的 checker 应该遵循类似的步骤。
  1. 在目录 lib/StaticAnalyzer/Checkers 中创建了一个新的 checker 实现文件 SimpleStreamChecker.cpp
  2. 以下注册代码已添加到实现文件中:
    void ento::registerSimpleStreamChecker(CheckerManager &mgr) {
      mgr.registerChecker<SimpleStreamChecker>();
    }
    
  3. 为 checker 选择了一个包,并且 checkers 在 lib/StaticAnalyzer/Checkers/Checkers.td 的 checkers 表中定义。由于所有 checkers 应首先开发为“alpha”,并且 SimpleStreamChecker 执行 UNIX API 检查,正确的包是“alpha.unix”,并且以下内容添加到 Checkers.td 的相应 UnixAlpha 部分:
    let ParentPackage = UnixAlpha in {
    ...
    def SimpleStreamChecker : Checker<"SimpleStream">,
      HelpText<"Check for misuses of stream APIs">,
      DescFile<"SimpleStreamChecker.cpp">;
    ...
    } // end "alpha.unix"
    
  4. 通过将源代码文件添加到 lib/StaticAnalyzer/Checkers/CMakeLists.txt,使源代码文件对 CMake 可见。.
在向分析器添加新 checker 后,可以通过查看是否在可用 checkers 列表中显示,来验证新 checker 是否已成功添加:
$clang -cc1 -analyzer-checker-help

事件、回调和 Checker 类结构

所有 checkers 都继承了 Checker 模板类;模板参数描述 checker 在处理中感兴趣的事件的类型。可用的各种类型的事件在 CheckerDocumentation.cpp 文件中描述

对于每个请求的事件类型,必须在 checker 类中定义一个相应的回调函数(CheckerDocumentation.cpp 显示每个事件类型的正确函数名和签名)。

例如,考虑 SimpleStreamChecker。此 checker 需按如下步骤:

将用于这些动作中的事件分别是 PreCallPostCallDeadSymbolsPointerEscape。checker 类的派生结构是:

class SimpleStreamChecker : public Checker<check::PreCall,
                                           check::PostCall,
                                           check::DeadSymbols,
                                           check::PointerEscape> {
public:

  void checkPreCall(const CallEvent &Call, CheckerContext &C) const;

  void checkPostCall(const CallEvent &Call, CheckerContext &C) const;

  void checkDeadSymbols(SymbolReaper &SR, CheckerContext &C) const;

  ProgramStateRef checkPointerEscape(ProgramStateRef State,
                                     const InvalidatedSymbols &Escaped,
                                     const CallEvent *Call,
                                     PointerEscapeKind Kind) const;
};

自定义程序状态

Checkers 通常需要跟踪他们执行的检查的特定信息。然而,由于 checkers 不能保证将探索程序的顺序,或者甚至将探索所有可能的路径,所以该状态信息不能保持在单个 checkers 内。因此,如果 checkers 需要存储自定义信息,他们需要向 ProgramState 添加新的数据类别。这样做的首选方法是使用为此而设计的几个宏中。他们是:

所有这些宏都将作为用于状态信息的自定义类别和用于存储的数据类型的名称的参数。指定的数据类型将成为操纵新类状态信息的方法的参数类型和/或返回类型。这些方法中的每一个都使用自定义数据类型的名称模板化。

例如,常见的情况是需要跟踪与符号表达式相关联的数据; map 类型是最合理的实现方式。此 map 的 key 是一个指向符号表达式(SymbolRef)的指针。如果要与符号表达式相关联的数据类型是整数,则状态信息的自定义类别将声明为

REGISTER_MAP_WITH_PROGRAMSTATE(ExampleDataType, SymbolRef, int)
将使用该函数访问数据
ProgramStateRef state;
SymbolRef Sym;
...
int currentlValue = state->get<ExampleDataType>(Sym);
并用该功能设置
ProgramStateRef state;
SymbolRef Sym;
int newValue;
...
ProgramStateRef newState = state->set<ExampleDataType>(Sym, newValue);

另外,宏定义用于存储新数据类别的数据的数据类型; 此类型的名称是带有“Ty”的数据类别的名称。对于 REGISTER_TRAIT_WITH_PROGRAMSTATE,这将简单地传递数据类型; 对于其他三个宏 llvm :: ImmutableListllvm :: ImmutableSetllvm :: ImmutableMap 模板类的专门版本。对于上面的 ExampleDataType 示例,创建的类型等价于写声明:

typedef llvm::ImmutableMap<SymbolRef, int> ExampleDataTypeTy;

这些宏将覆盖大多数用例; 然而,它们仍然有一些限制。它们不能在命名空间内使用(因为它们扩展为包含顶级命名空间引用),并且它们定义的数据类型不能从多个文件引用。

注意 ProgramStates 是不可变的; 而不是修改现有的状态,修改状态的函数将返回应用了更改的先前状态的副本。此更新的状态必须通过调用 CheckerContext::addTransition 函数提供给分析器核心。

错误报告

当 checker 检测到分析的代码中的错误时,它需要一种方式将其报告给分析器核心,以便可以显示它。用于构造此报告的两个类是 BugTypeBugReport

BugType,代表一种类型的错误。BugType 的构造函数有两个参数:错误类型的名称和错误类别的名称。这些在 scan-build 工具生成的摘要页面中使用。

BugReport 类表示一个特定的错误。在最常见的情况下,使用三个参数来形成 BugReport

  1. Bug 类型,指定为 BugType 类的实例。
  2. 一个简短的描述性字符串。这放置在由 scan-build 生成的详细逐行输出中的错误位置。
  3. 发生错误的上下文。这包括程序中的错误的位置和当到达位置时程序的状态。这些都封装在 ExplodedNode中。

为了获得正确的 ExplodedNode,必须决定分析是否可以沿着当前路径继续。该决定基于检测到的错误是否会阻止正在分析的程序继续。例如,资源的泄漏不应停止分析,因为程序可以在泄漏之后继续运行。另一方面,解除引用空指针应该停止分析,因为在这样的错误后程序没有办法有意义地继续。

如果分析可以继续,则由 checker 生成的最新 ExplodedNode 可以传递到 BugReport 构造函数,而无需进行其他修改。这个 ExplodedNode 将是最近一次调用 CheckerContext::addTransition返回的。如果在当前回调期间没有执行转换,则 checker 应调用 CheckerContext::addTransition() 并使用返回的节点进行错误报告。

如果分析不能继续,则当前状态应当被转换到所谓的 sink 节点,即不从中执行进一步分析的节点。这是通过调用 CheckerContext::generateSink 函数完成的; 此函数与 addTransition 函数相同,但将该状态标记为 sink 节点。像 addTransition,这返回一个具有更新的状态的 ExplodedNode,然后可以传递到 BugReport 构造函数。

BugReport 创建后,应通过调用 CheckerContext::emitReport 将其传递给分析器核心。

AST Visitors

一些检查可能不需要路径敏感性有效。简单的 AST 遍历可能就足够了。如果是这种情况,请考虑实现 Clang 编译器警告。另一方面,检查可能不能作为编译器警告接受;例如,由于相对高的误报率。在这种情况下,AST 回调 checkASTDeclcheckASTCodeBody 是您最好的朋友。

测试

每个补丁应该使用 Clang 回归测试进行良好测试。Checker 测试存在于 clang/test/Analysis 文件夹中。要运行所有分析器测试,请从 clang 构建目录执行以下命令:
    $ bin/llvm-lit -sv ../llvm/tools/clang/test/Analysis
    

有用的命令/调试提示

附加调试器

当您的命令包含 -cc1 标志时,可以直接将调试器附加到它:

    $ gdb --args clang -cc1 -analyze -analyzer-checker=core test.c
    $ lldb -- clang -cc1 -analyze -analyzer-checker=core test.c

否则,如果您的命令行包含 --analyze,实际的 clang 实例将在单独的进程中运行。为了调试它,使用 -### 标志获取子进程的命令行:

    $ clang --analyze test.c -\#\#\#

下面我们描述一些有用的命令行参数,所有这些参数都假定您正在运行 clang -cc1

缩小问题

在调查与 checker 相关的问题时,指示分析器只执行单个 checker:

    $ clang -cc1 -analyze -analyzer-checker=osx.KeychainAPI test.c

如果遇到崩溃,要查看处理大型文件时哪个函数失败,请使用 -analyzer-display-progress 选项。

您可以分析文件中的特定函数,这通常很有用,因为问题总是在某个函数中:

    $ clang -cc1 -analyze -analyzer-checker=core test.c -analyzer-display-progress
    ANALYZE (Syntax): test.c foo
    ANALYZE (Syntax): test.c bar
    ANALYZE (Path,  Inline_Regular): test.c bar
    ANALYZE (Path,  Inline_Regular): test.c foo
    $ clang -cc1 -analyze -analyzer-checker=core test.c -analyzer-display-progress -analyze-function=foo
    ANALYZE (Syntax): test.c foo
    ANALYZE (Path,  Inline_Regular): test.c foo

错误报告器机制当发现错误返回,并且无有用片段时,删除中间函数调用内的路径诊断。通常,通过添加自定义 BugReporterVisitor 对象来检查设计器是否生成更有趣的部分。但是,您可以在使用 -analyzer-config prune-paths=false 选项调试时禁用路径修剪。

可视化分析

AST dump,这通常有助于理解程序应该如何运行:

    $ clang -cc1 -ast-dump test.c

要查看/dump CFG使用 debug.ViewCFGdebug.DumpCFG checkers:

    $ clang -cc1 -analyze -analyzer-checker=debug.ViewCFG test.c

ExplodedGraph(由分析器探索的状态图)可以使用另一个调试 checker 进行可视化:

    $ clang -cc1 -analyze -analyzer-checker=debug.ViewExplodedGraph test.c

或者,等效地,使用 -analyzer-viz-egraph-graphviz 选项,它做同样的事情——在 graphviz .dot 格式中 dump 探索图。

您可以将 .dot 文件转换为其他格式——特别是,转换为 .svg 和在浏览器中查看可能比使用 .dot 查看器更舒适:

    $ dot -Tsvg ExprEngine-501e2e.dot -o ExprEngine-501e2e.svg

-trim-egraph 选项删除除了导致探索图 dump 的错误报告的其他路径。这是有用的,因为探索图通常是巨大的,很难浏览。

查看 ExplodedGraph 是理解分析器误报的最强大工具,因为它提供了分析器在所有分析路径上做出的每个决定的全面信息。

还有更多的调试 checkers 可用。要查看所有可用的调试 checkers:

    $ clang -cc1 -analyzer-checker-help | grep "debug"

调试打印和技巧

要在调试时查看“half-baked” ExplodedGraph,请跳转到具有 clang::ento::ExprEngine 对象的框架并执行:

    (gdb) p ViewGraph(0)

要在调试时查看 ProgramState,请使用以下命令。

    (gdb) p State->dump()

要在调试时查看 clang::Expr,请使用以下命令。如果传入 SourceManager 对象,它还将 dump 源代码中的相应行。

    (gdb) p E->dump()

要 dump 当前 ExplodedNode 所属方法的AST:

    (gdb) p C.getPredecessor()->getCodeDecl().getBody()->dump()

其他信息来源

以下是在 Clang 静态分析器上工作时有用的一些其他资源: