iOS的編譯過程 LLVM Clang

前言

語言類型

我們有很多維度可以將計算機語言進行分類纬黎,其中以編譯/執(zhí)行方式為維度,可以將計算機語言分為:

  • 編譯型語言

    • C++ Objective C Swift Kotlin
    • 先通過編譯器生成機器碼,機器碼可以直接在 CPU 上執(zhí)行
    • ?? 執(zhí)行效率較高
    • ?? 調(diào)試周期長
  • 直譯式語言(腳本語言)

    • JavaScript Python
    • 不需要經(jīng)過編譯缩幸,在執(zhí)行時通過一個中間的解釋器將代碼解釋為 CPU 可以執(zhí)行的代碼
    • ?? 編寫調(diào)試方便
    • ?? 執(zhí)行效率低

編譯型語言和直譯式語言的編譯過程如下

image

從上圖我們可以知道千贯,編譯型語言需要在運行之前就將代碼全部編譯好,最終運行的文件是編譯后的可執(zhí)行文件送朱。我們將編譯型語言所使用的編譯方式稱為 AOT (Ahead of time) 預(yù)先編譯娘荡。

而直譯式語言則是在運行的過程中,一邊編譯一邊執(zhí)行驶沼,最終運行的文件其實就是一開始寫的源代碼炮沐。我們將直譯式語言所使用的編譯方式稱為 JIT (Just in time)即時編譯。

iOS 編譯工具

iOS 在 Xcode 5 版本前使用的是 GCC 編譯器回怜,在 Xcode 5 中將 GCC 徹底拋棄大年,替換為了 LLVM 。LLVM 包含了編譯器前端玉雾、優(yōu)化器和編譯器后端三大模塊翔试。其中 Swift 除了在編譯器前端和 Objective-C 稍有不同,其他模塊都差不多复旬。

Objective-C 采用 Clang 作為編譯器前端

image

Swift 采用 Swift 作為編譯器前端

image

LLVM

作者 Chris Lattner

image

Chris Lattner 在2000年開發(fā)了一個叫作 Low Level Virtual Machine 的編譯器開發(fā)工具套件垦缅,后來涉及范圍越來越大,可以用于常規(guī)編譯器驹碍,JIT 編譯器壁涎,匯編器,調(diào)試器,靜態(tài)分析工具等一系列跟編程語言相關(guān)的工作柬脸,于是就把簡稱 LLVM 這個簡稱作為了正式的名字驼抹。Chris Lattner 后來又開發(fā)了 Clang ,使得 LLVM 直接挑戰(zhàn) GCC 的地位竟坛。

2005年加入蘋果革半,將蘋果使用的 GCC 全面轉(zhuǎn)為 LLVM。

2010年開始主導(dǎo)開發(fā) Swift 語言流码。

2017年離開了 Apple 入職特斯拉負責(zé)自動駕駛軟件的開發(fā)又官,并于同年下半年入職 Google 加入深度學(xué)習(xí)與人工智能研發(fā)團隊。

LLVM 簡介

LLVM 是一個開源的漫试,模塊化和可重用的編譯器和工具鏈技術(shù)的集合六敬,或者說是一個編譯器套件。

可以使用 LLVM 來編譯 Kotlin驾荣,Ruby外构,Python,Haskell播掷,Java审编,D,PHP歧匈,Pure垒酬,Lua 和許多其他語言

LLVM 核心庫還提供一個優(yōu)化器,對流行的 CPU 做代碼生成支持件炉。

LLVM 同時支持 AOT 預(yù)先編譯和 JIT 即時編譯

2012年勘究,LLVM 獲得美國計算機學(xué)會 ACM 的軟件系統(tǒng)大獎,和 UNIX斟冕,WWW口糕,TCP/IP,Tex磕蛇,JAVA 等齊名景描。

LLVM IR

LLVM IR (Intermediate Representation)直譯過來是“中間描述”,它是整個編譯過程中生成的區(qū)別于源碼和機器碼的一種中間代碼秀撇。IR 提供了獨立于任何特定機器架構(gòu)的源語超棺,因此它是 LLVM 優(yōu)化和進行代碼生成的關(guān)鍵,也是 LLVM 有別于其他編譯器的最大特點捌袜。LLVM 的核心功能都是圍繞的 IR 建立的说搅,它是 LLVM 編譯過程中前端的輸出,后端的輸入虏等。

