LLVM 之 Clang 静态分析器篇(3):检查器的整体工作机制

Author: stormQ

Created: Thursday, 08. April 2021 09:39PM

Last Modified: Thursday, 22. April 2021 10:26PM



摘要

本文基于release/12.x版本的 LLVM 源码,研究了 Clang 静态分析器中检查器的整体工作机制,并剖析了其实现的关键之处。从而,在扩展 Clang 静态分析器时可以了然于胸,也有助于理解 Clang 静态分析器的整体框架。


检查器之概述

检查器不仅仅是分析器引擎的被动接收者,还可以使用自定义的状态来扩展分析器引擎。

同一个检查器可以同时订阅多个不同的程序点。同一个程序点也可以同时被多个不同的检查器订阅。

每当识别出一个程序点时,分析器引擎就会调用订阅该程序点的所有检查器。


检查器类的实现

Clang 静态分析器是通过一组检查器对源码进行缺陷诊断的。本节我们介绍实现检查器类的一般过程,包括:订阅程序点和实现错误诊断代码逻辑。

step 1: 订阅程序点

1) 什么是程序点

程序点表示程序流中在语句之前或之后的特定节点。比如:在函数调用之前、在函数调用之后、在条件分支语句之前等都是程序点。

所有可以被检查器订阅的程序点见clang/lib/StaticAnalyzer/Checkers/CheckerDocumentation.cpp文件。

其中一个程序点为PreCall,表示在函数调用之前。其源码实现如下(定义在 clang/include/clang/StaticAnalyzer/Core/Checker.h 文件中):

class PreCall {
  template <typename CHECKER>
  static void _checkCall(void *checker, const CallEvent &msg,
                         CheckerContext &C) {
    ((const CHECKER *)checker)->checkPreCall(msg, C);
  }

public:
  template <typename CHECKER>
  static void _register(CHECKER *checker, CheckerManager &mgr) {
    mgr._registerForPreCall(
     CheckerManager::CheckCallFunc(checker, _checkCall<CHECKER>));
  }
};

从实现角度讲,一个程序点就是一个 C++ 类,并且不同程序点之间相互独立。意味着,它们之间不存在类继承等关系。

每个程序点都会调用一个由其规定的回调函数(这里是checkPreCall())。可以理解为,程序点负责为检查器提供信息(即回调函数的参数),而检查器利用这些信息实现错误诊断代码逻辑。

2) 订阅程序点

所谓的检查器订阅程序点,本质上就是检查器实现由所订阅程序点规定的回调函数。

每个具象检查器类都应该继承自类模板Checker<...>。其源码实现如下(定义在 clang/include/clang/StaticAnalyzer/Core/Checker.h 文件中):

template <typename CHECK1, typename... CHECKs>
class Checker : public CHECK1, public CHECKs..., public CheckerBase {
public:
  template <typename CHECKER>
  static void _register(CHECKER *checker, CheckerManager &mgr) {
    CHECK1::_register(checker, mgr);
    Checker<CHECKs...>::_register(checker, mgr);
  }
};

其关键之处为: 通过可变参数模板typename... CHECKs(即零个或多个模板参数),从而达到检查器可以同时订阅多个不同程序点的目的。

为了应对具象检查器类只订阅一个程序点的情况,该类模板实现了一个模板特例化。其源码实现如下:

template <typename CHECK1>
class Checker<CHECK1> : public CHECK1, public CheckerBase {
public:
  template <typename CHECKER>
  static void _register(CHECKER *checker, CheckerManager &mgr) {
    CHECK1::_register(checker, mgr);
  }
};

具象检查器类要想订阅程序点,只需要继承类模板Checker<...>,并将...替换为要订阅的程序点。然后,实现由所订阅程序点规定的回调函数。

推荐做法: 将具象检查器类的定义置于匿名空间中。从而,减少命名冲突。

订阅一个程序点时,具象检查器类的示例如下:

