clang static analyzer 自定义规则开发配置及入门

举报
maijun 发表于 2023/02/12 17:19:37 2023/02/12
【摘要】 本文是clang static analyzer 自定义规则开发的基础教程,主要会介绍下面的几点内容:① 基于 clion + wsl + ubuntu 20.04 的 clang static analyzer 自定义规则开发环境的搭建;② 一种基于插件方式独立开发 clang static analyzer 自定义规则的实践;③ 规则开发示例。

本文属于 clang static analyzer 自定义规则开发的基础教程,主要会介绍下面的几点内容:

  • 基于 clion + wsl + ubuntu 20.04 的 clang static analyzer 自定义规则开发环境的搭建;
  • 一种独立开发 clang static analyzer 自定义规则的实践(自定义规则单独开发和构建,不需要放到 llvm-project 的源码中);
  • 一个单独的规则开发示例。

1. 环境搭建

主要需要准备的内容如下:

在 clion 中,新建 c++ cmake 项目,唯一和其他开发方式不同的,是需要设置使用的工具链

在首页通过 File -> Settings -> Build -> Cmake 打开上面的设置框。

我自己之前,默认使用的工具链是 cygwin(应该是我安装过,我现在是直接挪去了),可以点 + 号,新增 wsl 工具链。其他选择默认即可。

2. 自定义规则项目目录及配置

本文介绍一个独立开发 clang static analyzer 自定义规则项目的开发方式。现在很多文章在介绍 clang static analyzer 自定义规则开发时,都是将自定义规则嵌入到 llvm-project 源码中,因此需要维护整个 llvm-project 项目,并且构建时,需要构建整个项目,增加了构建时间,增加了维护复杂度。因此本文介绍的项目开发方式,还是有一定参考价值的。

clang static analyzer 自定义规则,包含两种开发方式:一种是源码适配,需要修改 Checkers.td 文件,另一种是插件方式,本文介绍的是基于插件方式开发。

2.1 项目的组织方式

项目目录格式:

custom-csa-rule-example/
├── CMakeLists.txt
├── Readme.md
├── doc
│   └── help.md
└── source
    ├── CMakeLists.txt
    ├── category_1
    │   ├── CMakeLists.txt
    │   ├── CustomCSARuleCategory1.exports
    │   ├── MainCallChecker.cpp
    │   ├── MainCallChecker.h
    │   └── Register.cpp
    ├── category_2
    │   ├── CMakeLists.txt
    │   ├── CheckerOptionHandling.cpp
    │   ├── CheckerOptionHandling.h
    │   ├── CustomCSARuleCategory2.exports
    │   └── Register.cpp
    └── common

如上,是个典型的 cmake 项目,主要项目相关的说明如下:

  • 关键的业务代码在 source 下面,因为本项目内仅仅是 clang static analyzer 规则,并不是以库的形式给外部使用的,因此当前没有设计根路径下的 include,如果有必要,可以自己添
  • 在 source 下面,有两个不同的文件夹,category_1 和 category_2 两个目录,两个文件夹用来组织不同类型的规则,比如可以是基于不同的语言的维度(比如 c/c++/objc 等),可以是检查场景(比如 代码风格、代码性能、安全 等)维度,可以自由地扩展,比如新增子目录类型或者横向新增类型;
  • 构建时,每个 category 都会形成一个单独的动态库,方便使用;
  • 在上面,不同的 category_1 和 category_2 是独立构建的,两者不想关,因此两个目录下 Register.cpp 我设置了同名,并且里面有实现同名函数,其实构建不会报错,大家如果想区分,可以修改不同的目录下的 Register.cpp 的名字加以区分;
  • common 目录是设计用来放工具类或者函数的目录。

2.2 项目可以独立开发和构建说明

2.2.1 将llvm+clang作为依赖引入

如果将clang static analyzer 的自定义规则作为 llvm-project 的一部分,自然可以直接构建,但是如果我们将 clang static analyzer 的自定义规则独立开发,就需要将 llvm + clang 作为依赖引入,这些配置都是在根目录下面的 CMakeLists.txt 中配置的,该文件的内容如下:

CMAKE_MINIMUM_REQUIRED(VERSION 3.16)

PROJECT(custom-csa-rule-example)

IF(DEFINED LLVM_DIR)
    SET(ENV{LLVM_DIR} LLVM_DIR)
    SET(INPUT_LLVM_DIR LLVM_DIR)
ENDIF()

