iOS静态扫描OCLint自定义规则开发总结

773 阅读6分钟

基础介绍部分参考:一文说清 OCLint 源码解析及工作流分析

不得不提的 Clang

由于 OCLint 是一个基于 Clang tool 的静态代码分析工具,所以不得不提一下 Clang。 Clang 作为 LLVM 的子项目, 是一个用来编译 c,c++,以及 oc 的编译器。

OCLint 本身是基于 Clang tool 的,换句话说相当于做了一层封装。 它的核心能力是对 Clang AST 进行分析,最后输出违反规则的代码信息,并且导出指定格式的报告。

接下来就让我们看看作为输入信息的 Clang AST 是什么样子的。

Clang AST

Clang AST 是在编译器编译时的一个中间产物,从词法分析,语法分析(生成 AST),到语义分析,生成中间代码。

Clang-LLVM.jpg

抽象语法树示例

这里先对抽象语法树有一个初步的印象。

//Example.c
#include <stdio.h>
int global;
void myPrint(int param) {
    if (param == 1)
        printf("param is 1");
    for (int i = 0 ; i < 10 ; i++ ) {
        global += i;
    }
}
int main(int argc, char *argv[]) {
    int param = 1;
    myPrint(param);
    return 0;
}
复制代码

00.png

这里可以清晰的看到,这一段代码的每一个元素与其子节点的关系。其中的节点有两大类型,一个是 Stmt 类,包括 Expr 表达式类也是继承于 Stmt,它是语句,有一定操作;另一大类元素是 Decl 类,即定义。所有的类,方法,函数变量均是一个 Decl 类 (这两个类互不兼容,需要特殊容器节点来转换,比如 DeclStmt 节点) 。另外从数据结构中可以看到,这个树是单向的,只有从某一个顶层元素向下访问。

在终端中可以用如下指令查看语法树:

clang -Xclang -ast-dump -fsyntax-only Example.c
复制代码

访问抽象语法树

无论是 Stmt 还是 Decl 都自带迭代器,可以方便的遍历所有节点元素,再判断其类型进行操作。不过在 Clang 中还有更方便的方法:继承 RecursiveASTVisitor 类。 它是一个 AST 树递归器,可以递归的访问一个 AST 树的所有节点。最常用的方法是 TraverseStmt 和 TraverseDecl。

例如我要访问这么一段代码中所有的函数,即 FunctionDecl,并且输出这些函数的名字,我就要重写 (通过自定义 checker) 这么一个方法:

bool VisitFunctionDecl(FunctionDecl *decl){
    string name = decl->getNameAsString();
    printf(name);
    return true;
}
复制代码

这样,我们就能够访问到这棵 AST 树中所有的 FunctionDecl 节点,并且把其中函数名字给输出出来了。

OCLint 源码解析

首先看一下核心类关系图,有一点初步的印象后,我们开始看代码 👀 04.png

  1. 首先找到入口文件 oclint/driver/main.cpp,及入口函数 main()

该文件的精简后的代码框架如下所示:

int main(int argc, const char **argv)
{
    llvm::cl::SetVersionPrinter(oclintVersionPrinter);
    // 构造 parser 分析程序
    CommonOptionsParser optionsParser(argc, argv, OCLintOptionCategory);
    // 配置
    oclint::option::process(argv[0]);
    
    ...

// 构造 analyzer
    oclint::RulesetBasedAnalyzer analyzer(oclint::option::rulesetFilter().filteredRules());
// 构造 driver
    oclint::Driver driver;

    // 执行分析
    driver.run(optionsParser.getCompilations(), optionsParser.getSourcePathList(), analyzer);
    
    std::unique_ptr<oclint::Results> results(std::move(getResults()));

    ostream *out = outStream();
    // 输出报告
    reporter()->report(results.get(), *out);
    disposeOutStream(out);

    return handleExit(results.get());
}
复制代码
  1. 接着查看核心的 Driver 类的关键代码片段,有三个比较核心的方法 constructCompilers(),invoke(),run()
