LLVM(2)-編寫一個代碼檢查的Clang插件

上篇講到了玩轉(zhuǎn)LLVM最關(guān)鍵的一步-編譯自己的LLVM和Clang眷射,那么本篇文章將會走進LLVM的工作原理古今,探索我們的代碼是如何一步步轉(zhuǎn)換為機器能夠識別的機器碼的虹曙,我們又可以在哪些步驟下手耸峭,增加或者更改我們所需要的功能。

最后手?jǐn)]一個自己可以隨意玩轉(zhuǎn)的clang插件。

demo下載:demo

1、編譯過程

再編寫clang插件之前私恬,我們需要先了解clang在編譯一個項目的時候總共有哪些過程。

不用看厚厚的一本「編譯原理」炼吴,iOS開發(fā)者的mac電腦都自帶clang環(huán)境本鸣,我們利用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   // 預(yù)編譯輸出
2: compiler, {1}, ir   // 前端編譯成IR童芹,在此之前需要進行源文件的詞法分析和語法分析
3: backend, {2}, assembler // 后端編譯出匯編
4: assembler, {3}, object   // 匯編轉(zhuǎn)對象
5: linker, {4}, image  //  連接各個架包
6: bind-arch, "x86_64", {5}, image  // 適配各個平臺的架構(gòu)

二涮瞻、預(yù)編譯

為了更為直觀的查看我自己的代碼預(yù)編譯的結(jié)果,我們將唯一的頭文件Foundation刪掉假褪,然后再增加一個簡單的add函數(shù)署咽。

image.png

使用預(yù)編譯命令查看結(jié)果

// 預(yù)編譯
clang -E  main.m  
image.png

可以看到預(yù)編譯一種的一個作用就是講宏定義替換成真實的值。

如果之前我們沒有將頭文件Foundation刪掉生音,那么在這個階段艇抠,也會將Foundation的內(nèi)容加入到結(jié)果中,有興趣的筆者也可以試試久锥。這里就不過多的占用篇幅了家淤。

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

image.png

image.png

三、詞法分析

詞法分析階段是編譯過程的第一個階段瑟由。它是將字符序列轉(zhuǎn)換為單詞(Token)序列的過程絮重。這個階段的任務(wù)是從左到右一個字符一個字符地讀入源程序,即對構(gòu)成源程序的字符流進行掃描然后根據(jù)構(gòu)詞規(guī)則識別單詞(也稱單詞符號或符號)歹苦。詞法分析程序?qū)崿F(xiàn)這個任務(wù)青伤。

那么接下來來看看我們的這個簡單的add函數(shù)分為了哪些Token

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

可以看到殴瘦,詞法分析將預(yù)編譯后的代碼進行每個符號的拆分狠角,如:

  • int直接就定義為int
  • main定義為identifier
  • (定義為l_paren,)定義為r_paren
  • 源碼中的宏定義NUM在這已經(jīng)找不到,取而代之的真實值6

其他的符號蚪腋,如,``+``-``=``;等也有被分別對應(yīng)成相對于的Token丰歌。

四、語法分析

語法分析是編譯過程的一個邏輯階段屉凯。語法分析的任務(wù)是在詞法分析的基礎(chǔ)上將單詞序列組合成各類語法短語立帖,如“程序”,“語句”悠砚,“表達式”等等晓勇。語法分析程序判斷源程序在結(jié)構(gòu)上是否正確。

同樣,我們來看看绑咱,經(jīng)過語法分析后绰筛,我們的add函數(shù)會是什么結(jié)果。

// 語法分析
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
image.png

可以看到描融,語法分析后可以看到個描述類型别智,如:

  • 方法描述類型聲明FunctionDecladd
  • 參數(shù)描述類型聲明ParmVarDecla
  • 變量描述類型聲明VarDeclb
  • 整型值描述類型聲明IntegerLiteral10

當(dāng)然還有我們后面會講到的語法檢查這會在這一步實現(xiàn),這些申明類型在我們實現(xiàn)插件的時候也會用到稼稿。

上圖還有一個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中已經(jīng)講述了如何編譯自己的LLVMclang谋右。同時也講到了如何新建帶有clang的Xcode模板硬猫,這里就不在重復(fù)椎敘。

在下載的源碼目錄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拇泣,現(xiàn)在由于功能重復(fù)改成了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模板,因為這次是增量編譯霉翔,所以會比較快睁蕾。

