天天看點

iOS底層探索之LLVM(一)——初識LLVM

1. 寫在前面

現在出去面試,啟動優化是繞不開的,到底我們的

APP

該如何去進行優化呢 ?在優化之前我們必須要先了解

LLVM

,那什麼是

LLVM

呢?

iOS底層探索之LLVM(一)——初識LLVM

在介紹

LLVM

之前,先來認識一下

解釋型語言

編譯型語言

我們編寫的

源代碼

是偏向于我們人類直接的語言,我們非常輕松的就了解了,但是對于

計算機硬體

(CPU)而言,簡直就是個天書,計算機是無法直接運作的。計算機隻能識别某些特定的

二進制指令

,是以我們的代碼在程式真正運作之前必須将源代碼

轉換

成二進制指令。源代碼轉換成二進制指令,不同的程式設計語言有不同的規定。

解釋型語言

有的程式設計語言可以一邊執行一邊轉換,不會生成可執行檔案再去執行,這種程式設計語言稱為

解釋型語言

,使用的轉換工具稱為

解釋器

,比如

Python

JavaScript

PHP

等。

iOS底層探索之LLVM(一)——初識LLVM
下面就舉個例子,使用

vim

指令建立立一個

python

檔案,字尾為

.py

,寫入代碼

print("hello world!")

,通過

python

指令,解釋這段代碼,列印一下

hello world !

這句話。
iOS底層探索之LLVM(一)——初識LLVM
我看可以看到解釋型語言,它是邊解釋邊執行,不可脫離解釋器環境運作。

MAC

電腦自帶了

Python

環境,無需另外手動配置環境。

編譯型語言

有的程式設計語言要轉換成

二進制指令

,也就是生成一個可執行程式這種程式設計語言稱為

編譯型語言

,使用的轉換工具稱為

編譯器

,比如

C

語言、

C++

OC

等。

iOS底層探索之LLVM(一)——初識LLVM
編譯型語言也同樣舉個例子,建立立一個

C

檔案,寫入如下代碼:
#include<stdio.h>
int main (int argc,char *agrv[])
{
    printf("hello world\n");

	 return 0;
}

           

通過

clang hello.c

指令,進行編譯處理,會生成一個可執行檔案,如下圖中紅色的

a.out

檔案。

iOS底層探索之LLVM(一)——初識LLVM

這個可執行檔案,可以直接運作,通過

./a.out

即可運作,如圖中也可以正常輸出

hello world

這句話。

編譯型語言是先整體編譯,再執行,運作速度快,任意改動需重新編譯,可脫離編譯環境運作。

小結:

  • 解釋型語言:讀到相應代碼就直接執行。
  • 編譯型語言:先将代碼編譯成計算機可以識别的二進制檔案,才能執行。

擴充:

通過

open /usr/bin

指令可以檢視,電腦上安裝的一些系統軟體。

/usr

不是

user

的縮寫,其實

us

r是

Unix Software Resource

的縮寫, 也就是

Unix

作業系統軟體資源所放置的目錄,而不是使用者的資料;所有系統預設的軟體都會放置到

/us

r, 系統安裝完時,這個目錄會占用最多的硬碟容量。

iOS底層探索之LLVM(一)——初識LLVM
在該目錄下可以看到,有我們的

clang

編譯器,還有

Python

解釋器,如下圖所示:
iOS底層探索之LLVM(一)——初識LLVM

MacOS

系統 預設安裝的是

python2

的環境,輸入

python

,按下

enter

Enter鍵,可以檢視:

iOS底層探索之LLVM(一)——初識LLVM
警告:不推薦使用

Python 2.7

,為了與舊軟體相容

macOS

中才包含了此版本。

macOS

的未來版本将不包含

Python 2.7

相反,建議您從終端内過渡到使用

“python3”

如果你是

python

的開發者,那麼日常使用的是

python3

,可以在終端中輸入

python3

檢視是否支援:

iOS底層探索之LLVM(一)——初識LLVM

可以看到我的電腦是支援的,我這裡的版本是

Python 3.7.7

的版本,如果你的電腦沒有支援,可以去

python

官網下載下傳。

2. LLVM

LLVM簡介

LLVM

是構架編譯器(

compiler

)的架構系統,以

C++

編寫而成,用于優化以任意程式語言編寫的程式的

編譯時間

(

compile-time

)、

連結時間

(

link-time

)、

運作時間

(

run-time

)以及

空閑時間

(

idle-time

),對開發者保持開放,并相容已有腳本。

LLVM

計劃啟動于

2000

年,最初由美國

UIUC

大學的

ChrisLattner

博士主持開展。