namespace {

class MainCallChecker : public Checker<check::PreCall>
{
public:
  void checkPreCall(const CallEvent &Call, CheckerContext &Ctx) const;
};

// anonymous namespace

注:这里只订阅了一个程序点check::PreCall。其规定的回调函数为void checkPreCall(const CallEvent &Call, CheckerContext &Ctx) const;

订阅多个程序点时,具象检查器类的示例如下:

namespace {

class MainCallChecker : public Checker<check::PreCall, check::PostCall>
{
public:
  void checkPreCall(const CallEvent &Call, CheckerContext &Ctx) const;
  void checkPostCall(const CallEvent &Call, CheckerContext &Ctx) const;
};

// anonymous namespace

注:这里订阅了两个程序点check::PreCallcheck::PostCall。后者规定的回调函数为void checkPostCall(const CallEvent &Call, CheckerContext &Ctx) const;

step 2: 实现错误诊断代码逻辑

1) 在哪实现错误诊断代码逻辑

我们应该在由所订阅程序点规定的回调函数中实现错误诊断代码逻辑。这些回调函数既决定了我们可以利用哪些信息用于错误诊断代码逻辑的实现,也决定了这些错误诊断代码何时被调用。

2) 如何实现错误诊断代码逻辑

虽然不同的检查器要实现的错误诊断代码逻辑的细节各自不同。但是,它们的实现通常包含如下几部分:读取回调函数的参数、判断是否满足检查器给定的不变量以及报告所检测到的错误。

回调函数checkPreCall()实现的示例如下:

void MainCallChecker::checkPreCall(const CallEvent &Call,
                                   CheckerContext &Ctx) const {
  if (const IdentifierInfo *II = Call.getCalleeIdentifier()) {
    if (II->isStr("main")) {
      if (!BT_) {
        BT_.reset(new BugType(this"Call to main""Example checker"));
      }
      ExplodedNode *Node = Ctx.generateErrorNode();
      auto Report
        = std::make_unique<PathSensitiveBugReport>(*BT_, BT_->getDescription(), Node);
      Ctx.emitReport(std::move(Report));
    }
  }
}

检查器注册函数的实现

在实现检查器类后,我们还需要实现检查器注册函数。从而,分析器引擎可以在适当的时候调用这些检查器。本节介绍了如何实现检查器注册函数以及这样做的原因。

step 1: 声明检查器注册函数

要声明检查器注册函数,只需要在实现检查器的源文件中包含如下语句:

#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"

这样做的原因如下。

头文件BuiltinCheckerRegistration.h的主要内容如下:

namespace clang {

class LangOptions;

namespace ento {

class CheckerManager;
class CheckerRegistry;

#define GET_CHECKERS
#define CHECKER(FULLNAME, CLASS, HELPTEXT, DOC_URI, IS_HIDDEN)                 \
  void register##CLASS(CheckerManager &mgr);                                   \
  bool shouldRegister##CLASS(const CheckerManager &mgr);
#include "clang/StaticAnalyzer/Checkers/Checkers.inc"
#undef CHECKER
#undef GET_CHECKERS

// end ento namespace

// end clang namespace

其关键之处为: 头文件BuiltinCheckerRegistration.h提供了仅对Checkers.inc文件生效的宏定义CHECKER的实现。

Checkers.inc文件是由工具TableGen根据Checkers.td文件的内容在编译 Clang 时生成的。编译完成后,该文件位于构建目录的子文件夹tools/clang/include/clang/StaticAnalyzer/Checkers/中。

Checkers.td文件的内容如下(部分):

def MainCallChecker : Checker<"MainCall">,
  HelpText<"Check for calls to main">,
  Documentation<NotDocumented>;

注:def后面的MainCallChecker就是用于描述检查器的定义名称。

编译 Clang 后,生成的Checkers.inc文件中的对应内容如下:

// This file is automatically generated. Do not edit this file by hand.
// 省略 ...

#ifdef GET_CHECKERS
// 省略 ...

CHECKER("alpha.core.MainCall", MainCallChecker, "Check for calls to main"""false)

// 省略 ...
#endif // GET_CHECKERS

// 省略 ...

由于头文件BuiltinCheckerRegistration.h在包含Checkers.inc文件之前已经定义了宏GET_CHECKERS。因此,如果在实现检查器的源文件中包含头文件BuiltinCheckerRegistration.h,那么实质上就是声明了所有检查器的注册函数。

需要实现哪些检查器注册函数是由头文件BuiltinCheckerRegistration.h中的宏定义CHECKER决定的。其宏定义如下:

#define CHECKER(FULLNAME, CLASS, HELPTEXT, DOC_URI, IS_HIDDEN)                 \
  void register##CLASS(CheckerManager &mgr);                                   \
  bool shouldRegister##CLASS(const CheckerManager &mgr);

从上面的代码可以看出,我们需要为每个检查器实现如下两个注册函数:

需要注意的是, 检查器注册函数名称的后半部分(即XXX部分)必须与Checkers.td文件中用于描述检查器的定义名称保持一致。这是因为,Checkers.td文件的内容决定了Checkers.inc文件中的CLASS名称,而CLASS名称正是检查器注册函数名称的后半部分。

step 2:实现检查器注册函数

虽然用于实现检查器的源文件包含了所有检查器的注册函数声明,但只需要在该源文件中实现一个检查器的所有注册函数。

其关键之处为: 从物理层面讲,不同的具象检查器类的实现被组织到不同的源文件中。

自定义检查器类MainCallChecker的注册函数实现如下:

void ento::registerMainCallChecker(CheckerManager &Mgr) {
  Mgr.registerChecker<MainCallChecker>();
}

bool ento::shouldRegisterMainCallChecker(const CheckerManager &mgr) {
  return true;
}

注:

需要注意的是, 具象检查器的类名(这里是MainCallChecker)的命名不受限制。也就是说,不必是Checkers.td文件中用于描述检查器的定义名称。


检查器注册函数的被调用过程

本节我们通过 GDB 调试 Clang 静态分析器来分析检查器注册函数的被调用过程,并由此着手检查器注册过程细节层面的研究。

step 1: 获取检查器注册函数的调用堆栈信息

1) 启动 GDB

a) 方式 1

