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

Author: stormQ

Created: Tuesday, 06. April 2021 09:41PM

Last Modified: Saturday, 15. May 2021 08:36PM



摘要

本文通过实现一个简单的检查器介绍了扩展 Clang 静态分析器的两种不同方式:在源码中扩展以插件方式扩展。从而,为更复杂场景下的 Clang 静态分析器扩展做好准备。


如何在源码中扩展 Clang 静态分析器

本节在 Linux 系统中实现了一个简单的检查器——检查用户是否调用了main函数,并在 Clang 源码中进行扩展。

step 1: 添加自定义的检查器

1) 方式 1——在已有的检查器包中添加

如果要在已有的检查器包——alpha.core中添加自定义的检查器,那么在Checkers.td文件中添加如下内容:

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

修改后的内容如下:

let ParentPackage = CoreAlpha in {

// 省略 ...

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

// end "alpha.core"

注:

需要注意的是, 修改的Checkers.td文件在重新编译安装 Clang 后才生效。

step 2: 实现自定义的检查器

1) 添加检查器的实现代码

clang/lib/StaticAnalyzer/Checkers/目录中新建MainCallChecker.cpp文件,内容如下:

#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/CheckerManager.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"

using namespace clang;
using namespace ento;

namespace {

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

private:
  mutable std::unique_ptr<BugType> BT;
};

// anonymous namespace

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));
    }
  }
}

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

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

注:

2) 修改 CMakeLists.txt

clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt文件中添加如下内容:

MainCallChecker.cpp

注:MainCallChecker.cpp即上文中新建的用于存放检查器实现代码的源文件。

修改后的内容如下:

add_clang_library(
  # 省略 ...
  MainCallChecker.cpp
  # 省略 ...
)

step 3: 编译安装 Clang

如果你已经编译并安装过 Clang,那么在构建目录中执行如下命令:

$ ninja clang
$ sudo ninja install

注:如果你还未编译并安装过 Clang,那么可参考笔者的另一篇文章《LLVM 之 Clang 篇(1):如何从源码构建并安装 Clang》。

重新编译安装 Clang 后,执行如下命令查看自定义的检查器:

$ clang -cc1 -analyzer-checker-help-alpha | grep MainCall
  alpha.core.MainCall           (Enable only for development!) Check for calls to main

从上面的输出可以看出:

step 4: 运行自定义的检查器

1) 测试程序

测试程序——Example_Test.c的内容如下:

typedef int (*main_t)(intchar **);
int main(int argc, char **argv) {
  main_t foo = main;
  int exit_code = foo(argc, argv);   // actually calls main()!
  return exit_code;
}

上述测试程序存在这样的错误:通过函数指针变量foo调用main函数。

2) 运行自定义的检查器

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

注:关于 Clang 静态分析器使用方法的更多内容,可参考笔者的另一篇文章《LLVM 之 Clang 静态分析器篇(1):如何使用 Clang 静态分析器》。

执行上述命令后,输出如下:

Example_Test.c:4:19: warning: Call to main [alpha.core.MainCall]
  int exit_code = foo(argc, argv);   // actually calls main()!
                  ^~~~~~~~~~~~~~~
1 warning generated.

从上面的输出可以看出,我们自定义的检查器检测到了用户调用main函数的错误。


如何以插件方式扩展 Clang 静态分析器

本节介绍了如何将上一节中的检查器以插件方式扩展 Clang 静态分析器。

step 1: 实现自定义的检查器

1) 添加检查器的实现代码

clang/lib/Analysis/plugins目录中新建CheckerMainCall目录。并在该目录下新建MainCallChecker.cpp文件,内容如下:

#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h"

using namespace clang;
using namespace ento;

namespace {

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

private:
  mutable std::unique_ptr<BugType> BT;
};

// anonymous namespace

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));
    }
  }
}

// Register plugin!
extern "C" void clang_registerCheckers(CheckerRegistry &registry) {
  registry.addChecker<MainCallChecker>(
      "plugin.MainCallChecker""Disallows calls to functions called main",
      "");
}

extern "C" const char clang_analyzerAPIVersionString[] =
    CLANG_ANALYZER_API_VERSION_STRING;

注:

2) 添加 MainCallCheckerPlugin.exports

在目录clang/lib/Analysis/plugins/CheckerMainCall/中新建MainCallCheckerPlugin.exports文件。其内容如下:

clang_registerCheckers
clang_analyzerAPIVersionString

注:

3) 添加 CMakeLists.txt

在目录clang/lib/Analysis/plugins/CheckerMainCall/中新建CMakeLists.txt文件。其内容如下:

set(LLVM_LINK_COMPONENTS
  Support
  )

set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/MainCallCheckerPlugin.exports)
add_llvm_library(MainCallCheckerPlugin MODULE BUILDTREE_ONLY MainCallChecker.cpp PLUGIN_TOOL clang)

clang_target_link_libraries(MainCallCheckerPlugin PRIVATE
  clangAnalysis
  clangAST
  clangStaticAnalyzerCore
  clangStaticAnalyzerFrontend
  )

注:

step 2: 修改插件目录中的 CMakeLists.txt

clang/lib/Analysis/plugins/CMakeLists.txt文件中添加如下内容:

add_subdirectory(CheckerMainCall)

注: 这里的CheckerMainCall即为上文中新建的目录名称。

添加后的内容为:

if(CLANG_ENABLE_STATIC_ANALYZER AND LLVM_ENABLE_PLUGINS)
  # 省略 ...
  add_subdirectory(CheckerMainCall)
endif()

需要注意的是, 要生成插件,需要在编译 Clang 时打开CLANG_ENABLE_STATIC_ANALYZERLLVM_ENABLE_PLUGINS选项。默认情况下,这两个选项是打开的。

step 3: 编译插件

如果你已经编译并安装过 Clang,那么在构建目录中执行如下命令:

$ cmake -DLLVM_TARGETS_TO_BUILD="X86;AArch64;RISCV" -DLLVM_ENABLE_PROJECTS=clang -DBUILD_SHARED_LIBS=ON -G Ninja ../llvm/
$ ninja -j8

注:如果你还未编译并安装过 Clang,那么可参考笔者的另一篇文章《LLVM 之 Clang 篇(1):如何从源码构建并安装 Clang》。

需要注意的是, 这里的编译命令不能是ninja clang

编译完成后,执行如下命令查看所生成插件中的检查器:

$ clang -cc1 -load ./lib/MainCallCheckerPlugin.so -analyzer-checker-help | grep MainCall
  plugin.MainCallChecker        Disallows calls to functions called main

从上面的输出可以看出:

需要注意的是, 插件的路径(这里是./lib/MainCallCheckerPlugin.so)可以是相对路径,也可以是绝对路径。

step 4: 运行插件

以插件方式运行检查器,命令如下:

$ clang -cc1 -load ~/git-projects/llvm-project/build_ninja/lib/MainCallCheckerPlugin.so -analyze -analyzer-checker=plugin.MainCallChecker Example_Test.cpp

执行上述命令后,输出如下:

Example_Test.cpp:2:19: warning: Call to main [plugin.MainCallChecker]
  int exit_code = main(argc, argv);
                  ^~~~~~~~~~~~~~~~
1 warning generated.

从上面的输出可以看出,我们以插件方式自定义的检查器检测到了用户调用main函数的错误。


References


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

上一篇:LLVM 之 Clang 静态分析器篇(1):如何使用 Clang 静态分析器

首页