iOS 坑的集中营

使用clang编写XCode代码检测插件

2020-06-12  本文已影响0人  9a957efaf40a

一、项目配置

  1. 下载llvm源码
  2. 安装CMake
    2.1 配置CMAKE
    2.2 Loadable modules和Clang executables的区别

二、代码编写

  1. RecursiveASTVisitor
    3.1 编译工程
    3.2 编译JRVisitor
    3.3 运行JRVisitor
    3.4 测试OC源码
    3.5 报错

三、插件化配置

  1. 插件化
  2. 配置XCode工程
  3. 运行插件

四、补充内容:MatchFinder和CXCursor

一、项目配置

1.下载llvm源码

终端输入以下命令:

sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`

git clone -b release_60 https://github.com/llvm-mirror/llvm.git llvm  
git clone -b release_60 https://github.com/llvm-mirror/clang.git llvm/tools/clang  
git clone -b release_60 https://github.com/llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra  
git clone -b release_60 https://github.com/llvm-mirror/compiler-rt.git llvm/projects/compiler-rt

目前最新版本是release_90,这里采用和网上文章同样的release_60,避免出现其他配置错误(不影响最终效果)。

WeChat45967eacdc8fa29221e6e11edb8f3d77.png

2.安装CMake

cmake是跨平台的编译工具,使用简单的语句来描述所有平台的安装(编译)过程。

brew update
brew install cmake
2.1 配置CMAKE

在编写插件之前,需要结合CMake来描述编译过程,添加我们自己的源码。
首先进入工作目录:

cd llvm/llvm/tools/clang/examples

在该目录下,已经包含了官方提供的几个示例,如PrintFunctionNames等。
在该目录下,需要修改CMakeLists.txt文件:

if(NOT CLANG_BUILD_EXAMPLES)
  set_property(DIRECTORY PROPERTY EXCLUDE_FROM_ALL ON)
  set(EXCLUDE_FROM_ALL ON)
endif()

if(CLANG_ENABLE_STATIC_ANALYZER)
add_subdirectory(analyzer-plugin)
endif()
add_subdirectory(clang-interpreter)
add_subdirectory(PrintFunctionNames)
add_subdirectory(AnnotateFunctions)
# 新添加的自定义插件代码目录名称
add_subdirectory(JRVisitor)

我们在最后添加了add_subdirectory(JRVisitor),这表示在编译的时候,会包含当前目录下的JRVisitor目录文件,该目录就是我们编写插件代码的地方。

同样的,我们需要在当前目录下创建JRVisitor目录:

mkdir JRVisitor && cd JRVisitor
2.2 Loadable modules和Clang executables的区别

有了上面的JRVisitor目录,接下来我们将在该目录下编写插件代码。
首先,我们创建源文件:

touch JRVisitor.cpp

JRVisitor.cpp文件就是编写插件代码的文件。此时它里面是空的,没有任何代码。

其次,我们需要配置CMakeLists.txt文件,告诉编译JRVisitor.cpp文件时,需要有哪些依赖,输出哪种格式的文件:

touch CMakeLists.txt

新创建的CMakeLists.txt中的内容为:

set(LLVM_LINK_COMPONENTS support)

add_clang_executable(JRVisitor
  JRVisitor.cpp
  )
target_link_libraries(JRVisitor
  PRIVATE
  clangTooling
  libclang
  )

我们这里先采用 add_clang_executable的方式,方便我们编写代码并调试。

二、代码编写

3. RecursiveASTVisitor

源代码会被转化为抽象语法树(AST),我们需要对抽象语法树进行访问,找到我们需要检验的节点,这是我们编写插件主要做的事情。

RecursiveASTVisitor为访问抽象语法树提供了一系列的回调方法,我们可以在需要的方法中进行处理。

为了使用RecursiveASTVisitor,我们需要准备以下模版代码:

#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"

using namespace clang::tooling;
using namespace llvm;
using namespace clang;

// Apply a custom category to all command-line options so that they are the
// only ones displayed.
static llvm::cl::OptionCategory MyToolCategory("my-tool options");

// CommonOptionsParser declares HelpMessage with a description of the common
// command-line options related to the compilation database and input files.
// It's nice to have this help message in all tools.
static cl::extrahelp CommonHelp(CommonOptionsParser::HelpMessage);

// A help message for this specific tool can be added afterwards.
static cl::extrahelp MoreHelp("\nMore help text...\n");

/*
  我们在CFunctionCalledVisitor中做逻辑处理
*/
class CFunctionCalledVisitor : public RecursiveASTVisitor<CFunctionCalledVisitor> {
public:
    explicit CFunctionCalledVisitor(ASTContext *Context, std::string fName)
    : Context(Context), fName(fName) {}
    
    bool VisitCallExpr(CallExpr *CallExpression) {
        QualType q = CallExpression->getType();
        const clang::Type *t = q.getTypePtrOrNull();
        
        if (t != NULL) {
            FunctionDecl *func = CallExpression->getDirectCallee();
            const std::string funcName = func->getNameInfo().getAsString();
            if (fName == funcName) {
                /*
                如果你的llvm版本是release_90版本,则需要将CallExpression->getLocStart()修改为CallExpression->getBeginLoc()
                */
                FullSourceLoc FullLocation =
                Context->getFullLoc(CallExpression->getLocStart());
                if (FullLocation.isValid())
                    llvm::outs() << "Found call at "
                    << FullLocation.getSpellingLineNumber() << ":"
                    << FullLocation.getSpellingColumnNumber() << "\n";
                
            }
        }
        
        return true;
    }
    
private:
    ASTContext *Context;
    std::string fName;
};

class CFunctionCalledConsumer : public clang::ASTConsumer {
public:
    explicit CFunctionCalledConsumer(CompilerInstance &Instance, ASTContext *Context, std::string fName)
    : Instance(Instance), Visitor(Context, fName) {}
    
    bool HandleTopLevelDecl(DeclGroupRef DG) override {
        return true;
    }
    void HandleTranslationUnit(clang::ASTContext &Context) override {
        Visitor.TraverseDecl(Context.getTranslationUnitDecl());
    }
    
private:
    CompilerInstance &Instance;
    CFunctionCalledVisitor Visitor;
};

/*
  
*/
class CFunctionCalledAction : public clang::ASTFrontendAction {
public:
    CFunctionCalledAction() {}
    
    virtual std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
                                                                  clang::CompilerInstance &Compiler, llvm::StringRef InFile) {
        const std::string fName = "foo";
        return std::unique_ptr<clang::ASTConsumer>(
                                                   new CFunctionCalledConsumer(Compiler, &Compiler.getASTContext(), fName));
    }
};


int main(int argc, const char **argv) {
    CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
    // OptionsParser.getSourcePathList()获取要解析的源文件,目前是test.cpp
    const std::vector<std::string> vec = OptionsParser.getSourcePathList();
    for (size_t i = 0; i < vec.size(); ++i) {
        string str = vec[i];
        printf("filepath:%s\n",str.c_str());
    }
    
    ClangTool Tool(OptionsParser.getCompilations(),
                   OptionsParser.getSourcePathList());
    return Tool.run(newFrontendActionFactory<CFunctionCalledAction>().get());
}

ASTFrontendAction是编译前段操作的入口类,这里我们继承自它来定义CFunctionCalledAction,我们自己的前端操作类主要目的是用来查找函数foo的调用的地方。

ASTConsumer是处理AST的地方。

// test.cpp
1 void foo() {
2     
3 }
4 
5 class Y {
6 public:
7   void foo() {
8        
9    }
10 };
11
12 int main() {
13    foo();
14    return 1;
15 }

比如对于test.cpp来说,HandleTopLevelDecl会执行三次,分别为fooYmain

CFunctionCalledVisitor是我们处理每个节点的地方,现在,我们只定义了VisitCallExpr函数来捕获所有的调用表达式。

VisitCallExpr方法中,我们首先判断函数调用的类型是否存在?如果存在,获取函数调用的名称,判断是否等于foo,如果相等,则输出函数调用所在的行数。

现在,尝试编译下整个llvm项目:

3.1 编译工程
cd /User/LQ/llvm/llvm_build
cmake -G Xcode -DCMAKE_BUILD_TYPE:STRING=Release ../llvm

等待编译结束后,应该可以在llvm_build看到编译出的xcode工程LLVM.xcodeproj

3.2 编译JRVisitor

双击打开工程,会弹出如下提示,选择Automatically Create Schemes

屏幕快照 2020-06-08 下午7.12.36.png
选择JRVisitor的target,command+B进行编译,编译完成后,即可动态调试。

注意:后续修改代码,直接在项目中修改即可。修改完成command+B重新编译即可,不需要再使用cmake编译llvm的源码。

3.3 运行JRVisitor

运行有两种方式:

  1. 使用命令行工具,需要提前编译出可执行文件,使用终端进入目录后执行./JRVisitor /Users/58liqiang/llvm/test.cpp --
$ cd /Users/58liqiang/llvm_build/Debug/bin
$ ./JRVisitor /Users/58liqiang/llvm/test.cpp --
  1. XCode中设置JRVisitortarget的Scheme,在Arguments中添加两个参数/Users/58liqiang/llvm/test.cpp--
    1018593-876ac7b35375b531.png

不出意外的话,我们应该可以看到如下结果:

Found call at 13 : 5

即表明foo方法的调用在test.cpp文件的第13行,第5列。

  • 除了VisitCallExpr方法外,clang提供了其他的OC专属的回调接口,比如bool VisitObjCMethodDecl(ObjCMethodDecl *declaration)方法是找到OC方法时的回调;bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)则是找到OC类的定义时的回调,还有其他各种关于OC的方法回调。
  • 参数--表示不使用任何编译数据库。编译数据库是json结构的数据,包含了一定的编译规则。
3.4 测试OC源码

上面的插件代码,在源文件为OC代码的时候,是无法成功的(尤其导入了<Foundation/Foundation.h>),需要对各项配置进行修改。
比如我们现在添加两个文件Person.hPerson.m

.h文件
#import <Foundation/Foundation.h>

@interface person : NSObject

@property (nonatomic,copy)NSString *MyName;

@end

.m文件
#import "Person.h"

@implementation person

void foo() {
    
}

- (void)MyTestFunc {
    foo();
}

@end

Schemes中的第一个参数修改为Person.m文件路径,然后运行项目,报错结果为下:

/Users/58liqiang/llvm/Person.h:1:9: fatal error: 'Foundation/Foundation.h' file not found
#import <Foundation/Foundation.h>
        ^~~~~~~~~~~~~~~~~~~~~~~~~

为了使Foundation框架能被正常找到,我们需要在Scheme中添加如下参数:

1018593-480d54e01bce0c4c.png

之所以需要添加-extra-arg来分割增加的两个参数,是因为我们的这个命令行工具就是通过-extra-arg来追加参数的,你可以通过终端执行$ ./JRVisitor -help来查看具体信息。

bogon:~ 58liqiang$ cd /Users/58liqiang/llvm/llvm_build/Debug/bin/
bogon:bin 58liqiang$ ./JRVisitor -help
USAGE: JRVisitor [options] <source0> [... <sourceN>]

OPTIONS:

Generic Options:

  -help                      - Display available options (-help-hidden for more)
  -help-list                 - Display list of available options (-help-list-hidden for more)
  -version                   - Display the version of this program

my-tool options:

  -extra-arg=<string>        - Additional argument to append to the compiler command line
  -extra-arg-before=<string> - Additional argument to prepend to the compiler command line
  -p=<string>                - Build path

-p <build-path> is used to read a compile command database.

    For example, it can be a CMake build directory in which a file named
    compile_commands.json exists (use -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
    CMake option to get this output). When no build path is specified,
    a search for compile_commands.json will be attempted through all
    parent paths of the first input file . See:
    http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html for an
    example of setting up Clang Tooling on a source tree.

<source0> ... specify the paths of source files. These paths are
    looked up in the compile command database. If the path of a file is
    absolute, it needs to point into CMake's source tree. If the path is
    relative, the current working directory needs to be in the CMake
    source tree and the file must be in a subdirectory of the current
    working directory. "./" prefixes in the relative files will be
    automatically removed, but the rest of a relative path must be a
    suffix of a path in the compile command database.


More help text...
bogon:bin 58liqiang$ 

注意:SDK的路径必须是你自己的路径,我的路径是/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk

然后,修改我们JRVisitor.cppCFunctionCalledVisitor方法,增加以下代码:

bool VisitObjCMethodDecl(ObjCMethodDecl *declaration)
{
     const std::string name = declaration->getNameAsString();
     llvm::outs() << "Found OC method " << name << "\n";
     return true;
}
    
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
     const std::string name = declaration->getNameAsString();
     llvm::outs() << "Found OC Interface " << name << "\n";
     return true;
}

重新运行运行项目,不出意外的话,应该可以在控制台看到下面的结果:

filepath:/Users/58liqiang/llvm/Person.m
warning: using sysroot for 'iPhoneSimulator' but targeting 'MacOSX' [-Wincompatible-sysroot]
Found OC Interface NSArray
Found OC Interface NSAttributedString
Found OC Interface NSString
Found OC Interface NSNull
Found OC Interface NSCharacterSet
...
Found OC method initWithUUIDString:
Found OC method initWithUUIDBytes:
Found OC method getUUIDBytes:
Found OC Interface person
Found OC method MyTestFunc
Found call at 10:5
Program ended with exit code: 0

根据结果,我们找到了person类的定义,找到了MyTestFunc的OC方法,找到了foo的函数调用处。

3.5 报错

在以上的示例中,我们的person类是首字母小写的,需要对此作出提示。
一个提示的方法是使用printf在控制台输出错误。
更好的方法是使用编译器的诊断引擎DiagnosticsEngine,如此,在以后我们将程序编译成插件的时候,就可以使用XCode的诊断引擎了,这会直接在源码中进行错误提示。

需要考虑两点:

判断是否是用户代码,对于我们的例子来说,import <Foundation/Foundation.h>应该被忽略。在CFunctionCalledVisitor中添加如下代码:

bool isUserSourceCode (Decl *decl)
{
  std::string filename = Instance.getSourceManager().getFilename(decl->getSourceRange().getBegin()).str();
  if (filename.empty()) return false;
  //非XCode中的源码都认为是用户源码
  if(filename.find("/Applications/Xcode.app/") != filename.npos)  return false;
  return true;
}

诊断引擎,需要从编译器实例CompilerInstance中获取。我们修改bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)方法:

bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
  if (!isUserSourceCode(declaration)) return true;
  const std::string name = declaration->getNameAsString();
  const char *cname = name.c_str();
  if (cname[0] < 'A' || cname[0] > 'Z') {
    DiagnosticsEngine &D = Instance.getDiagnostics();
    int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "%0");
    SourceLocation location = declaration->getLocation();
    D.Report(location, diagID) << "类名应该大写";
  }
  return true;
}

现在重新运行程序,应该可以在控制台中看到以下结果:

...
In file included from /Users/58liqiang/llvm/Person.m:1:
/Users/58liqiang/llvm/Person.h:3:12: error: 类名应该大写
@interface person : NSObject
           ^
Found OC method MyTestFunc
Found call at 10:5
1 error generated.
Error while processing /Users/58liqiang/llvm/Person.m.
Program ended with exit code: 1

4.插件化

以上代码编译为可执行文件,方便动态调试。如果我们需要编译成.dylib,则需要修改部分配置。

4.1. 修改CMakeLists文件
cd /User/58liqiang⁩/llvm⁩⁨/llvm⁩/tools⁩/clang⁩/examples⁩/JRVisitor⁩

CMakeLists内容替换为:

if( NOT MSVC ) # MSVC mangles symbols differently
  if( NOT LLVM_REQUIRES_RTTI )
    if( NOT LLVM_REQUIRES_EH )
      set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/JRVisitor.exports)
    endif()
  endif()
endif()

add_llvm_loadable_module(JRVisitor
  JRVisitor.cpp
  )
if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
  target_link_libraries(JRVisitor
    PRIVATE
    clangAST
    clangBasic
    clangFrontend
    LLVMSupport
    )
endif()
4.2. 修改JRVisitor.cpp文件

主要修改三个部分:

  1. 头文件修改为以下(为防止干扰,删除不必要的头文件):
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"

using namespace clang;

// 删除下面三句代码
//static llvm::cl::OptionCategory MyToolCategory("my-tool options");
//static cl::extrahelp CommonHelp(CommonOptionsParser::HelpMessage);
//static cl::extrahelp MoreHelp("\nMore help text...\n");
  1. 删除main函数,并且添加static clang::FrontendPluginRegistry::Add<CFunctionCalledAction> X("JRVisitor", "use visitor");
  2. 修改CFunctionCalledAction类如下:
class CFunctionCalledAction : public clang::PluginASTAction {
public:
    CFunctionCalledAction() {}
    
     virtual std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
                                                                  clang::CompilerInstance &Compiler, llvm::StringRef InFile) override {
        const std::string fName = "foo";
        return llvm::make_unique<CFunctionCalledConsumer>(Compiler,
                                                          &Compiler.getASTContext(),
                                                          fName);
    }
    bool ParseArgs(const CompilerInstance &CI,
                   const std::vector<std::string> &args) override
    {
        return true;
    }
};

/*
删除main函数
int main(int argc, const char **argv) {
  ...
}
*/

static clang::FrontendPluginRegistry::Add<CFunctionCalledAction>
X("JRVisitor", "use visitor");
4.3. 重新编译生成XCode项目

编译之前,需要删除现有的编译项目,删除llvm_build。然后执行下面操作:

$ cd /Users/58liqiang/llvm
$ mkdir llvm_build && cd llvm_build
$ cmake -G Xcode -DCMAKE_BUILD_TYPE:STRING=Release ../llvm

成功后,同上面一样,打开LLVM.xcodeproj,编译JRVisitorclang。直到完成,整个插件化的工作就做完了。

5. 配置XCode工程

新建xcode项目。

5.1 配置build setting中的Other C Flags-Xclang -load -Xclang /Users/58liqiang/Desktop/JRvisitor.dylib -Xclang -add-plugin -Xclang JRVisitor

在4.3步骤中编译生成的JRVisitor.dylib默认在llvm_build/Debug/lib目录下,这里我把它拷贝到了桌面,因此你需要根据自己的实际情况填写这里的路径。

5.2 build setting中增加两个自定义键值对:

屏幕快照 2020-06-11 下午5.26.40.png

分别为CCCXX,其中CC的值是4.3步骤中编译出的clang可执行文件,默认在llvm_build/Debug/bin中,CXX的值是clang++可执行文件,默认在llvm_build/Debug/bin中。

屏幕快照 2020-06-11 下午5.28.45.png

使用CXX或者CPLUSPLUS都可以。CC标记C编译器,CXX标记C++编译器。类似的还有LDLDPLUSPLUSLIBTOOL,分别为使用指定路径的C链接器、C++链接器、libtool。

5.3 build setting中设置Enable Index-While-Building Functionality为NO

6. 运行插件

现在,在新建的项目中增加自定义的person类,然后编译,会发现提示了我们自定义的错误:

屏幕快照 2020-06-11 下午5.33.54.png

修改插件代码不需要重新编译生成xcode项目,仅仅更新插件即可。


四、补充内容:MatchFinder和CXCursor

参考文章

上一篇 下一篇

猜你喜欢

热点阅读