$ gdb -q --args clang -cc1 -analyze -analyzer-checker=alpha.core.MainCall Example_Test.c

注:关于实现自定义检查器alpha.core.MainCall的详细过程,可参考笔者的另一篇文章:《LLVM 之 Clang 静态分析器篇(2):如何扩展 Clang 静态分析器》。

b) 方式 2

$ gdb -q --args clang --analyze -Xanalyzer -analyzer-checker=alpha.core.MainCall Example_Test.c

注:这里推荐使用第一种调试启动方式,从而只启用检查器alpha.core.MainCall,以缩小研究问题的范围。

2) 运行程序

(gdb) start

3) 设置断点

在我们要调试的检查器注册函数(这里是registerMainCallChecker())处设置断点。如下所示:

(gdb) b clang::ento::registerMainCallChecker

4) 继续执行,并查看堆栈信息

a) 继续执行

(gdb) c

b) 查看堆栈信息

(gdb) bt

打印的堆栈信息如下:

#0  clang::ento::registerMainCallChecker (Mgr=...) at /home/xxq/git-projects/llvm-project/clang/lib/StaticAnalyzer/Checkers/MainCallChecker.cpp:39
#1  0x00007fffe9f92ede in clang::ento::CheckerRegistry::initializeManager (this=0x7fffffffbb80, CheckerMgr=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/StaticAnalyzer/Frontend/CheckerRegistry.cpp:466
#2  0x00007fffe9fa210e in clang::ento::CheckerManager::CheckerManager(clang::ASTContext&, clang::AnalyzerOptions&, clang::Preprocessor const&, llvm::ArrayRef<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, llvm::ArrayRef<std::function<void (clang::ento::CheckerRegistry&)> >) (
    this=0x555555660c20, Context=..., AOptions=..., PP=..., plugins=..., checkerRegistrationFns=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/StaticAnalyzer/Frontend/CreateCheckerManager.cpp:30
#3  0x00007fffe9f000e9 in std::make_unique<clang::ento::CheckerManager, clang::ASTContext&, clang::AnalyzerOptions&, clang::Preprocessor&, llvm::ArrayRef<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&, std::vector<std::function<void (clang::ento::CheckerRegistry&)>, std::allocator<std::function<void (clang::ento::CheckerRegistry&)> > >&>(clang::ASTContext&, clang::AnalyzerOptions&, clang::Preprocessor&, llvm::ArrayRef<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&, std::vector<std::function<void (clang::ento::CheckerRegistry&)>, std::allocator<std::function<void (clang::ento::CheckerRegistry&)> > >&) () at /usr/include/c++/9/bits/unique_ptr.h:857
#4  0x00007fffe9e763b2 in (anonymous namespace)::AnalysisConsumer::Initialize (this=0x555555660690, Context=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp:211
#5  0x00007ffff109bf1f in clang::CompilerInstance::setASTConsumer (this=0x55555563b150, Value=std::unique_ptr<clang::ASTConsumer> = {...})
    at /home/xxq/git-projects/llvm-project/clang/lib/Frontend/CompilerInstance.cpp:131
