阅读 735
LLVM(2)-编写一个代码检查的Clang插件

LLVM(2)-编写一个代码检查的Clang插件

上篇讲到了玩转LLVM最关键的一步-编译自己的LLVM和Clang,那么本篇文章将会走进LLVM的工作原理,探索我们的代码是如何一步步转换为机器能够识别的机器码的,我们又可以在哪些步骤下手,增加或者更改我们所需要的功能。

最后手撸一个自己可以随意玩转的clang插件。

demo下载:demo

1、编译过程

再编写clang插件之前,我们需要先了解clang在编译一个项目的时候总共有哪些过程。

不用看厚厚的一本「编译原理」,iOS开发者的mac电脑都自带clang环境,我们利用clang的一些命令来观看部分前端的过程。

由于只是为了清晰的查看编译过程,所在这里只是新建一个没有乱七八糟依赖的命令行工程testclang。

1.png 2.png

一、编译过程总览

使用自带的clang查看编译过程

命令行查看clang编译的过程
clang -ccc-print-phases main.m
复制代码

3.png

0: input, "main.m", objective-c   // 源码输入
1: preprocessor, {0}, objective-c-cpp-output   // 预编译输出
2: compiler, {1}, ir   // 前端编译成IR,在此之前需要进行源文件的词法分析和语法分析
3: backend, {2}, assembler // 后端编译出汇编
4: assembler, {3}, object   // 汇编转对象
5: linker, {4}, image  //  连接各个架包
6: bind-arch, "x86_64", {5}, image  // 适配各个平台的架构
复制代码

二、预编译

为了更为直观的查看我自己的代码预编译的结果,我们将唯一的头文件Foundation删掉,然后再增加一个简单的add函数。
4.png

使用预编译命令查看结果

// 预编译
clang -E  main.m  
复制代码

5.png

可以看到预编译一种的一个作用就是讲宏定义替换成真实的值。

如果之前我们没有将头文件Foundation删掉,那么在这个阶段,也会将Foundation的内容加入到结果中,有兴趣的笔者也可以试试。这里就不过多的占用篇幅了。

另外Xcode其实也提供了便捷的功能入口

6.png 7.png

三、词法分析

词法分析阶段是编译过程的第一个阶段。它是将字符序列转换为单词(Token)序列的过程。这个阶段的任务是从左到右一个字符一个字符地读入源程序,即对构成源程序的字符流进行扫描然后根据构词规则识别单词(也称单词符号或符号)。词法分析程序实现这个任务。

那么接下来来看看我们的这个简单的add函数分为了哪些Token

// 词法分析
clang -fmodules -E -Xclang -dump-tokens main.m
复制代码

8.png

可以看到,词法分析将预编译后的代码进行每个符号的拆分,如:

  • int直接就定义为int
  • main定义为identifier
  • (定义为l_paren,)定义为r_paren
  • 源码中的宏定义NUM在这已经找不到,取而代之的真实值6

其他的符号,如,``+``-``=``;等也有被分别对应成相对于的Token

四、语法分析

语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。语法分析程序判断源程序在结构上是否正确。

同样,我们来看看,经过语法分析后,我们的add函数会是什么结果。

// 语法分析
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
复制代码

9.png

可以看到,语法分析后可以看到个描述类型,如:

  • 方法描述类型声明FunctionDecladd
  • 参数描述类型声明ParmVarDecla
  • 变量描述类型声明VarDeclb
  • 整型值描述类型声明IntegerLiteral10

当然还有我们后面会讲到的语法检查这会在这一步实现,这些申明类型在我们实现插件的时候也会用到。

上图还有一个error的报错。

main.m:11:13: error: implicit declaration of function 'add' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
    int d = add(2);
复制代码

四、其他

剩下来的步骤 backend,assembler,linker,bind-arch不是本片的重点(主要笔者也似懂非懂),所以就不多加叙述。

2、新建clang插件

在上篇文章编译自己的LLVM和Clang中已经讲述了如何编译自己的LLVMclang。同时也讲到了如何新建带有clang的Xcode模板,这里就不在重复椎叙。