2006

ChrisLattner

加盟

AppleInc

并緻力于

LLVM

Apple

開發體系中的應用。

Apple

也是

LLVM

計劃的主要資助者。目前

LLVM

已經被蘋果

IOS

開發工具、

Xilinx Vivado

Facebook

Google

等各大公司采用。

傳統編譯器設計

我們先來看看傳統編譯器設計是怎麼樣的,如下圖所示:

iOS底層探索之LLVM(一)——初識LLVM
  • 編譯器前端(Frontend)

編譯器前端的任務是

解析源代碼

。它會進行:

詞法分析

文法分析

語義分析

,檢查源代碼是否存在錯誤,然後建構

抽象文法樹

(Abstract Syntax Tree, AST),

LLVM

的前端還會生成

中間代碼

(intermediate representation,IR)。

  • 優化器(Optimizer)

優化器負責進行各種優化,改善代碼的運作時間,例如消除備援計算等。

  • 後端(Backend)/代碼生成器(CodeGenerator)

将代碼映射到目标指令集,生成機器語言,并且進行機器相關的代碼優化。

iOS的編譯器架構

ObjectiveC/C/C++

使用的編譯器前端是

Clang

Swift

Swift

,後端都是

LLVM

iOS底層探索之LLVM(一)——初識LLVM

LLVM的設計

當編譯器決定支援多種源語言或多種硬體架構時,

LLVM

最重要的地方就來了。

其他的編譯器如

GCC

是非常成功的一款編譯器,但由于它是作為整體應用程式設計的,是以它的用途受到了很大的限制。

LLVM

設計的最重要方面是,使用通用的代碼表示形式

(IR)

,它是用來在編譯器中表示

代碼

的形式。是以

LLVM

可以為任何程式設計語言獨立編寫前端,并且可以為任意硬體架構

獨立

編寫後端。

iOS底層探索之LLVM(一)——初識LLVM
  • Clang

對于我們的開發人員來說,看得見摸得着的,接觸最多的就是我們的

Clang

Clang

LLVM

項目中的一個子項目。它是基于

LLVM

架構的

輕量級編譯器

,誕生之初是為了替代

GCC

,提供更快的編譯速度。它是負責編譯

C

C++

Objecte- C

語言的編譯器,它屬于整個

LLVM

架構中的,編譯器前端。對于開發者來說,研究

Clang

可以給我們帶來很多好處。

3. 編譯流程

那麼我們寫一段代碼,來測試一下,看看編譯流程是什麼樣子的。

int main(int argc, const char * argv[]) {
	@autoreleasepool {

	}
	return 0;
}
           

編譯的各個階段

通過一下指令,可以列印源碼的編譯階段。

clang -ccc-print-phases main.m
iOS底層探索之LLVM(一)——初識LLVM
  • 0:

    輸入檔案

    :找到源檔案。
  • 1:

    預處理階段

    :這個過程處理包括宏的替換,頭檔案的導入。
  • 2:

    編譯階段

    :進行詞法分析、文法分析、檢測文法是否正确,最終生成

    IR

  • 3:

    後端

    :這裡

    LLVM

    會通過一個一個的

    Pass

    (可以了解為一個節點)去優化,每個

    Pass

    做一些事情,最終生成

    彙編代碼

  • 4:彙編代碼

    生成目标檔案

  • 5:

    連結

    :連結需要的動态庫和靜态庫,生成相應的

    鏡像

    可執行檔案。
  • 6:根據不同的系統架構,生成對應的可執行檔案。

上面已經知道了編譯的流程了,那麼我們一步一步去看看各個階段是什麼樣子的。

#import <stdio.h>
#define B 50
int main(int argc, const char * argv[]) {
	int a = 10;
	int c = 20;
	printf("%d",a + c + B);
	return 0;
}
           

預處理階段

執行如下指令

clang -E main.m >> main1.m
iOS底層探索之LLVM(一)——初識LLVM

執行完畢後,我們可以在

main1.m

的檔案中,可以看到頭檔案的導入和

宏的替換

詞法分析

編譯階段-詞法分析

預處理

完成後就會進行

詞法分析

,這裡會把代碼切成一個個

Token

,比如大小括号,等于号還有字元串等。

#import <stdio.h>
#define B 50
typedef int JP_INT;
int main(int argc, const char * argv[]) {
	JP_INT a = 10;
	JP_INT c = 20;
	printf("%d",a + c + B);
	return 0;
}
           
clang -fmodules-fsyntax-only -Xclang -dump-tokens main.m
iOS底層探索之LLVM(一)——初識LLVM