#6  0x00007ffff11680de in clang::FrontendAction::BeginSourceFile (this=0x555555643780, CI=..., RealInput=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/Frontend/FrontendAction.cpp:893
#7  0x00007ffff10a1204 in clang::CompilerInstance::ExecuteAction (this=0x55555563b150, Act=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/Frontend/CompilerInstance.cpp:948
#8  0x00007ffff61ed6f3 in clang::ExecuteCompilerInvocation (Clang=0x55555563b150)
    at /home/xxq/git-projects/llvm-project/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp:278
#9  0x00005555555a7cd9 in cc1_main (Argv=..., Argv0=0x7fffffffe1fe "/usr/local/bin/clang", MainAddr=0x55555559a2fa <GetExecutablePath[abi:cxx11](char const*, bool)>)
    at /home/xxq/git-projects/llvm-project/clang/tools/driver/cc1_main.cpp:240
#10 0x000055555559bb9e in ExecuteCC1Tool (ArgV=...) at /home/xxq/git-projects/llvm-project/clang/tools/driver/driver.cpp:330
#11 0x000055555559c363 in main (argc_=5, argv_=0x7fffffffde48) at /home/xxq/git-projects/llvm-project/clang/tools/driver/driver.cpp:407

step 2: 分析检查器注册函数的调用堆栈信息

接下来,我们按照函数调用的先后顺序进行分析。

1) 调用堆栈的第 12 帧

#11 0x000055555559c363 in main (argc_=5, argv_=0x7fffffffde48) at /home/xxq/git-projects/llvm-project/clang/tools/driver/driver.cpp:407

从上面的堆栈信息可以看出,driver.cpp:407 行处的代码被调用了。其源码实现如下(定义在 clang/tools/driver/driver.cpp 中):

343 int main(int argc_, const char **argv_) {
// 省略 ...
401   if (FirstArg != argv.end() && StringRef(*FirstArg).startswith("-cc1")) {
// 省略 ...
407     return ExecuteCC1Tool(argv);
408   }
// 省略 ...
573 }

从上面的源码可以看出,最外层的调用者为 Clang 编译器驱动程序的main()函数,并且在其函数内部调用了ExecuteCC1Tool()函数。

通过 GDB 切换到该帧后,打印命令行参数argv_的值。结果如下:

(gdb) p argv_[0]
$1 = 0x7fffffffe1fe "/usr/local/bin/clang"
(gdb) p argv_[1]
$2 = 0x7fffffffe213 "-cc1"
(gdb) p argv_[2]
$3 = 0x7fffffffe218 "-analyze"
(gdb) p argv_[3]
$4 = 0x7fffffffe221 "-analyzer-checker=alpha.core.MainCall"
(gdb) p argv_[4]
$5 = 0x7fffffffe247 "Example_Test.c"
(gdb) p argv_[5]
$6 = 0x0

从上面的结果可以看出,Clang 编译器驱动程序在这里实际就是可执行目标文件/usr/local/bin/clang。并且,其命令行参数依次为"/usr/local/bin/clang""-cc1""-analyze""-analyzer-checker=alpha.core.MainCall""Example_Test.c"

2) 调用堆栈的第 11 帧

#10 0x000055555559bb9e in ExecuteCC1Tool (ArgV=...) at /home/xxq/git-projects/llvm-project/clang/tools/driver/driver.cpp:330

从上面的堆栈信息可以看出,driver.cpp:330 行处的代码被调用了。其源码实现如下(定义在 clang/tools/driver/driver.cpp 中):

316 static int ExecuteCC1Tool(SmallVectorImpl<const char *> &ArgV) {
// 省略 ...
327   StringRef Tool = ArgV[1];
328   void *GetExecutablePathVP = (void *)(intptr_t)GetExecutablePath;
329   if (Tool == "-cc1")
330     return cc1_main(makeArrayRef(ArgV).slice(1), ArgV[0], GetExecutablePathVP);
// 省略 ...
341 }