在下载的源码目录clang的tools目录,这个地方存放的就是clang的插件。

/llvm-project/clang/tools
复制代码

在tools里新建一个test-plugin1文件夹,由于clang都是用C++编写的,自然我们就需要新建C++的文件TestPlugin1.cpp,又因为我们是用cmake编译,所以CMakeLists文件是少不了的。 10.png

在CMakeLists告知我们的这个TestPlugin1插件包含哪些文件,是什么类型。以前是用的add_llvm_loadable_module,现在由于功能重复改成了add_llvm_library

add_llvm_library(TestPlugin1 MODULE TestPlugin1.cpp)
复制代码

11.png

然后在test-plugin1文件夹同级目录下的CMakeLists文件中增加test-plugin1的声明。

add_clang_subdirectory(test-plugin1)
复制代码

12.png

最后重新生成Xcode模板,因为这次是增量编译,所以会比较快。

总结这个过程: 1、 新建插件的文件夹test-plugin1
2、在test-plugin1文件夹中增加CMakeLists和cpp文件(如果有多个就新增多个cpp文件)
3、test-plugin1同级目录下的CMakeLists增加test-plugin1的申明 4、重新编译LLVM

3、调教Xcode

在上篇文章编译自己的LLVM和Clang中已经讲述了如何编译自己的clang。但Xcode有自己默认的clang版本,我们自己的工程并不能直接使用,所以我们需要对Xcode进行一些配置才能使我们自己编译的clang正常工作。

我们这次是需要模拟正常的app开发,所以需要重新新建一个App工程:TestApp

一、指定clang

Xcode默认使用的是自带的clang前端,新版Xcode自带的clang由于太多符号被strip了,所以在新版的Xcode中我们需要增加CCCXX参数来指定我们自己的clang地址。

如果不指定会出现如下error: unable to load plugin Symbok not found类似的报错 13.png

在配置文件中新增CCCXX绝对路径,也就是clang的绝对路径clang++

注:在上篇有讲到,clangclang++在LLVM的编译产物里。

CC = /Volumes/ExDisk/LLVM/llvm/llvm_xcode/Debug/bin/clang
CXX = /Volumes/ExDisk/LLVM/llvm/llvm_xcode/Debug/bin/clang++
复制代码

14.png

二、关闭 Enable Index-While-Building Functionality

Index-While-Building本来是Apple用来优化代码索引的,默认打开。作用是 Xcode 编译时会顺带建立代码索引,但影响编译速度。关闭后整体编译速度快 80s(Xcode 会换回以前的方式,在空闲时间建立代码索引)。 由于由于我们使用了自己的clang,不支持编译期建立索引,所以会报如下错误 15.png

clang: error: unknown argument: '-index-store-path'
clang: error: cannot specify -o when generating multiple output files
复制代码

这里我们只需要设为No关闭即可 16.png

三、指定需要加载的额外插件

在配置文件中搜索other c即可快速查询 17.png

增加如下内容

-Xclang -load 插件地址(dylib的地址) -Xclang -add-plugin -Xclang 插件名
// 实例
-Xclang -load -Xclang /Volumes/ExDisk/LLVM/llvm/llvm_xcode/Debug/lib/TestPlugin1.dylib -Xclang -add-plugin -Xclang TestPlugin
复制代码

注:有个地方需要注意,由于xcode有缓存,所以在重新编译插件后,xcode可能还是用的以前的老版本(没有TestPlugin1的版本),由于不知道怎么清这个缓存(实测clean无效),所以我采取的方式是: 1、将插件的地址改为一个错误的地址,重新cmd+B 2、然后改回正确的,就清好了。

4、编写插件代码

代码部分反而是最简单的部分了,稍微了解一些语法,特定的api即可。这部分不过多的叙述,代码中也有备注,直接上代码。

#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"

using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;

namespace TestPlugin {
    class TestHandler : public MatchFinder::MatchCallback{
    private:
        CompilerInstance &ci;