在這一點上 IR 和 JVM 的 Java bytecode 很像弄唧,兩者都是用于表述計算的模型适肠,但兩者所處的抽象層次不同。Java bytecode 更高層(更抽象)候引,包含了大量類 Java 的面向?qū)ο笳Z言的操作侯养。LLVM IR 則更底層(更接近機器)。IR 的存在意味著它可以作為多種語言的后端澄干,這樣 LLVM 就能夠提供和語言無關(guān)的優(yōu)化逛揩,同時還能夠方便的針對多種 CPU 代碼生成。

為什么需要 IR

編譯器的架構(gòu)分為前端麸俘、優(yōu)化器和后端。傳統(tǒng)編譯器(如 CGG )的前端和后端沒有完全分離逞泄,耦合在了一起拜效,因而如果要支持一門新的語言或硬件平臺喷众,需要做大量的工作紧憾。而 LLVM 和傳統(tǒng)編譯器最大的不同點在于,前端輸入的任何語言赴穗,在經(jīng)過編譯器前端處理后憔四,生成的中間碼都是 IR 格式的望抽。

傳統(tǒng)的靜態(tài)編譯器

image

LLVM 編譯器

image

這樣做的優(yōu)點是如果需要支持一種新的編程語言煤篙,那么我們只需要實現(xiàn)一種新的前端辑奈。如果我們需要支持一種新的硬件設(shè)備已烤,那我們只需要實現(xiàn)一個新的后端胯究。而優(yōu)化階段因為是針對了統(tǒng)一的 LLVM IR ,所以它是一個通用的階段臣嚣,不論是支持新的編程語言硅则,還是支持新的硬件設(shè)備,這里都不需要對優(yōu)化階段做修改暑认。所以從這里可以看出 LLVM IR 的作用蘸际。

LLVM IR 的三種格式:
  • 內(nèi)存中的編譯中間語言
  • 硬盤上存儲的可讀中間格式(以 .ll 結(jié)尾)
  • 硬盤上存儲的二進制中間語言(以 .bc 結(jié)尾)

這三種中間格式是完全等價的徒扶。

Bitcode

iOS 開發(fā)的小伙伴可能對 IR 不是很了解酷愧,但我相信你一定聽說過 Bitcode 。Bitcode 說白了其實就是我們前面提到的 LLVM IR 三種格式中的第三種乍迄,即存儲在磁盤上的二進制文件(以 .bc 結(jié)尾)闯两。

之所以要把 Bitcode 拿出來單獨說谅将,是因為 Apple 單獨對 Bitcode 進行了額外的優(yōu)化饥臂。從 Xcode 7 開始,Apple 支持在提交 App 編譯產(chǎn)物的同時提交 App 的 Bitcode (非強制)稽煤,并且之后對提交了 Bitcode 的 App 都單獨進行了云端編譯打包酵熙。也就是說驰坊,即便在提交時已經(jīng)將本地編譯好的 ipa 提交到 App Store,Apple 最終還是會使用 Bitcode 在云端再次打包借嗽,并且最終用戶下載到手機上的版本也是由 Apple 在云端編譯出來的版本恶导,而非開發(fā)人員在本地編譯的版本浸须。

這里有一篇文章Xcode 7 Bitcode的工作流程及安全性評估删窒,揭示了360團隊如何通過一個小 trick 來驗證 Apple 審核人員安裝的 App 是直接由本地編譯出來的版本還是云端通過 Bitcode 編譯出來的版本肌索,并由此發(fā)現(xiàn)了一個可能繞過審核的漏洞诚亚。

為什么需要 Bitcode

Apple 之所以這么做,一是因為 Apple 可以在云端編譯過程中做一些額外的針對性優(yōu)化工作闸准,而這些額外的優(yōu)化是本地環(huán)境所無法實現(xiàn)的夷家。二是 Apple 可以為安裝 App 的目標(biāo)設(shè)備進行二進制優(yōu)化库快,減少安裝包的下載大小钥顽。

比如我們在本地編譯生成的 ipa 是同時包含了多個CPU架構(gòu)的(armv7 耳鸯,arm64 )县爬,對于 iPhone X 而言 armv7 的架構(gòu)文件就是無用文件添谊。而 Apple 可以在云端為不同的設(shè)備編譯出對應(yīng) CPU 架構(gòu)的 ipa ,這樣 iPhone X 下載到的 App 就只包含了所需的 arm64 架構(gòu)文件扎瓶。