从上面的源码可以看出,在ExecuteCC1Tool()函数中调用了cc1_main()函数。

3) 调用堆栈的第 10 帧

#9  0x00005555555a7cd9 in cc1_main (Argv=..., Argv0=0x7fffffffe1f"/usr/local/bin/clang", MainAddr=0x55555559a2fa <GetExecutablePath[abi:cxx11](char const*, bool)>)
    at /home/xxq/git-projects/llvm-project/clang/tools/driver/cc1_main.cpp:240

从上面的堆栈信息可以看出,cc1_main.cpp:240 行处的代码被调用了。其源码实现如下(定义在 clang/tools/driver/cc1_main.cpp 中):

184 int cc1_main(ArrayRef<const char *> Argv, const char *Argv0, void *MainAddr) {
// 省略 ...
237   // Execute the frontend actions.
238   {
239     llvm::TimeTraceScope TimeScope("ExecuteCompiler");
240     Success = ExecuteCompilerInvocation(Clang.get());
241   }
// 省略 ...
274 }

从上面的源码可以看出,在cc1_main()函数中调用了ExecuteCompilerInvocation()函数。

通过 GDB 切换到该帧后,打印形参Argv的值。结果如下:

(gdb) p Argv[0]
$8 = (const char * const&) @0x7fffffffd528: 0x7fffffffe213 "-cc1"
(gdb) p Argv[1]
$9 = (const char * const&) @0x7fffffffd530: 0x7fffffffe218 "-analyze"
(gdb) p Argv[2]
$10 = (const char * const&) @0x7fffffffd538: 0x7fffffffe221 "-analyzer-checker=alpha.core.MainCall"
(gdb) p Argv[3]
$11 = (const char * const&) @0x7fffffffd540: 0x7fffffffe247 "Example_Test.c"

从上面的结果可以看出,函数cc1_main所使用的命令行参数依次为"-cc1""-analyze""-analyzer-checker=alpha.core.MainCall""Example_Test.c"。并且,这些参数的实际内存地址未发生变化。也就是说,这些命令行参数实际就是 Clang 编译器驱动程序的main函数中所使用的那些。

4) 调用堆栈的第 9 帧

#8  0x00007ffff61ed6f3 in clang::ExecuteCompilerInvocation (Clang=0x55555563b150)
    at /home/xxq/git-projects/llvm-project/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp:278

从上面的堆栈信息可以看出,ExecuteCompilerInvocation.cpp:278 行处的代码被调用了。其源码实现如下(定义在 clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp 中):

187 bool ExecuteCompilerInvocation(CompilerInstance *Clang) {
// 省略 ...
274   // Create and execute the frontend action.
275   std::unique_ptr<FrontendAction> Act(CreateFrontendAction(*Clang));
276   if (!Act)
277     return false;
278   bool Success = Clang->ExecuteAction(*Act);
// 省略 ...
281   return Success;
282 }

从上面的源码可以看出,在ExecuteCompilerInvocation()函数中调用了ExecuteAction()函数。

5) 调用堆栈的第 8 帧

#7  0x00007ffff10a1204 in clang::CompilerInstance::ExecuteAction (this=0x55555563b150, Act=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/Frontend/CompilerInstance.cpp:948

从上面的堆栈信息可以看出,CompilerInstance.cpp:948 行处的代码被调用了。其源码实现如下(定义在 clang/lib/Frontend/CompilerInstance.cpp:948 中):

 866 bool CompilerInstance::ExecuteAction(FrontendAction &Act) {
 // 省略 ...
 942   for (const FrontendInputFile &FIF : getFrontendOpts().Inputs) {
 943     // Reset the ID tables if we are reusing the SourceManager and parsing
 944     // regular files.
 945     if (hasSourceManager() && !Act.isModelParsingAction())
 946       getSourceManager().clearIDTables();
 947 
 948     if (Act.BeginSourceFile(*this, FIF)) {
 949       if (llvm::Error Err = Act.Execute()) {
 950         consumeError(std::move(Err)); // FIXME this drops errors on the floor.
 951       }
 952       Act.EndSourceFile();
 953     }
 954   }
 // 省略 ...
1005 }

从上面的源码可以看出,在CompilerInstance::ExecuteAction()函数中调用了BeginSourceFile()函数。

6) 调用堆栈的第 7 帧

