iOS 編譯原理與應用

引言

在Xcode中,當咱們按下command + B進行build操做後發生了那些事情,這是一個將代碼編譯的過程。Xcode如今使用的編譯器是LLVM,Xcode 早期使用的是GCC編譯器,因爲一些歷史緣由,從Xcode5開始正式過渡到使用LLVM編譯器。下文將着重介紹LLVM。html

編譯原理

LLVM簡介

  • 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

傳統的編譯器架構

  • Frontend:前端,對源碼作詞法分析、語法分析、語義分析、生成中間代碼
  • Optimizer:優化器,用於中間代碼優化
  • Backend:後端,用於生成機器碼

LLVM架構

  • 前端將各類類型的源代碼編譯爲中間代碼,也就是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從新編寫一套目標平臺的後端便可。

Clang

  • 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與LLVM

  • 廣義的LLVM:整個LLVM架構
  • 狹義的LLVM:LLVM後端(代碼優化、目標代碼生成等)

OC源文件的編譯過程

  • 命令行查看編譯的過程:
clang -ccc-print-phases main.m
複製代碼

  • 查看preprocessor(預處理)的結果:
clang -E main.m -F /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks 
複製代碼

1. 詞法分析

詞法分析,生成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類:

  • 關鍵字:語法中的關鍵字,好比 if、else、while、for 等;
  • 標識符:變量名;
  • 字面量:值、數字、字符串;
  • 特殊符號:加減乘除等符號。

2. 語法分析

利用上面輸出的Token先按照語法組合成語義,生成相似VarDecl這樣的節點,而後將這些節點按照層級關係構成抽象語法樹(AST)。

  • 語法分析,生成語法樹(AST,Abstract Syntax Tree)
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
複製代碼

TranslationUnitDecl是根節點,表示一個編譯單元;Decl表示一個聲明;Expr表示的是表達式;Literal表示字面量,是一個特殊的Expr;Stmt表示陳述。

除此以外,Clang還有衆多種類的節點類型。Clang裏,節點主要分紅Type類型、Decl聲明、Stmt陳述這三種,其餘的都是這三種的派生。經過擴展這三類節點,就可以將無限的代碼形態用有限的形式來表現出來。

3. LLVM IR

LLVM IR有3種表示形式,但本質上是等價的。

  • text:便於閱讀的文本格式,相似於彙編語言,拓展名 .ll
clang -S -emit-llvm main.m
複製代碼
  • memory:內存格式
  • bitcode:二進制格式,拓展名 .bc
clang -c -emit-llvm main.m
複製代碼

4. IR基本語法

.ll 文件部份內容,以下:

  • 註釋以分號 ; 開頭
  • 全局標識符以@開頭,局部標示符以%開頭
  • alloca,在當前函數棧幀中分配內存
  • i32 ,32bit,4 個字節
  • align,內存對齊
  • store,寫入數據
  • load,讀取數據

應用與實踐

基於LLVM 、Clang能夠作不少實踐,以下:

  • LibClang、LibTooling、Clang Plugin

官方參考:

clang.llvm.org/docs/Toolin…

應用:語法樹分析、語言轉換等

  • OCLint、Clang靜態分析器(Clang Static Analyzer)

  • Clang插件開發

官方參考:

clang.llvm.org/docs/ClangP…

clang.llvm.org/docs/Extern…

clang.llvm.org/docs/RAVFro…

應用:代碼檢查(命名規範、代碼規範)等

  • Pass開發

官方參考:

llvm.org/docs/Writin…

應用:中間代碼優化、代碼混淆等

  • 開發新的編程語言

llvm-tutorial-cn.readthedocs.io/en/latest/i…

kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh_CN/lates…

編寫及運行Clang插件

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在功能上是最全的。

1. 源碼下載

下載 LLVM Project

git clone https://github.com/llvm/llvm-project.git
複製代碼

上圖中,clang目錄就是類C語言編譯器的代碼目錄;llvm目錄的代碼包含兩部分,一部分是對源碼進行平臺 無關優化的優化器代碼,另外一部分是生成平臺相關彙編代碼的生成器代碼;lldb目錄裏是調試器的代碼;lld裏是連接器代碼。

2. 源碼編譯

macOS屬於類UNIX平臺,所以既能夠生成Makefile文件來編譯,也能夠生成Xcode工程來編譯。

進入llvm-project文件目錄,生成Makefile文件:

  • 在llvm同級目錄下新建一個llvm_make目錄
  • 在llvm_make中利用CMake進行編譯
cmake -DLLVM_ENABLE_PROJECTS=clang -G "Unix Makefiles" ../llvm
複製代碼

生成Xcode工程,可使用以下命令

  • 安裝CMake工具
  • 在llvm同級目錄下新建一個llvm_xcode目錄
  • 在llvm_xcode中利用CMake進行編譯
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 Commandline Tools的話,能夠執行以下命令安裝:
xcode-select --install
複製代碼
  • 若是你已經安裝了Xcode Commandline Tools的話,直接reset便可
sudo xcode-select --reset
複製代碼

生成Xcode工程後,打開生成的LLVM.xcodeproj文件,選擇Automatically Create Schemes。

生成Xcode項目後再利用Xcode進行編譯,可是速度很慢

3. 插件目錄

  • 在clang/tools源碼目錄下新建一個插件目錄,好比叫作mskj_plugin,添加MSKJPlugin.cpp文件和 CMakeLists.txt文件。其中,CMake編譯須要經過CMakeLists.txt文件來指導編譯,cpp是源文件。

  • 在clang/tools目錄下的CMakeList.txt文件當中最後一行加入:
add_clang_subdirectory(mskj-plugin)
複製代碼
  • 使用以下代碼編寫clang/tools/mskj-plugin/CMakeLists.txt文件,來定製編譯流程:
add_llvm_library(MSKJPlugin MODULE MSKJPlugin.cpp PLUGIN_TOOL clang)
複製代碼

MSKJPlugin是插件名,MSKJPlugin.cpp是源代碼文件,這段代碼是指,要將Clang插件代碼集成到LLVM的Xcode工程中,並做爲一個模塊進行編寫調試。添加了Clang插件的目錄和文件後,再次用cmake命令生成Xcode工程,裏面就可以集成MSKJPlugin.cpp文件。

4. 編寫插件源碼

① 編寫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:類名中不能帶有下劃線"));
                }
            }
        }
    };
複製代碼

5. 註冊Clang插件

在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插件的描述。

6. 使用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進程中:

  • 直接使用-cc1選項,缺點是要在命令行上指定完整的系統路徑配置;
  • 使用-Xclang來爲cc1進程添加這些選項。-Xclang參數只運行預處理器,直接將後面參數傳遞給cc1進程,而不影響clang driver的工做。

下面是一個編譯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
複製代碼

7. 更多

實現更復雜的插件功能,能夠利用clang的API對語法樹進行相應的分析與處理。

關於AST的資料:

clang.llvm.org/doxygen/nam…

clang.llvm.org/doxygen/cla…

clang.llvm.org/doxygen/cla…

Clang插件自己的編寫和使用並不複雜,關鍵是如何更好地應用到工做中,經過Clang插件不光可以檢查代 碼規範,還可以進行無用代碼分析、自動埋點打樁、線下測試分析、方法名混淆等。

結語

理解iOS的編譯原理,有利於咱們更加深層次的理解程序,讓咱們從底層的角度去看待問題和思考問題的解決方案。

做者簡介

範沖沖,民生科技有限公司 用戶體驗技術部 移動金融開發平臺開發工程師

Thanks!

相關文章
相關標籤/搜索