更為黑科技的是碌燕,由于 Bitcode 是無關(guān)設(shè)備架構(gòu)的,它可以被轉(zhuǎn)化為任何被支持的 CPU 架構(gòu)愈捅,包括現(xiàn)在還沒被發(fā)明的 CPU 架構(gòu)蓝谨。以后如果蘋果新出了一款新手機并且 CPU 也是全新設(shè)計的譬巫,在蘋果后臺服務(wù)器一樣可以從這個 App 的 Bitcode 開始編譯轉(zhuǎn)化為新 CPU 上的可執(zhí)行程序缕题,可供新手機用戶下載運行這個 App 烟零,而無需開發(fā)人員重新在本地編譯打包上傳咸作。

Clang & Swift

Clang 編譯器

Clang 是 LLVM 的子項目记罚,是 C桐智、C++ 和 Objective-C 編譯器,目標(biāo)是替代傳統(tǒng)編譯器 GCC 然磷。Clang 在整個 Objective-C 編譯過程中扮演了編譯器前端的角色姿搜,同時也參與到了 Swift 編譯過程中的 Objective-C API 映射階段舅柜。

Clang 的主要功能是輸出代碼對應(yīng)的抽象語法樹( AST )致份,針對用戶發(fā)生的編譯錯誤準(zhǔn)確地給出建議,并將代碼編譯成 LLVM IR绍载。

Clang 的特點是編譯速度快逛钻,模塊化曙痘,代碼簡單易懂边坤,診斷信息可讀性強谅年,占用內(nèi)存小以及容易擴展和重用等融蹂。

我們以 Xcode 為例超燃,Clang 編譯 Objective-C 代碼的速度是 Xcode 5 版本前使用的 GCC 的3倍意乓,其生成的 AST 所耗用掉的內(nèi)存僅僅是 GCC 的五分之一左右届良。

image

Clang 的主要工作:

  • 預(yù)處理: 比如把宏嵌入到對應(yīng)的位置,頭文件的導(dǎo)入,去除注釋( clang -E main.m )
  • 詞法分析: 這里會把代碼切成一個個 Token士葫,比如大小括號为障,等于號還有字符串等
  • 語法分析: 驗證語法是否正確
  • 生成 AST : 將所有節(jié)點組成抽象語法樹 AST
  • 靜態(tài)分析:分析代碼是否存在問題鳍怨,給出錯誤信息和修復(fù)方案
  • 生成 LLVM IR: CodeGen 會負責(zé)將語法樹自頂向下遍歷逐步翻譯成 LLVM IR

Swift 編譯器

和 Clang 一樣,Swift 編譯器主要負責(zé)對 Swift 源代碼進行靜態(tài)分析和糾錯声滥,并轉(zhuǎn)換為 LLVM IR 落塑。他是 Swift 編譯的前端模塊憾赁。不過和 Clang 不同龙考,Swift 編譯器會多出 SIL optimizer 晦款,它會先將 Swift 文件轉(zhuǎn)換成中間代碼 SIL 枚冗,然后再根據(jù) SIL 生成 IR 赁温。是不是覺得很復(fù)雜股囊,Swift 編譯器會在編譯其間生成兩種不同的中間代碼毁涉,這是為什么呢贫堰?下面會有詳細的解釋其屏。

Swift 編譯器的主要工作:

  • 解析:解析器負責(zé)生成沒有任何語義或類型信息的抽象語法樹( AST )偎行,并針對輸入源的語法問題發(fā)出警告或錯誤
  • 詞法分析:獲取解析的 AST 并將其轉(zhuǎn)換為格式良好,完全類型檢查的AST形式膨更,為源代碼中的詞法問題發(fā)出警告或錯誤
  • Clang 導(dǎo)入器:導(dǎo)入 Clang 模塊并將它們導(dǎo)出的 C 或 Objective-C API 映射到相應(yīng)的 Swift API
  • SIL 生成:將經(jīng)過類型檢查的 AST 降級為 SIL
  • SIL 規(guī)范化:執(zhí)行額外的數(shù)據(jù)流診斷(例如使用未初始化的變量)
  • SIL 優(yōu)化:為程序執(zhí)行額外的高級 Swift 特定優(yōu)化荚守,包括自動引用計數(shù)優(yōu)化练般,虛擬化和通用專業(yè)化
  • LLVM IR 生成:將 SIL 降級到 LLVM IR

為什么要增加 SIL 層