#6  0x00007ffff11680de in clang::FrontendAction::BeginSourceFile (this=0x555555643780, CI=..., RealInput=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/Frontend/FrontendAction.cpp:893

从上面的堆栈信息可以看出,FrontendAction.cpp:893 行处的代码被调用了。其源码实现如下(定义在 clang/lib/Frontend/FrontendAction.cpp 中):

 548 bool FrontendAction::BeginSourceFile(CompilerInstance &CI,
 549                                      const FrontendInputFile &RealInput) {
 // 省略 ...
 893     CI.setASTConsumer(std::move(Consumer));
 894     if (!CI.hasASTConsumer())
 895       goto failure;
 896   }
 // 省略 ...
 940 }

从上面的源码可以看出,在FrontendAction::BeginSourceFile()函数中调用了setASTConsumer()函数。

7) 调用堆栈的第 6 帧

#5  0x00007ffff109bf1f in clang::CompilerInstance::setASTConsumer (this=0x55555563b150, Value=std::unique_ptr<clang::ASTConsumer> = {...})
    at /home/xxq/git-projects/llvm-project/clang/lib/Frontend/CompilerInstance.cpp:131

从上面的堆栈信息可以看出,CompilerInstance.cpp:131 行处的代码被调用了。其源码实现如下(定义在 clang/lib/Frontend/CompilerInstance.cpp 中):

 127 void CompilerInstance::setASTConsumer(std::unique_ptr<ASTConsumer> Value) {
 128   Consumer = std::move(Value);
 129 
 130   if (Context && Consumer)
 131     getASTConsumer().Initialize(getASTContext());
 132 }

从上面的源码可以看出,在CompilerInstance::setASTConsumer()函数中调用了Initialize()函数。

8) 调用堆栈的第 5 帧

#4  0x00007fffe9e763b2 in (anonymous namespace)::AnalysisConsumer::Initialize (this=0x555555660690, Context=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp:211

从上面的堆栈信息可以看出,AnalysisConsumer.cpp:211 行处的代码被调用了。其源码实现如下(定义在 clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp 中):

 67 namespace {
 68 
 69 class AnalysisConsumer : public AnalysisASTConsumer,
 70                          public RecursiveASTVisitor<AnalysisConsumer> {
// 省略 ...
209   void Initialize(ASTContext &Context) override {
210     Ctx = &Context;
211     checkerMgr = std::make_unique<CheckerManager>(*Ctx, *Opts, PP, Plugins,
212                                                   CheckerRegistrationFns);
213 
214     Mgr = std::make_unique<AnalysisManager>(*Ctx, PP, PathConsumers,
215                                             CreateStoreMgr, CreateConstraintMgr,
216                                             checkerMgr.get(), *Opts, Injector);
217   }
// 省略 ...
348 }; // namespace
349 } // end anonymous namespace

从上面的源码可以看出,匿名空间中的AnalysisConsumer::Initialize()函数分别实例化了CheckerManagerAnalysisManager对象。因此,这两个类的构造函数会依次被调用。

9) 调用堆栈的第 4 帧(不必分析)

#3  0x00007fffe9f000e9 in std::make_unique<clang::ento::CheckerManager, clang::ASTContext&, clang::AnalyzerOptions&, clang::Preprocessor&, llvm::ArrayRef<std::__cxx11::basic_string<charstd::char_traits<char>, std::allocator<char> > >&, std::vector<std::function<void (clang::ento::CheckerRegistry&)>, std::allocator<std::function<void (clang::ento::CheckerRegistry&)> > >&>(clang::ASTContext&, clang::AnalyzerOptions&, clang::Preprocessor&, llvm::ArrayRef<std::__cxx11::basic_string<charstd::char_traits<char>, std::allocator<char> > >&, std::vector<std::function<void (clang::ento::CheckerRegistry&)>, std::allocator<std::function<void (clang::ento::CheckerRegistry&)> > >&) () at /usr/include/c++/9/bits/unique_ptr.h:857

10) 调用堆栈的第 3 帧

#2  0x00007fffe9fa210e in clang::ento::CheckerManager::CheckerManager(clang::ASTContext&, clang::AnalyzerOptions&, clang::Preprocessor const&, llvm::ArrayRef<std::__cxx11::basic_string<charstd::char_traits<char>, std::allocator<char> > >, llvm::ArrayRef<std::function<void (clang::ento::CheckerRegistry&)> >) (
    this=0x555555660c20, Context=..., AOptions=..., PP=..., plugins=..., checkerRegistrationFns=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/StaticAnalyzer/Frontend/CreateCheckerManager.cpp:30