// 构建编译器
static void constructCompilers(std::vector<oclint::CompilerInstance *> &compilers,
    CompileCommandPairs &compileCommands,
    std::string &mainExecutable)
{
    for (auto &compileCommand : compileCommands) // 遍历编译命令集
    {
        std::vector<std::string> adjustedCmdLine =
            adjustArguments(compileCommand.second.CommandLine, compileCommand.first);

#ifndef NDEBUG
        printCompileCommandDebugInfo(compileCommand, adjustedCmdLine);
#endif

        LOG_VERBOSE("Compiling ");
        LOG_VERBOSE(compileCommand.first.c_str());
	std::string targetDir = stringReplace(compileCommand.second.Directory, "\ ", " ");

        if(chdir(targetDir.c_str()))
        {
            throw oclint::GenericException("Cannot change dictionary into "" +
                targetDir + "", "
                "please make sure the directory exists and you have permission to access!");
        }
        clang::CompilerInvocation *compilerInvocation =
            newCompilerInvocation(mainExecutable, adjustedCmdLine);// 创建 CompilerInvocation 对象
        oclint::CompilerInstance *compiler = newCompilerInstance(compilerInvocation);
// 使用 clang 的 CompilerInvocation 对象 创建 oclint 的 CompilerInstance 对象,oclint 做了封装
        compiler->start(); // clang::FrontendAction 核心是获取到 action 并执行
        if (!compiler->getDiagnostics().hasErrorOccurred() && compiler->hasASTContext())
        {
            LOG_VERBOSE(" - Success");
            compilers.push_back(compiler); // oclint 封装的 CompilerInstance 对象放入集合中
        }
        else
        {
            LOG_VERBOSE(" - Failed");
        }
        LOG_VERBOSE_LINE("");
    }
}

// 实际的进行分析的唤起方法
static void invoke(CompileCommandPairs &compileCommands,
    std::string &mainExecutable, oclint::Analyzer &analyzer)
{
    std::vector<oclint::CompilerInstance *> compilers; // 编译器容器
    constructCompilers(compilers, compileCommands, mainExecutable);  // 构建编译器

    // collect a collection of AST contexts
    std::vector<clang::ASTContext *> localContexts;
    for (auto compiler : compilers) // 遍历编译器集合
    {
        localContexts.push_back(&compiler->getASTContext()); // 将 AST 上下文放入 上下文集合
    }

    // use the analyzer to do the actual analysis
    analyzer.preprocess(localContexts); // 将上下文集合送入分析器 预处理
    analyzer.analyze(localContexts); // 分析
    analyzer.postprocess(localContexts); // 发送处理

    // send out the signals to release or simply leak resources
    for (size_t compilerIndex = 0; compilerIndex != compilers.size(); ++compilerIndex)
    {
        compilers.at(compilerIndex)->end();
        delete compilers.at(compilerIndex);
    }
}
// main.cpp 调用的核心方法,执行分析
void Driver::run(const clang::tooling::CompilationDatabase &compilationDatabase,
    llvm::ArrayRef<std::string> sourcePaths, oclint::Analyzer &analyzer)
{
    CompileCommandPairs compileCommands; // 生成编译指令对容器
    constructCompileCommands(compileCommands, compilationDatabase, sourcePaths); // 构造编译指令对

    static int staticSymbol; // 静态符号
    std::string mainExecutable = llvm::sys::fs::getMainExecutable("oclint", &staticSymbol);// 获取 oclint 可执行程序的路径

    if (option::enableGlobalAnalysis()) // 启用全局分析的情况
    {
        invoke(compileCommands, mainExecutable, analyzer);// 调用 invoke 方法,注意 analyzer 也一并入参
    }
    else 
    { // 非全局分析的情况 逐个 compileCommand 进行分析
        for (auto &compileCommand : compileCommands)
        {
            CompileCommandPairs oneCompileCommand { compileCommand };
            invoke(oneCompileCommand, mainExecutable, analyzer);
        }
    }

    if (option::enableClangChecker()) // 启用 clang checker
    {
        invokeClangStaticAnalyzer(compileCommands, mainExecutable); // 调用 clang 的静态分析器
    }
}
复制代码
  1. 最后一个就是 RulesetBasedAnalyzer 类,这个类的代码量非常少,如下所示