Swift 中間語言( SWIFT Integration Layer )是一種高級的薄料,特定于 Swift 的中間語言摄职,適用于進一步分析和優(yōu)化 Swift 代碼琳钉。SIL 屬于 High-Level IR歌懒,其相對于LLVM IR 的抽象層級更高及皂,而且是特定于 Swift 語言的验烧。

由于源碼和 LLVM IR 之間存在著非常大的抽象鴻溝碍拆,IR 不適用對源碼進行分析和檢查感混。因此 Clang 使用了 Analysis 通過 CFG (控制流圖)來對代碼進行分析和檢查。但是 CFG 本身不夠精準(zhǔn)婆跑,且不在主流程上(會和 IR 生成過程并行執(zhí)行)滑进,因此 CFG 和 IR 生成中會出現(xiàn)部分重復(fù)分析扶关,做無用功驮审。

而在 Swift 的編譯過程中疯淫,SIL 會在生成 LLVM IR 之前做好所有的分析和規(guī)范化熙掺,并在 IRGen 的幫助下降級到 LLVM IR 币绩,避免了部分重復(fù)任務(wù)缆镣,也使得整個編譯流程更加統(tǒng)一董瞻。

image
image

而且因為 Swift 在編譯時就完成了方法綁定直接通過地址調(diào)用屬于強類型語言钠糊,方法調(diào)用不再是像 Objective-C 那樣的消息轉(zhuǎn)發(fā)抄伍,這樣編譯就可以獲得更多的信息用在后面的后端優(yōu)化上截珍。因此我們可以在 SIL 上對 Swift 做針對性的優(yōu)化岗喉,而這些優(yōu)化是 LLVM IR 所無法實現(xiàn)的沈堡。

這些優(yōu)化包括:
  • 臨界拆分:不支持任意的基礎(chǔ) block 參數(shù)通過終端進行臨界拆分
  • 泛型優(yōu)化:分析泛型函數(shù)的特定調(diào)用,并生成新的特定版本的函數(shù).然后將泛型的特定用法全部重寫為對應(yīng)的特定函數(shù)的直接調(diào)用
  • witness和虛函數(shù)表的去虛擬化優(yōu)化:通過給定類型去查找關(guān)聯(lián)的類的虛函數(shù)表或者類型的 witness 表,并將虛函數(shù)調(diào)用替換為調(diào)用函數(shù)映射
  • 內(nèi)聯(lián)優(yōu)化:對于transparent函數(shù)進行內(nèi)聯(lián)
  • 內(nèi)存提升:將 alloc_box 結(jié)構(gòu)優(yōu)化為 alloc_stack
  • 引用計數(shù)優(yōu)化
  • 高級領(lǐng)域特定優(yōu)化:對基礎(chǔ)的 Swift 類型容器(類似 ArrayString )實現(xiàn)了高級優(yōu)化

通過分析和檢查的安全 SIL 會被 IRGen 轉(zhuǎn)換成 LLVM IR鲸拥,并進一步接受 LLVM 的優(yōu)化刑赶。