總結(jié)這個過程:
1、 新建插件的文件夾test-plugin1
2债朵、在test-plugin1文件夾中增加CMakeLists和cpp文件(如果有多個就新增多個cpp文件)
3子眶、test-plugin1同級目錄下的CMakeLists增加test-plugin1的申明
4、重新編譯LLVM

3葱弟、調(diào)教Xcode

在上篇文章編譯自己的LLVM和Clang中已經(jīng)講述了如何編譯自己的clang壹店。但Xcode有自己默認(rèn)的clang版本,我們自己的工程并不能直接使用芝加,所以我們需要對Xcode進行一些配置才能使我們自己編譯的clang正常工作。

我們這次是需要模擬正常的app開發(fā),所以需要重新新建一個App工程:TestApp藏杖。

一将塑、指定clang

Xcode默認(rèn)使用的是自帶的clang前端,新版Xcode自帶的clang由于太多符號被strip了蝌麸,所以在新版的Xcode中我們需要增加CCCXX參數(shù)來指定我們自己的clang地址点寥。

如果不指定會出現(xiàn)如下error: unable to load plugin Symbok not found類似的報錯

5631d2e4f41e58606221388c2fe76bdb14768de4_2_690x97.png

在配置文件中新增CCCXX絕對路徑,也就是clang的絕對路徑clang++来吩。

注:在上篇有講到敢辩,clangclang++在LLVM的編譯產(chǎn)物里。

CC = /Volumes/ExDisk/LLVM/llvm/llvm_xcode/Debug/bin/clang
CXX = /Volumes/ExDisk/LLVM/llvm/llvm_xcode/Debug/bin/clang++
image.png

二弟疆、關(guān)閉 Enable Index-While-Building Functionality

Index-While-Building本來是Apple用來優(yōu)化代碼索引的戚长,默認(rèn)打開。作用是 Xcode 編譯時會順帶建立代碼索引怠苔,但影響編譯速度同廉。關(guān)閉后整體編譯速度快 80s(Xcode 會換回以前的方式,在空閑時間建立代碼索引)柑司。
由于由于我們使用了自己的clang迫肖,不支持編譯期建立索引,所以會報如下錯誤

image.png

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

這里我們只需要設(shè)為No關(guān)閉即可

image.png

三攒驰、指定需要加載的額外插件

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

image.png

增加如下內(nèi)容

-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中的源碼都認(rèn)為是用戶的
            if (filename.find("/Applications/Xcode.app/") == 0) return false;
            return  true;
        }

        // 代碼檢查的回調(diào)方法
        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);
        }
    };


    // 定義觸發(fā)插件的動作
    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鉴竭、總結(jié)

可看到歧譬,其實編寫一個clang插件并不復(fù)雜,主要就是了解一些clang編譯的過程搏存,了解各個過程該干的事情瑰步,哪些步驟可以為我們所用,這次我們寫的是一個代碼檢查的Clang插件祭埂,那么下次我們是不是可以玩一玩代碼混淆面氓?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蛆橡,隨后出現(xiàn)的幾起案子舌界,更是在濱河造成了極大的恐慌,老刑警劉巖泰演,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呻拌,死亡現(xiàn)場離奇詭異,居然都是意外死亡睦焕,警方通過查閱死者的電腦和手機藐握,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來垃喊,“玉大人猾普,你說我怎么就攤上這事”久眨” “怎么了初家?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長乌助。 經(jīng)常有香客問我溜在,道長,這世上最難降的妖魔是什么他托? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任掖肋,我火速辦了婚禮,結(jié)果婚禮上赏参,老公的妹妹穿的比我還像新娘志笼。我一直安慰自己沿盅,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布籽腕。 她就那樣靜靜地躺著嗡呼,像睡著了一般纸俭。 火紅的嫁衣襯著肌膚如雪皇耗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天揍很,我揣著相機與錄音郎楼,去河邊找鬼。 笑死窒悔,一個胖子當(dāng)著我的面吹牛呜袁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播简珠,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼阶界,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了聋庵?” 一聲冷哼從身側(cè)響起膘融,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎祭玉,沒想到半個月后氧映,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡脱货,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年岛都,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片振峻。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡臼疫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扣孟,到底是詐尸還是另有隱情烫堤,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布哈打,位于F島的核電站塔逃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏料仗。R本人自食惡果不足惜湾盗,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望立轧。 院中可真熱鬧格粪,春花似錦躏吊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疆导,卻和暖如春赁项,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背澈段。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工悠菜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人败富。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓悔醋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兽叮。 傳聞我的和親對象是個殘疾皇子芬骄,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容