指令運作之後,進行了詞法分析,每一行的代碼都分開了,切成一個個

Token

文法分析

詞法分析完成之後就是

文法分析

,它的任務是驗證文法是否正确。在

詞法分析

的基礎上将

單詞序列組合成

各類

文法短語

,如“

程式

”,“

語句

”,“

表達式

”等等,然後将所有節點組成

抽象文法樹

(AbstractSyntaxTree,AST)。

文法分析

其目的就是對源程式進行分析判斷,在結構上是否正确。

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
iOS底層探索之LLVM(一)——初識LLVM
  • FunctionDecl

    函數方法聲明,範圍是第

    10

    行第

    1

    個字元開始 到第

    15

    行第

    1

    個字元結束。第

    10

    行第

    5

    個字元開始,名稱叫

    main

    ,傳回值是

    int

    類型,第一個參數的類型是

    int

    ,第二個參數的類型是

    const char **

    。這裡為什麼是

    const char **

    呢?因為數組的名稱就是一個指針,

    const char ** argv

    等于

    const char * argv[]

    iOS底層探索之LLVM(一)——初識LLVM
  • ParmVarDecl

    參數,目前行的第

    10

    個字元到第

    14

    個字元是

    int

    類型所占有,第

    14

    個字元是參數

    argc

  • CompoundStmt

    複合語句,目前行第

    41

    個字元到,第

    15

    行代碼的第

    1

    個字元,也就是

    {}

    包裹的範圍。
  • 這兩句代碼

    JP_INT a = 10; JP_INT c = 20;

    對應的是下面這個
    iOS底層探索之LLVM(一)——初識LLVM
  • CallExpr

    調用表達式, 代碼中的

    printf

    函數的列印文法分析如下圖
    iOS底層探索之LLVM(一)——初識LLVM
    包括

    printf

    函數的指針,告訴我們函數的類型和傳回值的類型;第一個參數"%d",第二個參數是一個

    +

    加運算的結果,是由

    a

    c

    相加之和,再與

    50

    進行相加得到。
  • ReturnStmt

    傳回
  • VarDecl

    變量聲明
  • StringLiteral

    字元串字面量
  • IntegerLiteral

    整型字面量
  • BinaryOperator

    二進制運算符

補充:如果導入的頭檔案找不到,可以指定SDK

clang isysroot/Applications/Xcode.app/Contents/Developer/Platforms/

iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己的 sdk路徑) -fmodules -fsyntax-only -Xclang -ast-dump main.m

中間代碼IR

完成以上步驟後就開始生成中間代碼

IR

(intermediate representation)了,代碼生成器(

Code Generation

)會将

文法樹

自頂向下周遊逐漸翻譯成

LLVM IR

#import <stdio.h>
//#define B 50
//typedef int JP_INT;
int JPTest(int a,int b) {
	return  a + b + 1;
}
int main(int argc, const char * argv[]) {
	int c = JPTest(1, 2);
	printf("%d",c);
	return 0;
}

           

通過下面指令可以生成

.ll

的文本檔案,檢視

IR

代碼,如下。

clang -S -fobjc-arc -emit-llvm main.m
iOS底層探索之LLVM(一)——初識LLVM

從圖中可以看到,生成了一個

.ll

的檔案,使用

VS Code

打開如下:

iOS底層探索之LLVM(一)——初識LLVM

JPTest

方法的生成的

IR

代碼解讀如下:

iOS底層探索之LLVM(一)——初識LLVM

ObjectiveC

代碼在這一步會進行

runtime

的橋接:

property

合成,

ARC

處理等。

IR

的基本文法

@: 全局辨別

% : 局部辨別

alloca: 開辟空間

align: 記憶體對齊

i32: 32個bit,4個位元組

store: 寫入記憶體

load: 讀取資料

call: 調用函數

ret: 傳回

以上生成的代碼是沒有經過優化的,我們可以手動的開啟編譯器的優化,在

XCode

裡面可以進行設定的。

IR的優化

LLVM

的優化級别分别是-O0-O1-O2-O3-Os(第一個是大寫英文字母

O

)

iOS底層探索之LLVM(一)——初識LLVM

使用終端的指令,也是可以優化的,那麼現在去優化一下,剛剛的代碼。

clang -Os -S -fobjc-arc -emit-llvm main.m -o main1.ll
iOS底層探索之LLVM(一)——初識LLVM

從上面的對比圖,可以看出優化之後,

JPTest

mian

代碼都少了很多,在

mian

函數裡面并沒有看到調用

JPTest

函數,而是

printf

直接列印了

c

的結果

4

,這就是優化的強大之處,如下:

