上篇講到了玩轉(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荣德。
一、編譯過程總覽
使用自帶的clang查看編譯過程
命令行查看clang編譯的過程
clang -ccc-print-phases main.m
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ù)署咽。
使用預(yù)編譯命令查看結(jié)果
// 預(yù)編譯
clang -E main.m
可以看到預(yù)編譯一種的一個作用就是講宏定義替換成真實的值。
如果之前我們沒有將頭文件Foundation
刪掉生音,那么在這個階段艇抠,也會將Foundation
的內(nèi)容加入到結(jié)果中,有興趣的筆者也可以試試久锥。這里就不過多的占用篇幅了家淤。
另外Xcode其實也提供了便捷的功能入口
三、詞法分析
詞法分析階段是編譯過程的第一個階段瑟由。它是將字符序列轉(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
可以看到殴瘦,詞法分析將預(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
可以看到描融,語法分析后可以看到個描述類型别智,如:
- 方法描述類型聲明
FunctionDecl
:add
- 參數(shù)描述類型聲明
ParmVarDecl
:a
- 變量描述類型聲明
VarDecl
:b
- 整型值描述類型聲明
IntegerLiteral
:10
當(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)講述了如何編譯自己的LLVM
和clang
谋右。同時也講到了如何新建帶有clang
的Xcode模板硬猫,這里就不在重復(fù)椎敘。
在下載的源碼目錄clang的tools目錄改执,這個地方存放的就是clang的插件啸蜜。
/llvm-project/clang/tools
在tools里新建一個test-plugin1
文件夾,由于clang都是用C++
編寫的辈挂,自然我們就需要新建C++
的文件TestPlugin1.cpp
衬横,又因為我們是用cmake
編譯,所以CMakeLists文件是少不了的终蒂。
在CMakeLists告知我們的這個TestPlugin1
插件包含哪些文件蜂林,是什么類型。以前是用的add_llvm_loadable_module拇泣,現(xiàn)在由于功能重復(fù)改成了add_llvm_library
add_llvm_library(TestPlugin1 MODULE TestPlugin1.cpp)
然后在test-plugin1
文件夾同級目錄下的CMakeLists文件中增加test-plugin1
的聲明噪叙。
add_clang_subdirectory(test-plugin1)
最后重新生成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中我們需要增加CC
和CXX
參數(shù)來指定我們自己的clang
地址点寥。
如果不指定會出現(xiàn)如下error: unable to load plugin
Symbok not found
類似的報錯
在配置文件中新增CC
和CXX
絕對路徑,也就是clang
的絕對路徑clang++
来吩。
注:在上篇有講到敢辩,clang
和clang++
在LLVM的編譯產(chǎn)物里。
CC = /Volumes/ExDisk/LLVM/llvm/llvm_xcode/Debug/bin/clang
CXX = /Volumes/ExDisk/LLVM/llvm/llvm_xcode/Debug/bin/clang++
二弟疆、關(guān)閉 Enable Index-While-Building Functionality
Index-While-Building
本來是Apple用來優(yōu)化代碼索引的戚长,默認(rèn)打開。作用是 Xcode 編譯時會順帶建立代碼索引怠苔,但影響編譯速度同廉。關(guān)閉后整體編譯速度快 80s(Xcode 會換回以前的方式,在空閑時間建立代碼索引)柑司。
由于由于我們使用了自己的clang迫肖,不支持編譯期建立索引,所以會報如下錯誤
clang: error: unknown argument: '-index-store-path'
clang: error: cannot specify -o when generating multiple output files
這里我們只需要設(shè)為No
關(guān)閉即可
三攒驰、指定需要加載的額外插件
在配置文件中搜索other c
即可快速查詢
增加如下內(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插件祭埂,那么下次我們是不是可以玩一玩代碼混淆面氓?