从上面的堆栈信息可以看出,CreateCheckerManager.cpp:30 行处的代码被调用了。其源码实现如下(定义在 clang/lib/StaticAnalyzer/Frontend/CreateCheckerManager.cpp 中):

 17 namespace clang {
 18 namespace ento {
 19 
 20 CheckerManager::CheckerManager(
 21     ASTContext &Context, AnalyzerOptions &AOptions, const Preprocessor &PP,
 22     ArrayRef<std::string> plugins,
 23     ArrayRef<std::function<void(CheckerRegistry &)>> checkerRegistrationFns)
 24     : Context(&Context), LangOpts(Context.getLangOpts()), AOptions(AOptions),
 25       PP(&PP), Diags(Context.getDiagnostics()),
 26       RegistryData(std::make_unique<CheckerRegistryData>()) 
{
 27   CheckerRegistry Registry(*RegistryData, plugins, Context.getDiagnostics(),
 28                            AOptions, checkerRegistrationFns);
 29   Registry.initializeRegistry(*this);
 30   Registry.initializeManager(*this);
 31   finishedCheckerRegistration();
 32 }
 // 省略 ...
 49 } // namespace ento
 50 } // namespace clang

从上面的源码可以看出,在CheckerManager::CheckerManager()构造函数中调用了initializeManager()函数。

11) 调用堆栈的第 2 帧

#1  0x00007fffe9f92ede in clang::ento::CheckerRegistry::initializeManager (this=0x7fffffffbb80, CheckerMgr=...)
    at /home/xxq/git-projects/llvm-project/clang/lib/StaticAnalyzer/Frontend/CheckerRegistry.cpp:466

从上面的堆栈信息可以看出,CheckerRegistry.cpp:466 行处的代码被调用了。其源码实现如下(定义在 clang/lib/StaticAnalyzer/Frontend/CheckerRegistry.cpp 中):

462 void CheckerRegistry::initializeManager(CheckerManager &CheckerMgr) const {
463   // Initialize the CheckerManager with all enabled checkers.
464   for (const auto *Checker : Data.EnabledCheckers) {
465     CheckerMgr.setCurrentCheckerName(CheckerNameRef(Checker->FullName));
466     Checker->Initialize(CheckerMgr);
467   }
468 }

从上面的源码可以看出,在CheckerRegistry::initializeManager()函数中调用了Initialize()函数。

12) 调用堆栈的第 1 帧

#0  clang::ento::registerMainCallChecker (Mgr=...) at /home/xxq/git-projects/llvm-project/clang/lib/StaticAnalyzer/Checkers/MainCallChecker.cpp:39

从上面的堆栈信息可以看出,MainCallChecker.cpp:39 行处的代码被调用了。即我们实现的检查器注册函数clang::ento::registerMainCallChecker()被调用了。

到这里,想必已经清楚检查器注册函数被调用的基本过程了吧。


检查器的注册过程

本节介绍了要注册的检查器是从何而来的、注册成功需要满足的条件以及注册过程的实现。

step 1: 要注册的检查器是从何而来的

所有可以注册的检查器就是Checkers.td文件中所描述的那些检查器。而要注册的检查器就是通过-analyzer-checker标志指定的检查器。

注:这里直接给出了结论,具体研究过程可参考笔者的如下几篇文章:

step 2: 检查器注册成功需要满足的条件

一个检查器要注册成功需要同时满足以下两个条件:

注:这里直接给出了结论,具体研究过程可参考笔者的如下几篇文章:

step 3: 检查器注册过程的实现

1) 从哪着手研究

在上一节中我们有如下疑惑:在调用堆栈的第 2 帧中,为什么在调用Initialize()函数后下一帧就直接进入了我们实现的检查器注册函数clang::ento::registerMainCallChecker()

a) Initialize()实际是哪个函数

通过 GDB 切换到调用堆栈的第 2 帧后,打印Initialize()函数的值。结果如下:

(gdb) p/x Checker->Initialize
$12 = 0x7fffe3663742

查看地址0x7fffe3663742对应的符号:

