LLVM 之 Clang 静态分析器篇(2):如何扩展 Clang 静态分析器
Author: stormQ
Created: Tuesday, 06. April 2021 09:41PM
Last Modified: Saturday, 15. May 2021 08:36PM
本文通过实现一个简单的检查器介绍了扩展 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/include/clang/StaticAnalyzer/Checkers/
目录中。
clang -cc1 -analyzer-checker-help
和clang -cc1 -analyzer-checker-help-alpha
等命令所显示的检查器列表来源于Checkers.td
文件。
def MainCallChecker
,表示检查器的注册名称为MainCallChecker
。
"MainCall"
,表示检查器的名称为MainCall
。也就是说,可以通过-analyzer-checker=alpha.core.MainCall
标志来启用该检查器。
HelpText
选项,用于指定该检查器对应的描述。从而,在执行类似于-help
命令时显示。
Documentation
选项,用于指定检查器文档的URI
地址。
需要注意的是, 修改的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;
}
注:
新建的源文件名称不必是MainCallChecker.cpp
,也可以是其他名称。
检查器的类名不必与在Checkers.td
文件中定义的检查器的注册名称保持一致。
void ento::registerXXX(CheckerManager &Mgr)
中的XXX
必须与在Checkers.td
文件中定义的检查器的注册名称保持一致。
Mgr.registerChecker<XXX>();
中的XXX
必须与检查器的类名保持一致。
ento::shouldRegisterXXX(const CheckerManager &mgr)
中的XXX
必须与在Checkers.td
文件中定义的检查器的注册名称保持一致。
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
从上面的输出可以看出:
alpha.core.MainCall
,即我们自定义的检查器的完整名称。
Check for calls to main
,即我们在Checkers.td
文件中为自定义的检查器所添加的描述。
step 4: 运行自定义的检查器
1) 测试程序
测试程序——Example_Test.c
的内容如下:
typedef int (*main_t)(int, char **);
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 静态分析器。
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 ®istry) {
registry.addChecker<MainCallChecker>(
"plugin.MainCallChecker", "Disallows calls to functions called main",
"");
}
extern "C" const char clang_analyzerAPIVersionString[] =
CLANG_ANALYZER_API_VERSION_STRING;
注:
新建的目录名称不必是CheckerMainCall
,也可以是其他名称。
新建的源文件名称不必是MainCallChecker.cpp
,也可以是其他名称。
以插件方式扩展 Clang 静态分析器时,不需要修改Checkers.td
文件。
函数clang_registerCheckers()
用于注册以插件方式提供的检查器。
registry.addChecker<XXX>();
中的XXX
必须与检查器的类名保持一致。其第一个参数表示检查器的全称(这里是"plugin.MainCallChecker"
),第二个参数表示检查器的描述(这里是"Disallows calls to functions called main"
),第三个参数表示检查器文档的URI
地址(这里是""
),第四个参数表示检查器是否对用户可见(默认值为false
,表示对用户可见)。
全局变量clang_analyzerAPIVersionString
表示生成该插件时所使用的 Clang API 版本。如果与使用该插件的 Clang API 版本不兼容,那么检查器会注册失败。
2) 添加 MainCallCheckerPlugin.exports
在目录clang/lib/Analysis/plugins/CheckerMainCall/
中新建MainCallCheckerPlugin.exports
文件。其内容如下:
clang_registerCheckers
clang_analyzerAPIVersionString
注:
新建的文件名称不必是MainCallCheckerPlugin.exports
,也可以是其他名称。
这里的检查器注册函数名称clang_registerCheckers
必须与MainCallChecker.cpp
中的保持一致。
这里的全局变量名称clang_analyzerAPIVersionString
必须与MainCallChecker.cpp
中的保持一致。
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
)
注:
这里的MainCallCheckerPlugin
表示要生成的插件的名称为libMainCallCheckerPlugin.so
。
这里的MainCallCheckerPlugin.exports
即为上文中新添加的文件名称。
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_ANALYZER
和LLVM_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
从上面的输出可以看出:
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
函数的错误。
下一篇:LLVM 之 Clang 静态分析器篇(3):检查器的整体工作机制