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

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

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

demo下载:demo

1、编译过程

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

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

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

image.png

image.png

一、编译过程总览

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

命令行查看clang编译的过程
clang -ccc-print-phases main.m
image.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函数。

image.png

使用预编译命令查看结果

// 预编译
clang -E  main.m  
image.png

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

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

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

image.png

image.png

三、词法分析

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

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

// 词法分析
clang -fmodules -E -Xclang -dump-tokens main.m
image.png

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

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

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

四、语法分析

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

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

// 语法分析
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
image.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文件是少不了的。

image.png

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

add_llvm_library(TestPlugin1 MODULE TestPlugin1.cpp)
image.png

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

add_clang_subdirectory(test-plugin1)
image.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类似的报错

5631d2e4f41e58606221388c2fe76bdb14768de4_2_690x97.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++
image.png

二、关闭 Enable Index-While-Building Functionality

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

image.png

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

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

image.png

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

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

image.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插件,那么下次我们是不是可以玩一玩代码混淆?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容