(gdb) info symbol 0x7fffe3663742
clang::ento::registerMainCallChecker(clang::ento::CheckerManager&) in section .text of /usr/local/bin/../lib/../lib/../lib/libclangStaticAnalyzerCheckers.so.12

从上面的结果可以看出,Initialize()函数正是我们实现的检查器注册函数clang::ento::registerMainCallChecker()

调用Initialize()函数的语句为Checker->Initialize(CheckerMgr);。那么,这里的Checker又是什么。

b) 临时变量Checker

通过 GDB 查看临时变量Checker的数据类型。如下:

(gdb) whatis Checker
type = const clang::ento::CheckerInfo *

从上面的结果可以看出,临时变量Checker的数据类型为const clang::ento::CheckerInfo *

而临时变量Checker实际是Data.EnabledCheckers对象中的一个元素。

通过 GDB 查看Data.EnabledCheckers对象的数据类型。如下:

(gdb) whatis Data.EnabledCheckers
type = clang::ento::CheckerInfoSet

从上面的结果可以看出,Data.EnabledCheckers对象的数据类型为clang::ento::CheckerInfoSet

因此,我们可以从Data.EnabledCheckers对象中的元素来源入手,进而研究检查器是如何注册的。

通过搜索源码发现, Data.EnabledCheckers对象添加元素的操作仅发生在CheckerRegistry::initializeRegistry()函数中。因此,我们只需要研究该函数的源码实现以及被调用的过程即可。

2) 分析CheckerRegistry::initializeRegistry()函数

a) CheckerRegistry::initializeRegistry()函数被调用的地方有哪些

通过搜索源码发现, 该函数仅在CheckerManager::CheckerManager()构造函数中会被调用。而后者被调用的地方即上一节中所分析的调用堆栈的第 3 帧。也就是说,其调用过程我们已经在上文中分析过了。

b) CheckerRegistry::initializeRegistry()函数的作用

CheckerRegistry中的成员函数initializeRegistry()的作用为:收集所有启用的检查器及其所有依赖的检查器。依赖的检查器包括如下几种:

注:这里直接给出了结论,具体研究过程可参考笔者的另一篇文章:《LLVM 之 Clang 源码分析篇(3):clang::ento::CheckerRegistry 类》。

3) 分析检查器注册函数registerXXX()背后的实现

检查器注册函数registerXXX()的一般实现如下:

void ento::registerMainCallChecker(CheckerManager &Mgr) {
  Mgr.registerChecker<MainCallChecker>();
}

clang::ento::CheckerManager中的成员函数registerChecker()的作用为:创建指定的检查器类实例,并注册其订阅的程序点。

注:这里直接给出了结论,具体研究过程可参考笔者的另一篇文章:《LLVM 之 Clang 源码分析篇(4):clang::ento::CheckerManager 类》。

其关键之处为: 通过将检查器类作为模板参数,从而实现了一个可以创建任意检查器类实例的工厂函数。

到这里,检查器的注册工作就真正地完成了。


总结:检查器的整体工作机制

从设计层面看,Clang 静态分析器运用观察者模式实现了(一个或多个)检查器订阅(一个或多个)程序点的功能。

Checkers.td文件中描述了所有可以被注册的检查器以及它们之间的依赖关系等。在编译 Clang 时,工具TableGen会根据Checkers.td文件的内容生成相应地Checkers.inc文件。Checkers.inc文件中包含了一系列的宏调用,比如CHECKERCHECKER_DEPENDENCY等。不同地方的源代码通过提供这些宏的不同实现,从而达到Checkers.inc文件一途多用的目的。

我们可以通过-analyzer-checker标志启用检查器。然而,检查器最终是否会被启用,还受其他因素的影响,比如:检查器注册函数shouldRegisterXXX()的实现、强依赖的检查器是否满足等。如果一个检查器最终被启用了,那么意味着其依赖的检查器也会被尽可能多地启用。

对于最终需要启用的检查器,我们需要对这些检查器进行注册。从而,分析器引擎可以在适当的时候调用这些检查器以实现缺陷诊断功能。


References


下一篇:LLVM 之 Clang 静态分析器篇(4):程序缺陷诊断——除零错误

上一篇:LLVM 之 Clang 静态分析器篇(2):如何扩展 Clang 静态分析器

首页