動手實操

  1. 首先撞叨,我們寫一個簡單的程序牵敷,只有一個入口函數(shù)和簡單的邏輯枷餐。

    #import <Foundation/Foundation.h>
    #define DEFINEEight 8
    
    void test(int a, int b) {
    int c = a + b - DEFINEEight;
    }
    
  2. 寫好代碼后毛肋,通過以下命令润匙,LLVM 會預(yù)處理你的代碼孕讳,比如把宏嵌入到對應(yīng)的位置卫病,頭文件的導(dǎo)入典徘,去除注釋等逮诲。

    clang -E main.m
    

    得到的就是這樣的代碼

    # 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
    # 185 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
    # 2 "main.m" 2
    
    void test(int a, int b) {
    int c = a + b - 8;
    }
    
  1. 預(yù)處理完成后就會進行詞法分析裆甩,這里會把代碼切成一個個 Token 嗤栓,每一個 Token 都代表了一個特征元素茉帅。

    Token 的分類
    • 關(guān)鍵字:語法中的關(guān)鍵字堪澎,if else while for 等樱蛤。

    • 標(biāo)識符:變量名

    • 字面量:值昨凡,數(shù)字土匀,字符串

    • 特殊符號:加減乘除等符號

    通過以下代碼可以得到詞法分析后的代碼就轧。

    clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
    

    得到的詞法分析代碼

    void 'void'      [StartOfLine]  Loc=<main.m:4:1>
    identifier 'test'        [LeadingSpace] Loc=<main.m:4:6>
    l_paren '('             Loc=<main.m:4:10>
    int 'int'               Loc=<main.m:4:11>
    identifier 'a'   [LeadingSpace] Loc=<main.m:4:15>
    comma ','               Loc=<main.m:4:16>
    int 'int'        [LeadingSpace] Loc=<main.m:4:18>
    identifier 'b'   [LeadingSpace] Loc=<main.m:4:22>
    r_paren ')'             Loc=<main.m:4:23>
    l_brace '{'      [LeadingSpace] Loc=<main.m:4:25>
    int 'int'        [StartOfLine] [LeadingSpace]   Loc=<main.m:5:5>
    identifier 'c'   [LeadingSpace] Loc=<main.m:5:9>
    equal '='        [LeadingSpace] Loc=<main.m:5:11>
    identifier 'a'   [LeadingSpace] Loc=<main.m:5:13>
    plus '+'         [LeadingSpace] Loc=<main.m:5:15>
    identifier 'b'   [LeadingSpace] Loc=<main.m:5:17>
    minus '-'        [LeadingSpace] Loc=<main.m:5:19>
    numeric_constant '8'     [LeadingSpace] Loc=<main.m:5:21 <Spelling=main.m:2:21>>
    semi ';'                Loc=<main.m:5:32>
    r_brace '}'      [StartOfLine]  Loc=<main.m:6:1>
    eof ''          Loc=<main.m:6:2>
    
  2. 然后是語法分析,驗證語法是否正確乎莉。確認(rèn)無誤后將所有節(jié)點組成抽象語法樹 AST 惋啃。

    clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
    

    得到的 AST

    |-FunctionDecl 0x7f9a2108a0c0 <line:4:1, line:6:1> line:4:6 test 'void (int, int)'
    | |-ParmVarDecl 0x7f9a21089f48 <col:11, col:15> col:15 used a 'int'
    | |-ParmVarDecl 0x7f9a21089fc0 <col:18, col:22> col:22 used b 'int'
    | `-CompoundStmt 0x7f9a2108a348 <col:25, line:6:1>
    |   `-DeclStmt 0x7f9a2108a330 <line:5:5, col:32>
    |     `-VarDecl 0x7f9a2108a1e0 <col:5, line:2:21> line:5:9 c 'int' cinit
    |       `-BinaryOperator 0x7f9a2108a308 <col:13, line:2:21> 'int' '-'
    |         |-BinaryOperator 0x7f9a2108a2c0 <line:5:13, col:17> 'int' '+'
    |         | |-ImplicitCastExpr 0x7f9a2108a290 <col:13> 'int' <LValueToRValue>
    |         | | `-DeclRefExpr 0x7f9a2108a240 <col:13> 'int' lvalue ParmVar 0x7f9a21089f48 'a' 'int'
    |         | `-ImplicitCastExpr 0x7f9a2108a2a8 <col:17> 'int' <LValueToRValue>
    |         |   `-DeclRefExpr 0x7f9a2108a268 <col:17> 'int' lvalue ParmVar 0x7f9a21089fc0 'b' 'int'
    |         `-IntegerLiteral 0x7f9a2108a2e8 <line:2:21> 'int' 8
    `-<undeserialized declarations>
    

    為了方便查看,我們將 AST 以樹狀圖的形式表示

    image
    節(jié)點的分類:

    TranslationUnitDecl:根節(jié)點,表示一個源文件

    Decl:聲明

    Expr:表達式

    Literal:字面量惰帽,是特殊的 Expr

    Stmt:語句

  1. 拿到 AST 后 Clang 靜態(tài)分析器( Clang static analyzer )會對代碼進行靜態(tài)分析该酗。

    Clang static analyzer 的架構(gòu)包含了一個 Analyzer core 核心分析引擎和用于檢查具體代碼的 checkers 呜魄,所有的 checkers 都是基于 analyzer core 提供的基礎(chǔ)功能來實現(xiàn)具體的代碼檢查的耕赘。

    AST 生成后 Clang static analyzer 會使用 checkers 對代碼進行檢查操骡,比如是否使用了未聲明的變量等等册招。你也可以編寫新的 checkers 來添加自定義檢查是掰。這種方式能夠方便用戶擴展對代碼檢查規(guī)則或者對 bug 類型進行擴展键痛,但是這種架構(gòu)也有不足絮短,每執(zhí)行完一條語句后丁频,分析引擎會遍歷所有 checker 中的回調(diào)函數(shù)席里,所以 checker 越多奖磁,速度越慢改基。

    Clang 的靜態(tài)分析器不僅能夠?qū)⒊霈F(xiàn)問題的代碼位置暴露出來,還能夠提供多個修復(fù)代碼的方法署穗。

  2. 完成這些步驟后就可以開始 IR 中間代碼的生成了寥裂,CodeGen 會負責(zé)將 AST 自上向下遍歷逐步翻譯成 LLVM IR。

    clang -S -fobjc-arc -emit-llvm main.m -o main.ll
    

    生成的代碼如下(此處僅截取了 test 方法)

    ; Function Attrs: noinline nounwind optnone ssp uwtable
    define void @test(i32, i32) #0 {
    %3 = alloca i32, align 4
    %4 = alloca i32, align 4
    %5 = alloca i32, align 4
    store i32 %0, i32* %3, align 4
    store i32 %1, i32* %4, align 4
    %6 = load i32, i32* %3, align 4
    %7 = load i32, i32* %4, align 4
    %8 = add nsw i32 %6, %7
    %9 = sub nsw i32 %8, 8
    store i32 %9, i32* %5, align 4
    ret void
    }
    

    是不是看的頭大了案疲。其實 IR 也不是很難封恰,稍微了解一下 IR 的語法就能夠讀懂其中的邏輯了褐啡。

    LLVM IR 語法:

    ; 注釋

    @ 全局

    % 局部

    alloca 分配內(nèi)存空間

    i32 32bit诺舔,即4個字節(jié)

    align 內(nèi)存對齊

    Store 寫入內(nèi)存

    load 讀取內(nèi)存

    icmp 兩個整數(shù)值比較,返回布爾值

    br 選擇分支,根據(jù) cond 來轉(zhuǎn)向 label低飒,不根據(jù)條件跳轉(zhuǎn)的話類似 goto

    indirectbr 根據(jù)條件間接跳轉(zhuǎn)到一個 label许昨,而這個 label 一般是在一個數(shù)組里,所以跳轉(zhuǎn)目標(biāo)是可變的褥赊,由運行時決定的

    label 代碼標(biāo)簽

    如果有學(xué)習(xí)過機器碼的同學(xué)有沒有發(fā)現(xiàn)糕档,IR代碼其實已經(jīng)很像機器碼了。

  3. 這里 LLVM 會去做些優(yōu)化工作拌喉,在 Xcode 的編譯設(shè)置里也可以設(shè)置優(yōu)化級別 -01 / -03 / -0s 速那,還可以寫些自己的 Pass

    Pass 是 LLVM 優(yōu)化工作的一個節(jié)點尿背,一個節(jié)點做些事端仰,一起加起來就構(gòu)成了 LLVM 完整的優(yōu)化和轉(zhuǎn)化。

    我們可以通過在上面一段命令中加入 -O3 / -O2 優(yōu)化參數(shù)來控制優(yōu)化登記田藐。

    clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll
    

    下面通過一個小例子荔烧,來展示 LLVM 對 IR 所做的具體優(yōu)化。我們先寫一個方法汽久,包含一個循環(huán)語句鹤竭。

    int main() {
    int i = DEFINEEight;
    while (i < 10) {
    I++;
    printf("%d",i);
    }
    return 0;
    }
    

    使用 -O3 參數(shù)生成 IR

    ; Function Attrs: nounwind ssp uwtable
    define void @demo() local_unnamed_addr #0 {
    %1 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 1), !clang.arc.no_objc_arc_exceptions !9
    %2 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 2), !clang.arc.no_objc_arc_exceptions !9
    %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 3), !clang.arc.no_objc_arc_exceptions !9
    %4 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 4), !clang.arc.no_objc_arc_exceptions !9
    %5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 5), !clang.arc.no_objc_arc_exceptions !9
    %6 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 6), !clang.arc.no_objc_arc_exceptions !9
    %7 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 7), !clang.arc.no_objc_arc_exceptions !9
    %8 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 8), !clang.arc.no_objc_arc_exceptions !9
    %9 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 9), !clang.arc.no_objc_arc_exceptions !9
    %10 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 10), !clang.arc.no_objc_arc_exceptions !9
    ret void
    }
    

    此時我們將循環(huán)條件 (i < 10) 修改為 (i < 100)

    ; Function Attrs: nounwind ssp uwtable
    define void @demo() local_unnamed_addr #0 {
    br label %1
    
    ; <label>:1:                                      ; preds = %1, %0
    %2 = phi i32 [ 0, %0 ], [ %3, %1 ]
    %3 = add nuw nsw i32 %2, 1
    %4 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 %3), !clang.arc.no_objc_arc_exceptions !9
    %5 = icmp eq i32 %3, 100
    br i1 %5, label %6, label %1
    
    ; <label>:6:                                      ; preds = %1
    ret void
    }
    

    可以發(fā)現(xiàn)當(dāng)循環(huán)次數(shù)較低時,生成的 IR 會直接將所有循環(huán)的邏輯都寫出來回窘,將執(zhí)行效率最大化诺擅。而當(dāng)循環(huán)次數(shù)過大時,則會退而使用更為復(fù)雜的邏輯去實現(xiàn)啡直。
    除了上面的這種簡單優(yōu)化,LLVM 還提供了其他優(yōu)化 Pass:

    • 各種類苍碟,方法酒觅,成員變量等的結(jié)構(gòu)體的生成,并將其放到對應(yīng)的 Mach-O 的 section 中
    • Non-Fragile ABI 合成 OBJC_IVAR_$_ 偏移值常量
    • ObjCMessageExpr 翻譯成相應(yīng)版本的 objc_msgSend微峰,super 翻譯成 objc_msgSendSuper
    • strong舷丹,weakcopy蜓肆,atomic 合成@property 自動實現(xiàn) settergetter
    • @synthesize 的處理
    • 生成 block_layout 數(shù)據(jù)結(jié)構(gòu)
    • _block__weak
    • _block _invoke
    • ARC 處理颜凯,插入 objc_storeStrongobjc_storeWeak 等 ARC 代碼
    • ObjCAutoreleasePoolStmt 轉(zhuǎn) objc_autorealeasePoolPush / Pop,自動添加 [super dealloc]仗扬,給每個 ivar 的類合成 .cxx_destructor 方法自動釋放類的成員變量症概。
  4. 如果開啟了 Bitcode , 蘋果會做進一步的優(yōu)化

    clang -emit-llvm -c main.m -o main.bc
    
  5. 生成匯編

    clang -S -fobjc-arc main.m -o main.s
    
  6. 生成目標(biāo)文件

    clang -fmodules -c main.m -o main.o
    
  7. 生成可執(zhí)行文件

    clang main.o -o main
    
  8. 執(zhí)行

    ./main
    

總結(jié)

LLVM 編譯過程

  • 預(yù)處理
  • 詞法分析
  • 語法分析
  • 生成 AST
  • 靜態(tài)分析
  • 生成 LLVM IR
  • 編譯器優(yōu)化
  • Bitcode (可選)
  • 生成匯編
  • 生成目標(biāo)文件
  • 生成可執(zhí)行文件

LLVM 分工

LLVM 編譯器分為前端、優(yōu)化器早芭、后端彼城,他們各自分工,相互獨立,共同組成了一個模塊化和可重用的編譯器和工具鏈技術(shù)的集合募壕。

編譯器前端

在 iOS 編譯中调炬,Clang 就是整個編譯器的前端。它包含了詞法分析器舱馅、語法分析器缰泡、靜態(tài)分析器、IR 生成器等一系列組件代嗤。這些組件共同協(xié)作棘钞,為 LLVM 提供了預(yù)處理,語法分析资溃,語義分析武翎,靜態(tài)分析、錯誤處理溶锭、生成 IR 等各種各樣的功能宝恶。

image

編譯器優(yōu)化

IR 是編譯器前端的輸出,也是編譯器后端的輸入趴捅,在整個 LLVM 編譯器中擔(dān)任承上啟下的角色垫毙。可以說拱绑,LLVM 的核心功能都是圍繞著 IR 而構(gòu)建的综芥。LLVM 的優(yōu)化器通過各種各樣的 Pass 來直接優(yōu)化 IR,使得代碼優(yōu)化過程和語言猎拨、平臺無關(guān)膀藐,大大提升了開發(fā)效率。

編譯器后端

在 iOS 編譯中红省,編譯器后端其實就是 LLVM 自己提供的一套后端额各。它包含了 機器碼生成器、鏈接器等工具吧恃,會對 IR 進行機器無關(guān)的代碼優(yōu)化虾啦,生成機器語言。

image

編譯器后端生成的產(chǎn)物

LLVM 機器碼生成器會針對不同的架構(gòu)痕寓,生成不同的機器碼傲醉。

image

實際應(yīng)用

深入了解 LLVM 和 Clang ,以及他們提供的開發(fā)工具呻率,我們能夠?qū)崿F(xiàn)很多有意思的功能硬毕。比如通過 Libclang、libTooling 筷凤,我們可以實現(xiàn)語法樹分析昭殉、語言轉(zhuǎn)化(例如將 Objective-C 轉(zhuǎn)換為 Swift )苞七。我們還可以開發(fā)自己的 Clang 插件,對我們的代碼做個性化的檢查挪丢。我們還可以通過寫 Pass 實現(xiàn)自定義的代碼優(yōu)化蹂风、代碼混淆。我們甚至可以開發(fā)自己的新語言乾蓬,你需要做的僅僅是寫一個編譯器前端惠啄,將你的代碼轉(zhuǎn)換成 LLVM IR 。

除此之外任内,了解了編譯內(nèi)部的實現(xiàn)過程和細節(jié)撵渡,也同樣有助于我們在解決問題時找到新的思路。OCEval 是 iOS 的一個動態(tài)執(zhí)行熱修復(fù)的第三方開源庫死嗦,和 js 的 eval 函數(shù)類似趋距,這個庫可以將字符串形式的 Objective-C 代碼轉(zhuǎn)換成實際的運行代碼并執(zhí)行,其中非常重要的一個實現(xiàn)細節(jié)就是實現(xiàn)了一套簡單的詞法分析和語法分析越除,將字符串轉(zhuǎn)成了 AST 并最終獲得執(zhí)行所需要的方法名节腐、參數(shù)等。


引用

深入剖析 iOS 編譯 Clang LLVM By 戴銘

iOS編譯過程的原理和應(yīng)用 By ??面對疾風(fēng)吧

深入淺出 iOS 編譯 By ??面對疾風(fēng)吧

Swift的高級中間語言:SIL By sea_biscute

LLVM

Clang

Swift

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末摘盆,一起剝皮案震驚了整個濱河市翼雀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌孩擂,老刑警劉巖狼渊,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異类垦,居然都是意外死亡狈邑,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門蚤认,熙熙樓的掌柜王于貴愁眉苦臉地迎上來官地,“玉大人,你說我怎么就攤上這事烙懦。” “怎么了赤炒?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵氯析,是天一觀的道長。 經(jīng)常有香客問我莺褒,道長掩缓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任遵岩,我火速辦了婚禮你辣,結(jié)果婚禮上巡通,老公的妹妹穿的比我還像新娘。我一直安慰自己舍哄,他們只是感情好宴凉,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著表悬,像睡著了一般弥锄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蟆沫,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天籽暇,我揣著相機與錄音,去河邊找鬼饭庞。 笑死戒悠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的舟山。 我是一名探鬼主播绸狐,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼捏顺!你這毒婦竟也來了六孵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤幅骄,失蹤者是張志新(化名)和其女友劉穎劫窒,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拆座,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡主巍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了挪凑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孕索。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖躏碳,靈堂內(nèi)的尸體忽然破棺而出搞旭,到底是詐尸還是另有隱情,我是刑警寧澤菇绵,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布肄渗,位于F島的核電站,受9級特大地震影響咬最,放射性物質(zhì)發(fā)生泄漏翎嫡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一永乌、第九天 我趴在偏房一處隱蔽的房頂上張望惑申。 院中可真熱鬧具伍,春花似錦、人聲如沸圈驼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碗脊。三九已至啼肩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間衙伶,已是汗流浹背祈坠。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留矢劲,地道東北人赦拘。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像芬沉,于是被迫代替她去往敵國和親躺同。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容

  • http://www.starming.com/index.php?v=index&view=107 http:/...
    111浪子111閱讀 3,117評論 0 11
  • iOS app的編譯過程 在 iOS 開發(fā)的過程中丸逸,Xcode 為我們提供了非常完善的編譯能力蹋艺,正常情況下,我們只...
    帽子和五朵玫瑰閱讀 2,832評論 0 17
  • 在 iOS 開發(fā)的過程中黄刚,Xcode 為我們提供了非常完善的編譯能力捎谨,正常情況下,我們只需要 Command + ...
    CoderLF閱讀 12,907評論 0 17
  • 在說這篇文章之前憔维,首先我們帶入一個問題涛救,在Xcode中我們最常使用的一個組合鍵cmd+b按下之后都進行了哪一些工作...
    瞇大帥閱讀 11,616評論 1 57
  • LLVM 簡介 LLVM 全稱是 Low Level Virtual Machine,它是源自 the Unive...
    juniway閱讀 37,649評論 0 21