void RulesetBasedAnalyzer::analyze(std::vector<clang::ASTContext *> &contexts)
{
    for (const auto& context : contexts)
    {
        LOG_VERBOSE("Analyzing ");
        auto violationSet = new ViolationSet();
        auto carrier = new RuleCarrier(context, violationSet); // 规则运载者,context 是传递给规则来分析的数据,violationSet 是用于存放处理好的结果集
        LOG_VERBOSE(carrier->getMainFilePath().c_str());
        for (RuleBase *rule : _filteredRules) // 遍历已经过滤的规则集合
        {
            rule->takeoff(carrier); // 调用规则的 takeoff
        }
        ResultCollector *results = ResultCollector::getInstance(); // 取得结果收集器实例
        results->add(violationSet); // 将规则处理好的数据加入收集器
        LOG_VERBOSE_LINE(" - Done");
    }
}
复制代码

从上面的代码可以看出 analyzer 会遍历规则集合,来调用 rule 的 takeoff 方法。rule 的基类是 RuleBase,这个基类含有一个 RuleCarrier 的示例作为成员,RuleCarrier包含了每个文件对应的 ASTContext 和 violationSet,violationSet 用来存放违例的相关信息。 rule 的职责就是,检查其成员变量 ruleCarrier 的 ASTContext,有违例的情况,就将结果写入 ruleCarrier 的 violationSet 中。

环境准备

  • OCLint的github上clone下代码 github.com/oclint/ocli…
  • cd进入oclint-scripts文件夹,执行./make。大约30分钟后编译完成,大概过程是下载LLVM、clang的源代码。
  • 编译成功后就可以写规则并且编译执行了。为了方便,OCLint提供了一个叫scaffoldRule的脚本程序,它在oclint-rules目录下。我们通过他传入要生成的规则名,级别,类型,脚本就会在目录oclint-rules/rules/custom/自动帮我们生成一个模板代码,并且加入编译路径中。
# 生成一个名为HdwTestRule类型是ASTVisitor的规则模板  
oclint-scripts/scaffoldRule HdwTestRule -t ASTVisitor
  • 生成一个包含所有规则的xcodeproj工程
# 在OCLint源码目录下建立一个文件夹,我这里命名为oclint-xcoderules
mkdir oclint-xcoderules
cd oclint-xcoderules
# 创建一个脚本(代码如下段),并执行它(我写的方便修改参数,其实里面就一句命令,直接执行也行)(PS:刚创建的文件是没有执行权限的,不要忘了chmod)
./create-xcode-rules.sh
#! /bin/sh -e
cmake -G Xcode -D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++  -D CMAKE_C_COMPILER=../build/llvm-install/bin/clang -D OCLINT_BUILD_DIR=../build/oclint-core -D OCLINT_SOURCE_DIR=../oclint-core -D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics -D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics -D LLVM_ROOT=../build/llvm-install/ ../oclint-rules
  • 于是我们就得到了xcode工程
  • 选择自己的规则,编译,成功后就可以在Products下看到生成的dylib

如何编写规则

  • oclint的原理是在编译阶段将语法树以对象的形式返回给检测rule
  • 在编写规则前,首先查看文件的语法树
clang -Xclang -ast-dump -fsyntax-only ViewController.m
  • 语法树会以stmtDecl两种对象嵌套生成。stmt类似一个容器,其中的Decl类似语法描述。
  • 在检测rule内监听对应的stmtDecl的子类回调,在回调中再对规则进行判断即可。

如何将自己的rule添加到检测中

  • 查看OCLint的安装路径
$ which oclint
/usr/local/bin/oclint
  • 在OCLint安装路径下找到oclint/rules文件夹,并将规则替换进去
  • 在工程根路径下生成报告,测试新规则是否已被添加
xcodebuild -scheme XXX clean && xcodebuild -scheme XXX -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO -sdk iphonesimulator | xcpretty -r json-compilation-database -o compile_commands.json

参考资料

iOS 编译知识小结

iOS底层学习 - 从编译到启动的奇幻旅程(一)

OCLint静态代码分析

OCLint + Infer + Jenkins + SonarQube 搭建iOS代码静态分析系统

OCLint自定义规则环境

OCLint 自定义规则101

Clang AST 基础学习

一文说清 OCLint 源码解析及工作流分析