在Xcode中,當咱們按下command + B進行build操做後發生了那些事情,這是一個將代碼編譯的過程。Xcode如今使用的編譯器是LLVM,Xcode 早期使用的是GCC編譯器,因爲一些歷史緣由,從Xcode5開始正式過渡到使用LLVM編譯器。下文將着重介紹LLVM。html
LLVM項目是模塊化、可重用的編譯器以及工具鏈技術的集合。前端
美國計算機協會(ACM)將其 2012 年軟件系統獎頒給了LLVM,以前曾得到此獎項的軟件和技術包括:Java、Apache、Mosaic、the World Wide Web、SmallTalk、UNIX、Eclipse等等。c++
LLVM項目的發展起源於2000年伊利諾伊大學厄巴納-香檳分校維克拉姆·艾夫(Vikram Adve)與克里斯·拉特納(Chris Lattner)的研究,他們想要爲全部靜態及動態語言創造出動態的編譯技術。LLVM是以BSD受權來發展的開源軟件。2005年,蘋果電腦僱用了克里斯·拉特納及他的團隊爲蘋果電腦開發應用程序系統,LLVM爲現今Mac OS X及iOS開發工具的一部分。git
LLVM的命名最先源自於底層虛擬機(Low Level Virtual Machine)的首字母縮寫,因爲這個項目的範圍並不侷限於建立一個虛擬機,這個縮寫致使了普遍的疑惑。官方描述以下:The name 「LLVM」 itself is not an acronym;it is the full name of the project。LLVM這個名稱並非首字母縮略詞,它是項目的全名。github
LLVM開始成長以後,成爲衆多編譯工具及低級工具技術的統稱,使得這個名字變得更不貼切,開發者於是決定放棄這個縮寫的意涵,現今LLVM已單純成爲一個品牌,適用於LLVM下的全部項目,包含LLVM中介碼(LLVM IR)、LLVM除錯工具、LLVM C++標準庫等。編程
目前NDK/Xcode均採用LLVM做爲默認的編譯器。swift
前端將各類類型的源代碼編譯爲中間代碼,也就是bitcode,在LLVM體系內,不一樣的語言有不一樣的編譯器前端,常見的如clang負責 c/c++/oc的編譯,flang負責fortran的編譯,swiftc負責swift的編譯等等。後端
不一樣的先後端使用統一的中間代碼LLVM Intermediate Representation(LLVM IR)。xcode
優化階段是一個通用的階段,針對的是統一的LLVM IR,不管是新的編程語言,仍是支持新的硬件設備,都不須要對優化階段作修改,具體是對bitcode進行各類類型的優化,將bitcode代碼進行一些邏輯的轉換,使得代碼效率更高,體積更小,好比DeadStrip/SimplifyCFG。bash
後端,也叫CodeGenerator,負責把優化後的bitcode編譯爲指定目標架構的機器碼,好比 X86Backend負責把bitcode編譯爲x86指令集的機器碼。
GCC相比之下,先後端耦合在了一塊兒。因此,GCC支持一門新的語言,或是爲了支持一個新的平臺,就變得異常困難。
LLVM如今被做爲實現各類靜態和運行時編譯語言通用基礎架構(GCC 家族、Java、.Net、Python、Ruby、Scheme、Haskell、D等)。
LLVM體系中,不一樣語言源代碼將會被轉化爲統一的bitcode格式,三個模塊相互獨立,能夠充分複用。好比,若是開發一門新的語言,只要製造一個該語言的前端,將源碼編譯爲bitcode,優化和後端不用管。同理,若是新的芯片架構問世,只需基於LLVM從新編寫一套目標平臺的後端便可。
LLVM項目的一個子項目。
基於LLVM架構的C/C++/Objective-C/Objective-C++編譯器前端。
相比於 GCC,Clang具備以下優勢:
編譯速度快:在某些平臺上,Clang的編譯速度顯著的快過GCC(Debug 模式下編譯 OC 速度比 GCC 快 3 倍);
佔用內存小:Clang生成的AST所佔用的內存是GCC的五分之一左右;
模塊化設計:Clang採用基於庫的模塊化設計,易於IDE集成及其餘用途的重用;
診斷信息可讀性強:在編譯過程當中,Clang建立並保留了大量詳細的元數據(metadata),有利於調試和錯誤報告;
設計清晰簡單,容易理解,易於擴展加強。
客觀的說GCC也有不少優勢:例如支持多平臺,基於C無需 C++編譯器便可編譯。這個優勢到蘋果那裏反而成了缺點,蘋果須要的是快。
clang -ccc-print-phases main.m
複製代碼
clang -E main.m -F /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks
複製代碼
詞法分析,生成Token
int sum(int a,int b){
int c = a + b;
return c;
}
複製代碼
clang -fmodules -E -Xclang -dump-tokens main.m
複製代碼
這個命令的做用是,顯示每一個Token的類型、值,以及位置。參考該連接,能夠看到Clang定義的全部Token類型。 能夠分爲下面這4類:
利用上面輸出的Token先按照語法組合成語義,生成相似VarDecl這樣的節點,而後將這些節點按照層級關係構成抽象語法樹(AST)。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
複製代碼
TranslationUnitDecl是根節點,表示一個編譯單元;Decl表示一個聲明;Expr表示的是表達式;Literal表示字面量,是一個特殊的Expr;Stmt表示陳述。
除此以外,Clang還有衆多種類的節點類型。Clang裏,節點主要分紅Type類型、Decl聲明、Stmt陳述這三種,其餘的都是這三種的派生。經過擴展這三類節點,就可以將無限的代碼形態用有限的形式來表現出來。
LLVM IR有3種表示形式,但本質上是等價的。
clang -S -emit-llvm main.m
複製代碼
clang -c -emit-llvm main.m
複製代碼
.ll 文件部份內容,以下:
基於LLVM 、Clang能夠作不少實踐,以下:
官方參考:
應用:語法樹分析、語言轉換等
OCLint、Clang靜態分析器(Clang Static Analyzer)
Clang插件開發
官方參考:
應用:代碼檢查(命名規範、代碼規範)等
官方參考:
應用:中間代碼優化、代碼混淆等
llvm-tutorial-cn.readthedocs.io/en/latest/i…
kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh_CN/lates…
Clang使用模塊化設計,能夠將自身功能以庫的方式來供上層應用來調用。好比,編碼規範檢查、IDE 中的語法高亮、語法檢查等上層應用,都是使用Clang庫的接口開發出來的。Clang有三個接口庫能夠供上層應用調用,分別是LibClang、Clang Plugin、LibTooling。
LibClang爲了兼容更多Clang版本,相比Clang少了不少功能;Clang Plugin和LibTooling具有Clang 的全量能力。Clang Plugin編寫代碼的方式,和LibTooling幾乎同樣,不一樣的是Clang Plugin還可以控制編譯過程,能夠加warning或者直接中斷編譯提示錯誤。另外,編寫好的LibTooling可以很是方便地轉成Clang Plugin。 所以,Clang Plugin在功能上是最全的。
下載 LLVM Project
git clone https://github.com/llvm/llvm-project.git
複製代碼
上圖中,clang目錄就是類C語言編譯器的代碼目錄;llvm目錄的代碼包含兩部分,一部分是對源碼進行平臺 無關優化的優化器代碼,另外一部分是生成平臺相關彙編代碼的生成器代碼;lldb目錄裏是調試器的代碼;lld裏是連接器代碼。
macOS屬於類UNIX平臺,所以既能夠生成Makefile文件來編譯,也能夠生成Xcode工程來編譯。
進入llvm-project文件目錄,生成Makefile文件:
cmake -DLLVM_ENABLE_PROJECTS=clang -G "Unix Makefiles" ../llvm
複製代碼
生成Xcode工程,可使用以下命令
cmake -G Xcode -DLLVM_ENABLE_PROJECTS=clang ../llvm
複製代碼
想要更多地瞭解CMake的語法和功能,你能夠查看官方文檔。
執行cmake命令時,你可能會遇到下面的提示:
-- The C compiler identification is unknown -- The CXX compiler identification is unknown CMake Error at CMakeLists.txt:39 (project):
No CMAKE_C_COMPILER could be found.
CMake Error at CMakeLists.txt:39 (project):
No CMAKE_CXX_COMPILER could be found.
複製代碼
這代表cmake沒有找到代碼編譯器的命令行工具。分兩種狀況處理:
xcode-select --install
複製代碼
sudo xcode-select --reset
複製代碼
生成Xcode工程後,打開生成的LLVM.xcodeproj文件,選擇Automatically Create Schemes。
生成Xcode項目後再利用Xcode進行編譯,可是速度很慢
add_clang_subdirectory(mskj-plugin)
複製代碼
add_llvm_library(MSKJPlugin MODULE MSKJPlugin.cpp PLUGIN_TOOL clang)
複製代碼
MSKJPlugin是插件名,MSKJPlugin.cpp是源代碼文件,這段代碼是指,要將Clang插件代碼集成到LLVM的Xcode工程中,並做爲一個模塊進行編寫調試。添加了Clang插件的目錄和文件後,再次用cmake命令生成Xcode工程,裏面就可以集成MSKJPlugin.cpp文件。
① 編寫PluginASTAction代碼
因爲Clang插件是沒有main函數的,入口是PluginASTAction的ParseArgs函數。因此,編寫Clang插件還要實現ParseArgs來處理入口參數。代碼以下所示:
class MSKJASTAction: public PluginASTAction {
public:
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) {
return unique_ptr<MSKJASTConsumer> (new MSKJASTConsumer(ci));
}
bool ParseArgs(const CompilerInstance &ci, const vector<string> &args) {
return true;
}
};
複製代碼
② 編寫ASTConsumer
FrontActions是編寫Clang插件的入口,也是一個接口,是基於ASTFrontendAction的抽象基類。FrontActions爲接下來基於AST操做的函數提供了一個入口和工做環境。
經過這個接口,你能夠編寫在編譯過程當中自定義的操做,具體方式是:經過ASTFrontendAction在 AST上自定義操做,重載CreateASTConsumer函數返回你本身的Consumer,以獲取AST上的 ASTConsumer單元。ASTConsumer能夠提供不少入口,是一個能夠訪問AST的抽象基類,能夠重載 HandleTopLevelDecl()和 HandleTranslationUnit()兩個函數,以接收訪問AST時的回調。其中,HandleTopLevelDecl()函數是在訪問到全局變量、函數定義這樣最上層聲明時進行回調,HandleTranslationUnit()函數會在接收每一個節點訪問時的回調。
class MSKJASTConsumer: public ASTConsumer {
private:
MatchFinder matcher;
MSKJHandler handler;
public:
MSKJASTConsumer(CompilerInstance &ci) :handler(ci) {
matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler);
}
void HandleTranslationUnit(ASTContext &context) {
matcher.matchAST(context);
}
};
複製代碼
③ 處理節點
class MSKJHandler : public MatchFinder::MatchCallback {
private:
CompilerInstance &ci;
public:
MSKJHandler(CompilerInstance &ci) :ci(ci) {}
void run(const MatchFinder::MatchResult &Result) {
if (const ObjCInterfaceDecl *decl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("ObjCInterfaceDecl")) {
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::Error, "MSKJ:類名中不能帶有下劃線"));
}
}
}
};
複製代碼
在Clang插件源碼中編寫註冊代碼。編譯器會在編譯過程當中從動態庫加載Clang插件。使用FrontendPluginRegistry::Add<>在庫中註冊插件。註冊Clang插件的代碼以下:
static FrontendPluginRegistry::Add<MSKJPlugin::MSKJASTAction> X("MSKJPlugin", "The MSKJPlugin is my first clang-plugin.");
複製代碼
在Clang插件代碼的最下面,定義的MSKJPlugin字符串是命令行字符串,供之後調用時使用,The MSKJPlugin is my first clang-plugin是對Clang插件的描述。
利用CMake命令從新生成Xcode工程,可在Loadable modules下看到MSKJPlugin:
選擇MSKJPlugin這個target進行編譯,編譯完會生成一個動態庫文件。
LLVM官方有一個完整可用的Clang插件示例,能夠幫咱們打印出最上層函數的名字。
經過學習這個插件示例,看看如何使用Clang插件。
使用Clang插件能夠經過-load命令行選項加載包含插件註冊表的動態庫,-load命令行會加載已經註冊了的全部Clang插件。使用-plugin選項選擇要運行的Clang插件。Clang插件的其餘參數經過-plugin-arg-來傳遞。
cc1進程相似一種預處理,這種預處理會發生在編譯以前。cc1和Clang driver是兩個單獨的實體,cc1負責前端預處理,Clang driver則主要負責管理編譯任務調度,每一個編譯任務都會接受cc1前端預處理的參數,而後進行調整。
有兩個方法可讓-load 和-plugin等選項到Clang的cc1進程中:
下面是一個編譯Clang插件,而後使用-Xclang加載使用Clang插件的例子:
$ export BD=/path/to/build/directory
$ (cd $BD && make PrintFunctionNames )
$ clang++ -D_GNU_SOURCE -D_DEBUG -D__STDC_CONSTANT_MACROS \
-D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -D_GNU_SOURCE \
-I$BD/tools/clang/include -Itools/clang/include -I$BD/include -Iinclude \ tools/clang/tools/clang-check/ClangCheck.cpp -fsyntax-only \
-Xclang -load -Xclang $BD/lib/PrintFunctionNames.so -Xclang \
-plugin -Xclang print-fns
複製代碼
上面命令中,先設置構建的路徑,再經過make命令進行編譯生成PrintFunctionNames.so,最後使用clang命令配合-Xclang參數加載使用Clang插件。
你也能夠直接使用-cc1參數,可是就須要按照下面的方式來指定完整的文件路徑:
$ clang -cc1 -load ../../Debug+Asserts/lib/libPrintFunctionNames.dylib -plugin print-fns some-input-file.c
複製代碼
實現更復雜的插件功能,能夠利用clang的API對語法樹進行相應的分析與處理。
關於AST的資料:
Clang插件自己的編寫和使用並不複雜,關鍵是如何更好地應用到工做中,經過Clang插件不光可以檢查代 碼規範,還可以進行無用代碼分析、自動埋點打樁、線下測試分析、方法名混淆等。
理解iOS的編譯原理,有利於咱們更加深層次的理解程序,讓咱們從底層的角度去看待問題和思考問題的解決方案。
範沖沖,民生科技有限公司 用戶體驗技術部 移動金融開發平臺開發工程師
Thanks!