    public:
        TestHandler(CompilerInstance &ci) :ci(ci) {}
        
        //判断是否是用户源文件
        bool isUserSourceCode(const string filename) {
            //文件名不为空
            if (filename.empty()) return  false;
            //非xcode中的源码都认为是用户的
            if (filename.find("/Applications/Xcode.app/") == 0) return false;
            return  true;
        }

        // 代码检查的回调方法
        void run(const MatchFinder::MatchResult &Result) {

            // 检查类名(Interface),不能带有下划线
            if (const ObjCInterfaceDecl *decl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("ObjCInterfaceDecl")) {
                string filename = ci.getSourceManager().getFilename(decl->getSourceRange().getBegin()).str();
                if ( !isUserSourceCode(filename) ) return;
                size_t pos = decl->getName().find('_');
                if (pos != StringRef::npos) {
                    DiagnosticsEngine &D = ci.getDiagnostics();
                    // 获取位置
                    SourceLocation loc = decl->getLocation().getLocWithOffset(pos);
                    D.Report(loc, D.getCustomDiagID(DiagnosticsEngine::Warning, "TestPlugin:类名中不能带有下划线"));
                }
            }
            // 检查变量(Interface),不能带有下划线
            if (const VarDecl *decl = Result.Nodes.getNodeAs<VarDecl>("VarDecl")) {
                string filename = ci.getSourceManager().getFilename(decl->getSourceRange().getBegin()).str();
                if ( !isUserSourceCode(filename) ) return;
                size_t pos = decl->getName().find('_');
                if (pos != StringRef::npos && pos != 0) {
                    DiagnosticsEngine &D = ci.getDiagnostics();
                    SourceLocation loc = decl->getLocation().getLocWithOffset(pos);
                    D.Report(loc, D.getCustomDiagID(DiagnosticsEngine::Warning, "TestPlugin2:请使用驼峰命名,不建议使用下划线"));
                }
            }
        }
    };


    // 定义语法树的接受事件
    class TestASTConsumer: public ASTConsumer{
    private:
        MatchFinder matcher;
        TestHandler handler;
        
    public:
        TestASTConsumer(CompilerInstance &ci) :handler(ci) {
            matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler);
            matcher.addMatcher(varDecl().bind("VarDecl"), &handler);
            matcher.addMatcher(objcMethodDecl().bind("ObjCMethodDecl"), &handler);
        }
        void HandleTranslationUnit(ASTContext &Ctx) {
            printf("TestPlugin1: All ASTs has parsed.");
            DiagnosticsEngine &D = Ctx.getDiagnostics();
            // 在编译log中可以看到
            D.Report(D.getCustomDiagID(DiagnosticsEngine::Warning, "TestPlugin警告提示"));
            D.Report(D.getCustomDiagID(DiagnosticsEngine::Error, "TestPlugin错误信息"));
            matcher.matchAST(Ctx);
        }
    };


    // 定义触发插件的动作
    class TestAction : public PluginASTAction{
    public:
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                  StringRef InFile){
            return unique_ptr<TestASTConsumer> (new TestASTConsumer(CI));
            
        }

        bool ParseArgs(const CompilerInstance &CI,
                       const std::vector<std::string> &arg){
            return true;
        }
    };
}


// 告知clang,注册一个新的plugin
static FrontendPluginRegistry::Add<TestPlugin::TestAction>
X("TestPlugin", "Test a new Plugin");
// X 变量名,可随便写,也可以写自己有意思的名称
// TestPlugin  插件名称,️很重要,这个是对外的名称
// Test a new Plugin  插件备注

复制代码

代码部分都是自己的逻辑,比如上面的核心部分也就是在getName ,然后find('_')

5、总结

可看到,其实编写一个clang插件并不复杂,主要就是了解一些clang编译的过程,了解各个过程该干的事情,哪些步骤可以为我们所用,这次我们写的是一个代码检查的Clang插件,那么下次我们是不是可以玩一玩代码混淆?

文章分类
iOS
文章标签