iOS之LLVM编译流程和Clang插件开发集成
生活随笔
收集整理的這篇文章主要介紹了
iOS之LLVM编译流程和Clang插件开发集成
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
LLVM 簡介
一、什么是 LLVM?
- LLVM 是構(gòu)架編譯器(compiler)的框架系統(tǒng),以 C++ 編寫而成,用于優(yōu)化以任意程序語言編寫的程序的編譯時間(compile-time)、鏈接時間(link-time)、運(yùn)行時間(runtime)以及空閑時間(idle-time),對開發(fā)者保持開放,并兼容已有腳本。
- LLVM 最早的時候是 Illinois 的一個研究項目,主要負(fù)責(zé)人是 Chris Lattner,他現(xiàn)在就職于Apple。Apple 目前也是 LLVM 項目的主要贊助者之一。
- 在理解 LLVM 時,我們可以認(rèn)為它包括了一個狹義的 LLVM 和一個廣義的 LLVM。廣義的 LLVM 其實(shí)就是指整個 LLVM 編譯器架構(gòu),包括了前端、后端、優(yōu)化器、眾多的庫函數(shù)以及很多的模塊;而狹義的 LLVM 其實(shí)就是聚焦于編譯器后端功能(代碼生成、代碼優(yōu)化、JIT等)的一系列模塊和庫。
二、傳統(tǒng)編譯器設(shè)計
- 傳統(tǒng)編譯器分三個階段: 前端(Frontend)-> 優(yōu)化器(Optimizer)-> 后端(Backend);
- 前端Frontend:負(fù)責(zé)分析源代碼,可以檢查語法級錯誤(包括詞法分析、語法分析、語義分析),并構(gòu)建針對語言的抽象語法樹(AST:Abstract Syntax Tree);抽象語法樹可以進(jìn)一步轉(zhuǎn)換為優(yōu)化,最終轉(zhuǎn)為新的表示方式,然后再交給讓優(yōu)化器和后端處理,LLVM 的前端還會生成中間代碼(intermediate representation,簡稱IR);最終由后端生成可執(zhí)行的機(jī)器碼(可以理解為 LLVM 是編譯器 + 優(yōu)化器, 接收的是 IR 中間代碼,輸出的還是 IR,給后端,經(jīng)過后端翻譯成目標(biāo)指令集);
- 優(yōu)化器 Optimizer:優(yōu)化器負(fù)責(zé)進(jìn)行各種優(yōu)化,改善代碼的運(yùn)行時間,例如消除冗余計算等;
- 后端 Backend(代碼生成器 Code Generator):將代碼映射到目標(biāo)指令集,生成機(jī)器代碼,并且進(jìn)行機(jī)器代碼相關(guān)的代碼優(yōu)化;
- 源碼 Source Code + 前端 Frontend + 優(yōu)化器 Optimizer + 后端 Backend(代碼生成器 CodeGenerator)+ 機(jī)器碼 Machine Code,如下所示:
- LLVM 的優(yōu)點(diǎn)在于,中間表示IR代碼編寫良好,而且不同的前端語言最終都轉(zhuǎn)換成同一種的IR。
三、iOS 的編譯器架構(gòu)
- OC、C、C++ 使用的編譯器前端是Clang,Swift是swift,后端都是LLVM,如下圖所示:
四、LLVM 的設(shè)計
- LLVM 設(shè)計的最重要方面是,使用通用的代碼表示形式(IR),它是用來在編譯器中表示代碼的形式,所有 LLVM 可以為任何編程語言獨(dú)立編寫前端,并且可以為任意硬件架構(gòu)獨(dú)立編寫后端,如下所示:
- LLVM的優(yōu)點(diǎn):
- 中間表示IR代碼編寫良好,而且不同的前端語言最終都轉(zhuǎn)換成統(tǒng)一的中間代碼LLVM IR(LLVM Intermediate Representation);
- 如果需要支持一種新的變成語言,那么只需要實(shí)現(xiàn)一個新的前端;
- 如果需要支持一種新的硬件設(shè)備,那么只需要實(shí)現(xiàn)一個新的后端;
- 優(yōu)化階段是一個通用的階段,它只針對統(tǒng)一的LLVM IR,不論是支持新的編程語言,還是支持新的硬件設(shè)備,都不需要對優(yōu)化階段做修改;
- LLVM現(xiàn)在被座位實(shí)現(xiàn)何種靜態(tài)和運(yùn)行時編譯語言的通用基礎(chǔ)結(jié)構(gòu)(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等);
- 相比之下,GCC的前端和后臺沒分的太開,前端后端耦合在一起,所以GCC為了支持一門新的語言,或者為了支持一個新的目標(biāo)平臺,就變的特別困難;
五、Clang
- clang 是 LLVM 項目中的一個子項目,它是基于 LLVM 架構(gòu)圖的輕量級編譯器,誕生之初是為了替代 GCC,提供更快的編譯速度,它是負(fù)責(zé) C、C++、OC 語言的編譯器,屬于整個 LLVM 架構(gòu)中的編譯器前端,對于開發(fā)者來說,研究 Clang 可以給我們帶來很多好處。
- 相比于 GCC,Clang 具有如下優(yōu)點(diǎn):
- 編譯速度塊:在某平臺上,Clang 的編譯速度顯著的快過 GCC(Debug 模式下編譯 OC 速度比 GCC 快 3 倍);
- 占用內(nèi)存小:Clang 生成的AST所占用的內(nèi)存是 GCC 的五分之一左右;
- 模塊化設(shè)計:Clang 采用基于庫的模塊化設(shè)計,易于 IDE 集成及其他用途的重用;
- 診斷信息可讀性強(qiáng):在編譯過程中,Clang 創(chuàng)建并保留了大量纖細(xì)的元數(shù)據(jù);(metadata),有利于調(diào)試和錯誤信息更加友善;
- 設(shè)計清晰簡單,易于理解,擴(kuò)展性強(qiáng)。
六、Clang 與 LLVM
LLVM編譯流程
一、通過命令打印源碼編譯階段
- clang -ccc-print-phases main.m
- 輸入文件:找到源文件
± 0: input, “main.m”, objective-c - 預(yù)處理階段:這個過程處理包括宏的替換,頭文件的導(dǎo)入
± 1: preprocessor, {0}, objective-c-cpp-output - 編譯階段:進(jìn)行詞法分析、語法分析、檢測語法是否正確,最終生成IR
± 2: compiler, {1}, ir - 后端:這里L(fēng)LVM會通過一個一個的pass去優(yōu)化,每個pass做一些事情,最終生成匯編代碼
± 3: backend, {2}, assembler - 匯編代碼生成目標(biāo)文件
± 4: assembler, {3}, object - 鏈接:鏈接需要的動態(tài)庫和靜態(tài)庫,生成可執(zhí)行文件
± 5: linker, {4}, image(鏡像文件) - 綁定:通過不同的架構(gòu),生成對應(yīng)的可執(zhí)行文件
6: bind-arch, “x86_64”, {5}, image
- 輸入文件:找到源文件
- 新建一個工程,cd 到 main.m 路徑,然后執(zhí)行 clang -ccc-print-phases main.m,結(jié)果如下:
二、預(yù)處理編譯階段
- 在 main.m 中鍵入以下代碼:
- 然后執(zhí)行 clang -E main.m,可以看到,結(jié)果如下:
- 不難看出,這個階段主要是處理了包括宏的替換和頭文件的導(dǎo)入;
三、編譯階段
① 詞法分析
- 預(yù)處理完成后就會進(jìn)行詞法分析,這里會把代碼切成一個個Token,比如大小括號、等于號還有字符串等。
- 通過如下命令查看詞法分析的結(jié)果:
- 執(zhí)行結(jié)果如下:
- 如果頭文件找不到,指定sdk:
② 語法分析
- 詞法分析完成后就是語法分析,它的任務(wù)是驗證語法是否正確,在詞法分析的基礎(chǔ)上將單詞序列組合成各類此法短語,如程序、語句、表達(dá)式等,然后將所有節(jié)點(diǎn)組成抽象語法樹(Abstract Syntax TreeAST),語法分析程序判斷程序在結(jié)構(gòu)上是否正確。
- 可以通過下面命令查看語法分析的結(jié)果:
- 如果導(dǎo)入頭文件找不到,可以指定SDK:
- 語法分析的結(jié)果如下所示:
③ 生成中間代碼IR
- 生成中間代碼IR,代碼生成器(Code Generation)會將語法樹自頂向下遍歷逐步翻譯成LLVM IR;
- 可以通過下面命令可以生成.ll的文本文件,查看IR代碼。
- OC 代碼在這一步會進(jìn)行 runtime 橋接:property合成、ARC處理等;
- IR 的基本語法:
- @ 全局標(biāo)識
- % 局部標(biāo)識
- alloca 開辟空間
- align 內(nèi)存對齊
- i32 32bit 4個字節(jié)
- store 寫入內(nèi)存
- load 讀取數(shù)據(jù)
- call 調(diào)用函數(shù)
- ret 返回
- 生成的中間代碼.ll文件如下:
- 其中,test 函數(shù)的參數(shù)為:
- IR 文件在OC中是可以進(jìn)行優(yōu)化的,一般設(shè)置是在target - Build Setting - Optimization Level(優(yōu)化器等級)中設(shè)置。LLVM的優(yōu)化級別分別是-O0 -O1 -O2 -O3 -Os(第一個是大寫英文字母O),下面是帶優(yōu)化的生成中間代碼IR的命令:
- 優(yōu)化之后如下所示:
- Xcode7 以后開啟 bitcode,蘋果會做進(jìn)一步優(yōu)化,生成.bc的中間代碼,通過優(yōu)化后的IR代碼生成 .bc 代碼:
四、生成匯編代碼(后端)
- 通過最終的.bc或者.ll代碼生成匯編代碼:
- 生成匯編代碼也可以進(jìn)行優(yōu)化:
- 此時生成的main.s文件的格式為匯編代碼:
五、生成目標(biāo)文件(編譯器)
- 目標(biāo)文件的生成,是匯編器以匯編代碼作為插入,將匯編代碼轉(zhuǎn)換為機(jī)器代碼,最后輸出目標(biāo)文件(object file)
- 可以通過 nm 命令,查看下 main.o 中的符號:
- 以下是 main.o 中的符號,其文件格式為目標(biāo)文件:
- 分析說明:
- _printf 函數(shù)是一個是undefined 、external 的
- undefined 表示在當(dāng)前文件暫時找不到符號_printf
- external 表示這個符號是外部可以訪問的
六、鏈接
- 鏈接主要是鏈接需要的動態(tài)庫和靜態(tài)庫,生成可執(zhí)行文件,其中
- 靜態(tài)庫會和可執(zhí)行文件合并;
- 動態(tài)庫是獨(dú)立的;
- 連接器把編譯生成的 .o 文件和 .dyld .a 文件鏈接,生成一個mach-o文件:
- 查看鏈接之后的符號:
- 其中的undefined表示會在運(yùn)行時進(jìn)行動態(tài)綁定:
- 查看 main 是什么格式,此時是 mach-o可執(zhí)行文件:
七、綁定
綁定主要是通過不同的架構(gòu),生成對應(yīng)的mach-o格式可執(zhí)行文件
八、LLVM 的編譯流程如下
Clang插件開發(fā)
一、LLVM 下載
- 由于國內(nèi)網(wǎng)絡(luò)限制,需要借助鏡像下載 LLVM 的源碼:LLVM 的鏡像鏈接
- 下載 LLVM 項目:
- 在 LLVM 的 tools 目錄下下載 Clang:
- 在 LLVM 的 projects 目錄下下載 compiler-rt、libcxx、libcxxabi:
- 在 Clang 的 tools 下安裝 extra 工具:
二、LLVM 編譯
由于最新的 LLVM 只支持 cmake 來編譯,所以需要安裝 cmake。
① 安裝 cmake
- 查看brew是否安裝cmake,如果已經(jīng)安裝,則跳過下面步驟:
- 通過 brew 安裝 cmake:
② 通過 Xcode 編譯 LLVM
- cmake 編譯成 Xcode 項目:
- 如果編譯過程中遇到如下的錯誤:刪除構(gòu)建目錄下的 CMakeCache.txt 即可。
- 使用 Xcode 編譯 Clang,選擇自動創(chuàng)建 Schemes:
- 編譯(CMD + B),選擇 ALL_BUILD Secheme 進(jìn)行編譯(時間較長,預(yù)計一個小時以上)
- 如果編譯過程中遇到錯誤:error: The i386 architecture is deprecated. You should update your ARCHS build setting to remove the i386 architecture。
只需要將對應(yīng)中的 Build Settings 選項 Architectures 中的值切換為 Standard Architectures(64-bit Intel) 即可。
或者:選擇手動創(chuàng)建Schemes,然后編譯編譯 Clang + ClangTooling 即可。
③ 通過 ninja 編譯 LLVM
- 使用 ninja 進(jìn)行編譯則還需要安裝 ninja,使用以下命令安裝ninja:
- 在 LLVM 源碼根目錄下新建 build_ninja 目錄,最終會在 build_ninja 目錄下生成build.ninja;
- 在 LLVM 源碼根目錄下新建 llvm_release 目錄,最終編譯文件會在 llvm_release 文件夾路徑下:
- 一次執(zhí)行編譯,安裝指令:
三、創(chuàng)建插件
- 在 /llvm/tools/clang/tools 目錄下新建插件 YDWPlugin:
- 在 /llvm/tools/clang/tools 目錄下的 CMakeLists.txt 文件,新增add_clang_subdirectory(YDWPlugin),此處的 YDWPlugin 即為上一步創(chuàng)建的插件名稱;
- 在 YDWPlugin 目錄下新建兩個文件,分別是 YDWPlugi.cpp 和 CMakeLists.txt,并在CMakeLists.txt 中加上以下代碼:
- 利用 cmake 重新生成 Xcode 項目,在 build_xcode 目錄下執(zhí)行以下命令:
- 最后可以在 LLVM 的 Xcode 項目中可以看到 Loadable modules 目錄下由自定義的YDWPlugin 目錄,然后就可以在里面編寫相應(yīng)的插件代碼。
四、編寫插件代碼
- 在 YDWPlugin 目錄下的 YDWPlugin.cpp 文件中,加入以下代碼:
- 原理分析:
- ① 注冊插件,并自定義AST語法樹Action類
- 繼承自 PluginASTAction,自定義 ASTAction,需要重載兩個方法 ParseArgs 和 CreateASTConsumer,其中的重點(diǎn)方法是 CreateASTConsumer,方法中有個參數(shù) CI 即編譯實(shí)例對象,主要用于判斷文件是否是屬于用戶和拋出警告;
- 通過 FrontendPluginRegistry 注冊插件,需要關(guān)聯(lián)插件名與自定義的 ASTAction 類;
- ② 掃描配置完畢
- 繼承自 ASTConsumer 類,實(shí)現(xiàn)自定義的子類 YDWASTConsumer,有兩個參數(shù) MatchFinder 對象 matcher 以及 YDWMatchCallback 自定義的回調(diào)對象 callback;
- 實(shí)現(xiàn)構(gòu)造函數(shù),主要是創(chuàng)建 MatchFinder 對象,以及將 CI 傳遞給回調(diào)對象;
- 實(shí)現(xiàn)兩個回調(diào)方法:HandleTopLevelDecl:解析完一個頂級的聲明,就回調(diào)一次;
HandleTranslationUnit:整個文件都解析完成的回調(diào),將文件解析完畢后的上下文context(即AST語法樹) 給 matcher;
- ③ 掃描完畢的回調(diào)函數(shù)
- 繼承自 MatchFinder::MatchCallback,自定義回調(diào)類 YDWMatchCallback;
- 定義 CompilerInstance 私有屬性,用于接收 ASTConsumer 類傳遞過來的 CI 信息;
- 重寫 run 方法:
- 通過 result,根據(jù)節(jié)點(diǎn)標(biāo)記,獲取相應(yīng)節(jié)點(diǎn),此時的標(biāo)記需要與 YDWASTConsumer 構(gòu)造方法中一致;
- 判斷節(jié)點(diǎn)有值,并且是用戶文件即 isUserSourceCode 私有方法;
- 獲取節(jié)點(diǎn)的描述信息;
- 獲取節(jié)點(diǎn)的類型,并轉(zhuǎn)成字符串;
- 判斷應(yīng)該使用 copy,但是沒有使用 copy;
- 通過 CI 獲取診斷引擎;
- 通過診斷引擎報告錯誤;
- ① 注冊插件,并自定義AST語法樹Action類
五、測試插件
- 在終端中測試插件:
- 結(jié)果展示:
六、Xcode 集成插件
- 加載插件,打開測試項目,在 target -> Build Settings -> Other C Flags 添加以下內(nèi)容:
- 設(shè)置編譯器,由于 Clang 插件需要使用對應(yīng)的版本去加載,如果版本不一致會導(dǎo)致編譯失敗,如下所示:
- 在 Build Settings 欄目中新增兩項用戶定義的設(shè)置,分別是 CC 和 CXX;
- CC 對應(yīng)的是自己編譯的 Clang 的絕對路徑;
- CXX 對應(yīng)的是自己編譯的 Clang++ 的絕對路徑;
- 接下來在 Build Settings 中搜索 index,將 Enable Index-Wihle-Building Functionality 的 Default 改為 NO;
- 最后,重新編譯測試項目,會出現(xiàn)下面的效果:
總結(jié)
以上是生活随笔為你收集整理的iOS之LLVM编译流程和Clang插件开发集成的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS之性能优化·优化App的启动速度
- 下一篇: iOS之深入解析Block的使用和外部变