clang static analyzer 自定义规则开发配置及入门
本文属于 clang static analyzer 自定义规则开发的基础教程,主要会介绍下面的几点内容:
- 基于 clion + wsl + ubuntu 20.04 的 clang static analyzer 自定义规则开发环境的搭建;
- 一种独立开发 clang static analyzer 自定义规则的实践(自定义规则单独开发和构建,不需要放到 llvm-project 的源码中);
- 一个单独的规则开发示例。
1. 环境搭建
主要需要准备的内容如下:
- wsl2 + ubuntu 20.07,安装参考:Win10+WSL2+Ubuntu20.04+Docker+SonarQube实践-云社区-华为云 (huaweicloud.com)
- gcc 9.x + cmake + make,均可以直接通过 apt install 安装
- llvm + clang 15.0.7,编译方式参考:在 ubuntu 20.04 中源码编译 llvm-clang 15.0.7-云社区-华为云 (huaweicloud.com)
- clion 工具
在 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 ®istry) {
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
说明:
- 上面的 LLVM_INSTALL_PATH 是 llvm 的安装路径,对应了 在 ubuntu 20.04 中源码编译 llvm-clang 15.0.7-云社区-华为云 (huaweicloud.com) 中的 cmake 参数 LLVM_INSTALL_PREFIX;
- 可以同时给 cmake 添加其他的参数,也可以同步参考 在 ubuntu 20.04 中源码编译 llvm-clang 15.0.7-云社区-华为云 (huaweicloud.com) 中关于 llvm 的构建的描述;
- 构建完成后,生成的 so 库可以拷贝到 clang 所在机器其他地方执行。
这样,构建完成后,在 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),大家可以参考或修改之后使用。
- 点赞
- 收藏
- 关注作者
评论(0)