iOS底層探索之LLVM(一)——初識LLVM

優化之後,直接就算出來結果了,這優化還是很給力的哈!優化等級也不是越高就越好。在

XCode

裡面的優化選項裡面

release

環境下預設的優化就是最好的了,蘋果肯定是給你最好的優化啊。

iOS底層探索之LLVM(一)——初識LLVM
  • 小結:

編譯流程:首先是

預處理

,對輸入代碼的宏進行展開;然後是

詞法分析

,會分成一個一個的

token

;再是

文法分析

,會生成

AST文法樹

;再就會生成

IR

代碼,交給優化器去處理優化代碼。

  • bitCode

這是

xcode7

以後開啟

bitcode

蘋果會做進一步的優化,生成

bc

的中間代碼。我們通過優化後的

IR

代碼生成

bc

代碼,這也是一個中間代碼,目的是會根據

CPU

的不同架構生成不同大小的包(App Store 商店下載下傳)。

clang -emit-llvm -c main.ll -o main.bc

生成彙編代碼

  • 生成彙編代碼

我們通過最終的

.bc

或者

.ll

代碼生成彙編代碼

clang -S -fobjc-arc main.bc -o main.s

clang -S -fobjc-arc main.ll -o main.s

iOS底層探索之LLVM(一)——初識LLVM
  • 生成的彙編比較
iOS底層探索之LLVM(一)——初識LLVM

圖中是三種不同字尾生成的彙編代碼

  • IR

    直接生成的彙編是

    55

    行,計算優化了
  • IR

    生成的

    bc

    在生成彙編,在

    IR

    的基礎上沒有進一步的優化了,還是

    55

  • 原始的

    main

    代碼直接生成的彙編就是

    62

    行了

生成彙編代碼的時候也是可以再次進行優化的,那麼我們用上面生成的

.bc

試一下,開啟優化最大,看看生成的彙編是有多少行呢?

clang -Os -S -fobjc-arc main.bc -o main3.s
iOS底層探索之LLVM(一)——初識LLVM
我們把優化等級調到最高,生成的彙編代碼就

47

行了,比上面的

55

行少了

8

行,也就是說生成的

IR

或者

bc

的時候,優化并沒有停止,每一個節點上面都有可能再次優化。

生成目标檔案(彙編器)

目标檔案的生成,是

彙編器

以彙編代碼作為

輸入

,将彙編代碼轉換為

機器代碼

,最後輸出

目标檔案

(object-file),這個階段就是屬于編譯器後端的工作了。

clang -fmodules -c main.s -o main.o
iOS底層探索之LLVM(一)——初識LLVM
通過

nm

指令,檢視下

main.o

中的符号
xcrun nm -nm main.o
iOS底層探索之LLVM(一)——初識LLVM
  • _printf

    是一個是

    undefined external

    的。
  • undefined

    表示在目前檔案暫時找不到符号

    _printf

  • external

    表示這個符号是外部可以通路的。

生成可執行檔案(連結)

連接配接器把編譯産生的

.o

檔案和(dylib .a)檔案,生成一個

mach-o

檔案(可執行檔案)。

clang main.o -o main
iOS底層探索之LLVM(一)——初識LLVM
  • 檢視連結之後的符号
iOS底層探索之LLVM(一)——初識LLVM
  • 現在列印的資訊就多了,

    _JPTest

    _main

    也還在,偏移位址也有了,也就是說在執行檔案中的位置就确定了。
  • 現在的外部函數除了

    _printf

    還有

    dyld_stub_binder

    ,這是為什麼呢?
  • dyld_stub_binder

    是在

    dyld

    裡面,當我們的執行檔案

    mach-o

    進入的記憶體之後,外部的符号就會立刻馬上和

    dyld_stub_binder

    進行綁定,這個過程是

    dyld

    強制綁定的。
  • 連結和綁定是兩個概念:連結是我要知道你外部的符号在哪個動态庫裡面,就是做個标記,我要知道去哪個動态庫裡面找到你。
  • 綁定是在執行的時候,把動态庫

    libSystem

    裡面的和你這個外部調用的

    _printf

    進行綁定,綁定是在執行期,連結是在編譯期。

以上就是

LLVM

大緻的工作流程,接下來将介紹如何寫一個自己的

Clang

插件。

4. 寫在後面

關注我,更多内容持續輸出
  • CSDN
  • 掘金
  • 簡書
🌹 喜歡就點個贊吧👍🌹
🌹 覺得有收獲的,可以來一波 收藏+關注,以免你下次找不到我😁🌹
🌹歡迎大家留言交流,批評指正,

轉發

請注明出處,謝謝合作!🌹