IF(DEFINED ENV{LLVM_DIR})
    SET(CMAKE_CXX_STANDARD 14)

    IF (CMAKE_BUILD_TYPE MATCHES "Debug")
        SET(CMAKE_CXX_FLAGS "-fPIC -std=gnu++14 -O0 -fno-rtti -Wno-deprecated \
                               -DHAVE_IEEE_754 -DSIZEOF_VOID_P=8 -DSIZEOF_LONG=8")
    ELSE()
        SET(CMAKE_CXX_FLAGS "-fPIC -std=gnu++14 -O3 -fno-rtti -Wno-deprecated \
                               -DHAVE_IEEE_754 -DSIZEOF_VOID_P=8 -DSIZEOF_LONG=8 -DNDEBUG")
    ENDIF()

    SET(CMAKE_C_FLAGS "-fPIC")

    FIND_PACKAGE(LLVM REQUIRED CONFIG)
    LIST(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
    INCLUDE(AddLLVM)

    ADD_DEFINITIONS("${LLVM_DEFINATIONS}")
    INCLUDE_DIRECTORIES("${LLVM_INCLUDE_DIRS}")
ELSE()
    MESSAGE(FATAL_ERROR "please set LLVM_DIR")
ENDIF()

ADD_SUBDIRECTORY(source)

说明:在使用前,需要先设置 LLVM_DIR(该环境变量就是前置步骤中安装的 llvm 的根路径),可以通过两种方式设置:① 设置为环境变量;② 在 cmake 构建时通过 -DLLVM_DIR=... 设置。

如上,通过配置的 LLVM_DIR,可以在下面找到 CONFIG 信息,从而,补齐 LLVM_CMAKE_DIR、LLVM_DEFINATIONS、LLVM_INCLUDE_DIRS 等目录信息,这样就可以在构建中添加各种 llvm 和 clang 的各种依赖了。

2.2.2 通过exports文件导出规则信息

这里主要有三个地方的配置:

① 在 Register.cpp 中添加动态库导出信息

#include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h"

#include "MainCallChecker.h"

using namespace clang;
using namespace ento;

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

extern "C"
#ifdef _WINDOWS
_declspec(dllexport)
#endif
const char clang_analyzerAPIVersionString[] =
        CLANG_ANALYZER_API_VERSION_STRING;

如上,重点是导出函数 clang_registerCheckers 和变量 clang_analyzerAPIVersionString:

  • clang_registerCheckers 中完成规则的注册,同时,规则参数、规则之间的依赖关系等都在该函数中完成;
  • clang_analyzerAPIVersionString 表示该自定义规则和构建时用的 clang 版本保持一致(就是 LLVM_DIR 中如果是 15.0.7,就不能使用其他的,比如 10.x 的clang 来使用构建出来的规则)。

② 在 exports 文件中设置上面的导出信息

例如,CustomCSARuleCategory1.exports 文件的内容为:

clang_registerCheckers
clang_analyzerAPIVersionString

都写成一样的内容就好了。

③ 在当前的自定义规则 category 文件夹中的 CMakeLists.txt 中,添加 exports 相关信息

FILE(GLOB_RECURSE hdrs "*.h" "../common/*.h")
FILE(GLOB_RECURSE srcs "*.cpp" "../common/*.cpp")

SET(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/CustomCSARuleCategory1.exports)
ADD_LIBRARY(CustomCSARuleCategory1 MODULE ${srcs})

IF (LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
    TARGET_LINK_LIBRARIES(CustomCSARuleCategory1 PRIVATE
            clangAnalysis
            clangAST
            clangStaticAnalyzerCore
            LLVMSupport
            )
ENDIF ()

如上,第四行添加了 LLVM_EXPORTED_SYMBOL_FILE 配置为 exports 文件,

这样操作之后,最后构建的时候,就可以只构建上面的自定义规则,并且生成 so 库,只是在 clang 分析时,加载 so 库即可。

3. 自定义规则举例

本文示例的自定义规则,直接来自于 llvm-project 的原例子,代码目录在 clang/lib/Analysis/plugins/SampleAnalyzer 下的 MainCallChecker。只是对代码结构做了调整。当前不涉及代码内部的开发,涉及到的是开发步骤,后面要是有时间,会简单分几部分梳理一下 clang static analyzer 不同类型的规则、引擎源码实现机制等内容。

3.1 规则源码简单介绍

 这里对 llvm-project 的源码做的调整,主要是将 MainCallChecker.cpp 拆成了 MainCallChecker.h 和 MainCallChecker.cpp 两个,头文件中是定义,cpp 中是实现,从而方便在 Register.cpp 中进行注册。

主要的各个文件的内容:

  • CMakeLists.txt: 可以直接参考 2.2.2 节 第③点的文件介绍;
  • exports 文件:可以直接参考 2.2.2 节 第②点的文件介绍;
  • Register.cpp 文件:可以直接参考 2.2.2 节第①点的文件介绍;
  • MainCallChecker.h 文件:规则类的声明,核心内容如下:
class MainCallChecker : public Checker<check::PreStmt<CallExpr>> {
    mutable std::unique_ptr<BugType> BT;

public:
    void checkPreStmt(const CallExpr *CE, CheckerContext &C) const;
};
  • MainCallChecker.cpp 文件,规则类的实现,内容如下:
#include "MainCallChecker.h"

using namespace clang;
using namespace ento;

void MainCallChecker::checkPreStmt(const CallExpr *CE,
                                   CheckerContext &C) const {
    const Expr *Callee = CE->getCallee();
    const FunctionDecl *FD = C.getSVal(Callee).getAsFunctionDecl();

    if (!FD)
        return;

    // Get the name of the callee.
    IdentifierInfo *II = FD->getIdentifier();
    if (!II) // if no identifier, not a simple C function
        return;

    if (II->isStr("main")) {
        ExplodedNode *N = C.generateErrorNode();
        if (!N)
            return;

        if (!BT)
            BT.reset(new BugType(this, "call to main", "example analyzer plugin"));

        auto report =
                std::make_unique<PathSensitiveBugReport>(*BT, BT->getDescription(), N);
        report->addRange(Callee->getSourceRange());
        C.emitReport(std::move(report));
    }
}

3.2 构建规则源码

下面给出来一个比较简单的构建脚本

mkdir build && cd build
cmake -DLLVM_DIR=${LLVM_INSTALL_PATH}/lib/cmake/llvm ..
make -j8

说明:

这样,构建完成后,在 build 目录下面,可以在 source/category_1 下面,找到 libCustomCSARuleCategory1.so 文件,就是我们需要使用的结果了。

3.3 自定义规则测试

下面介绍三个 clang 的关于代码检查的命令:

# 查看clang支持的原生规则的列表
clang -cc1 -analyzer-checker-help

# 查看clang支持的原生规则和自定义规则的列表
# 下面的 -load 后面,是 前面生成的自定义规则的so库的路径
clang -cc1 -analyzer-checker-help -load xx.so

# 执行分析,需要关注两个参数的值:
# -fplugin 的值是前面生成的自定义规则的so库的路径,也可以使用 -load xx.so
# -analyzer-checker后面是使用的规则列表
clang -c --analyzer -fplugin=xx.so -Xanalyzer -analyzer-checker=checker-list

举例:

maijun@LAPTOP-EEQ9H1QS:/mnt/d/wsl/test/csa$ ll
total 28
drwxrwxrwx 1 maijun maijun  4096 Feb 12 16:33 ./
drwxrwxrwx 1 maijun maijun  4096 Feb 12 15:40 ../
-rwxrwxrwx 1 maijun maijun 28576 Feb 12 16:33 libCustomCSARuleCategory1.so*
-rwxrwxrwx 1 maijun maijun   151 Feb 12 15:42 test_main_call.c*
maijun@LAPTOP-EEQ9H1QS:/mnt/d/wsl/test/csa$ /mnt/d/wsl/tools/llvm-15.0.7.obj/bin/clang -cc1 -analyzer-checker-help -load ./libCustomCSARuleCategory1.so  | grep example.MainCallChecker
  example.MainCallChecker       Disallows calls to functions called main
maijun@LAPTOP-EEQ9H1QS:/mnt/d/wsl/test/csa$ /mnt/d/wsl/tools/llvm-15.0.7.obj/bin/clang -c -fplugin=./libCustomCSARuleCategory1.so --analyze -Xanalyzer -analyzer-checker=example.
MainCallChecker test_main_call.c
test_main_call.c:6:15: warning: call to main [example.MainCallChecker]
    int ret = foo(argc, argv);
              ^~~~~~~~~~~~~~~
1 warning generated.

3.4 源码

本文示例的源码,在 maijun-sec/custom-csa-rule-example (github.com),大家可以参考或修改之后使用。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。