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