iOS 底層探索:LLVM入門

iOS 底層探索: 學習大綱 OC篇

前言

  • 今天來學習一下牛逼的架構編譯器LLVM

學習大綱

  1. 簡單了解編譯器
  2. LLVM概述
  3. LLVM案例體驗
  4. LLVM源碼 & 編譯流程

準備

一 、簡單了解編譯器

百度百科

  1. 解釋型語言編譯型語言
    • 編譯型語言:編譯后輸出的是指令(0、1組合),cpu可直接執行指令
    • 解釋性語言:生成的是數據,不是0、1組合,機器也能直接識別

python解釋型語言,一邊翻譯一邊執行。和js一樣,機器可直接執行
C語言編譯型語言,不能直接執行,需要編譯器將其轉換成機器識別語言

  1. 編譯器的作用,就是將高級語言轉化為機器能夠識別的語言(可執行文件)。

  2. 匯編指令

  • 早期科學家,使用0、1編碼。 比如 00001111 對應 call, 00000111 對應bl。有了對應關系后。 再手敲0和1就有點難受了。于是寫個中間解釋器,我們只用輸入call、bl這樣的標記指令,經過解釋器,變成0和1的組合,再交給機器去執行。 這就是匯編的由來。
  • 而基于匯編往上,再映射封裝相關對應關系。就跨時代性的c語言,再往上層封裝,就出現了高級語言oc、swift等語言。所以匯編執行快,因為它是直接轉換為機器語言的。
  • 匯編的指令集,是針對同一操作系統而言,它不支持跨平臺。機器指令是cpu的在識別。早期的計算機廠家非常多,雖然都用0和1的組合,但相同組合背后卻是相應不同的指令。所以匯編無法跨平臺不同操作系統下,匯編指令不同的。
  1. 案例創建體驗
  • Python 案例:創建python文件夾,新建helloDemo.py文件,
    內容:
print("hello")
  • 可以看出 Python文件內容 可直接執行

  • C 案例:vim創建helloDemo.c文件:
    內容:

 #include <stdio.h>
 int main(int a, char * argv[]) {
         printf("hello \n");
         return 0;
 }
  • clang helloDemo.c編譯,生成a.out文件。file a.out查看文件:
  • 發現.out文件是:64位的Mach-O可執行文件,當前clang出來的是x86_64架構, mac電腦可讀。 所以可以./a.out直接執行:

  • 可以看出 C 文件內容 需要編譯

二 、 LLVM概述

  • LLVM是架構編譯器(compiler)的框架系統,以c++編寫而成,用于優化以任意程序語言編寫的程序的編譯時間(compile-time)、鏈接時間(link-time)、運行時間(run-time)以及空閑時間(idle-time),對開發者保持開放,并兼任已有腳本。
  • 2006年Chris Lattner加盟Apple Inc.并致力于LLVM在Apple開發體系中的應用。Apple也是LLVM計劃的主要資助者。目前LLVM已經被蘋果iOS開發工具、Xilinx Vivado、Facebook、Google等各大公司采用。

1. 傳統編譯器的設計

傳統編譯器的設計

  • 編譯器前端(Frontend):

編譯器的前端任務是解析源代碼。 會進行詞法分析、語法分析、語義分析。檢查源代碼是否存在錯誤,然后構建抽象語法樹(Abstract Syntax Tree AST),LLVM前端還會生成中間代碼(intermediate representation, IR)

  • 優化器(Optimizer)

優化器負責各種優化。改善代碼的運行時間,如消除冗余計算等

  • 后端(Backkend)/ 代碼生成器(CodeGenerator)

代碼映射到目標指令集生成機器語言,并進行機器相關的代碼優化(目標指不同操作系統)

2. iOS的編譯器架構
Objective C / C / C++ 使用的編譯器前端是Clang,Swift是swift,后端都是LLVM。

3. LLVM的設計

  • 傳統編譯器(如CGG )的前端和后端沒有完全分離,耦合在了一起,因而如果要支持一門新的語言或硬件平臺,需要做大量的工作。

  • LLVM最重要的地方:支持多種語言多種硬件架構。使用通用代碼表示形式:IR(用來在編譯器中表示代碼的形式)

  • LLVM可以為任何編程語言獨立編寫前端,也可以為任何硬件架構獨立編寫后端.

  • 所以LLVM不是一個簡單的編譯器,而是架構編譯器,可以兼容所有前端和后端。

  • LLVM同時支持 AOT 預先編譯和 JIT即時編譯

  • 不同的前端后端使用統一的中間代碼LLVM Intermediate Representation (LLVM IR)
      1. LLVM IR格式以 .ll結尾、以 .bc 的二進制格式結尾、內存格式
      1. Bitcode(Xcode 7之后)就是以.bc結尾的中間代碼,是LLVM-IR在磁盤上的一種二進制表示形式。例如:clang -c -emit-llvm xxxx.m 生成 xxxx. bc
      1. 如果要轉換成文本格式查看,例如:llvm-dis xxxx.bc -o xxxx.ll
      1. 蘋果單獨對 Bitcode 進行了額外的優化.
      • i) 應用上傳到 AppStore時,Xcode會將程序對應的 Bitcode一起上傳;
      • ii) AppStore會將 Bitcode重新編譯為可執行程序,供用戶下載;
      • iii) Bitcode被Xcode打包成 xar文檔,嵌入的 MachO中。
  • 如果需要支持一種新的編程語言、硬件設備,那么只需要實現一個新的前后端

4. Clang簡介

ClangLLVM項目的一個子項目。基于LLVM架構的輕量級編輯器,誕生之初就是為了替代GCC,提供更快的編譯速度。 他是負責編譯C、C++、Objecte-C語言的編譯器,它屬于整個LLVM架構中的編譯器前端

對于開發者而言,研究Clang可以給我們帶來很多好處。

三 、 LLVM案例體驗

  • 新建一個Mac OS命令行工程:

  • 沒有改動代碼

3.1 編譯流程

  • cd到main.m的文件夾。使用下面命令查看main.m的編譯步驟:
clang -ccc-print-phases main.m

編譯流程分為以下7步

  • 0: input, "main.m", objective-c
    輸入文件:找到源文件
  • 1: preprocessor, {0}, objective-c-cpp-output
    預處理:宏的展開,頭文件的導入
  • 2: compiler, {1}, ir
    編譯:詞法、語法、語義分析,最終生成IR
  • 3: backend, {2}, assembler ()
    匯編: LLVM通過一個個的Pass去優化,每個Pass做一些事,最后生成匯編代碼
  • 4: assembler, {3}, object
    目標文件
  • 5: linker, {4}, image
    鏈接: 鏈接需要的動態庫和靜態庫,生成可執行文件
  • 6: bind-arch, "x86_64", {5}, image
    架構可執行文件:通過不同架構,生成對應的可執行文件

optimizer優化沒有作為一個獨立階段,在編譯階段內部完成

3.2 預處理階段

  • main.m中準備測試代碼:
#import <stdio.h>
#define C 30

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        int b = 20;
        printf("%d", a + b + C);
    }
    return 0;
}

  • clang預編譯輸出main2.m文件:
clang -E main.m >> main2.m

  • 打開main2.m,有575行。其中大部分是stdio庫的代碼:

  • 我們發現測試代碼中的宏C,在預編譯階段完成了替換,變成了30

預編譯階段: 1. 導入頭文件 2.替換宏

  • 修改測試代碼,給int類型取個別名HT_INT_64,再次預編譯處理
#define C 30

typedef int HT_INT_64;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HT_INT_64 a = 10;
        HT_INT_64 b = 20;
        printf("%d", a + b + C);
    }
    return 0;
}

  • 發現typedef不會被替換

使用安全拓展:

  1. 使用define重要方法名稱進行替換。比如#define Pay XXXTest這樣開發者使用宏Pay開發舒服,但是被hank時,實際代碼是XXXTest,不容易被察覺。
    #define真實內容不應該寫成亂碼,最好弄成系統類似名稱或其他不經意名稱。這樣才容易忽視安全級別更高 ??)
  2. typedef有一種掩人耳目的效果。define只影響預處理期。

3.3 編譯階段

3.3.1 詞法分析

  • 編譯main.m文件:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
  • 詞法分析,就是根據空格括號這些將代碼拆分成一個個Token。標注了位置第幾行第幾個字符開始的。
Last login: Mon Nov 16 16:43:13 on ttys000
ios@HJ ~ % cd /Users/ios/Desktop/學習資料/hu/TestDemo/TestDemo 
ios@HJ TestDemo % clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

annot_module_include '#import <Foundation/Foundation.h>
#define C 30

int main(int argc, const char * argv[]) {
    @autoreleasepoo'        Loc=<main.m:8:1>
int 'int'    [StartOfLine]  Loc=<main.m:11:1>
identifier 'main'    [LeadingSpace] Loc=<main.m:11:5>
l_paren '('     Loc=<main.m:11:9>
int 'int'       Loc=<main.m:11:10>
identifier 'argc'    [LeadingSpace] Loc=<main.m:11:14>
comma ','       Loc=<main.m:11:18>
const 'const'    [LeadingSpace] Loc=<main.m:11:20>
char 'char'  [LeadingSpace] Loc=<main.m:11:26>
star '*'     [LeadingSpace] Loc=<main.m:11:31>
identifier 'argv'    [LeadingSpace] Loc=<main.m:11:33>
l_square '['        Loc=<main.m:11:37>
r_square ']'        Loc=<main.m:11:38>
r_paren ')'     Loc=<main.m:11:39>
l_brace '{'  [LeadingSpace] Loc=<main.m:11:41>
at '@'   [StartOfLine] [LeadingSpace]   Loc=<main.m:12:5>
identifier 'autoreleasepool'        Loc=<main.m:12:6>
l_brace '{'  [LeadingSpace] Loc=<main.m:12:22>
int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:13:9>
identifier 'a'   [LeadingSpace] Loc=<main.m:13:13>
equal '='    [LeadingSpace] Loc=<main.m:13:15>
numeric_constant '10'    [LeadingSpace] Loc=<main.m:13:17>
semi ';'        Loc=<main.m:13:19>
int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:14:9>
identifier 'b'   [LeadingSpace] Loc=<main.m:14:13>
equal '='    [LeadingSpace] Loc=<main.m:14:15>
numeric_constant '20'    [LeadingSpace] Loc=<main.m:14:17>
semi ';'        Loc=<main.m:14:19>
identifier 'printf'  [StartOfLine] [LeadingSpace]   Loc=<main.m:15:9>
l_paren '('     Loc=<main.m:15:15>
string_literal '"%d"'       Loc=<main.m:15:16>
comma ','       Loc=<main.m:15:20>
identifier 'a'   [LeadingSpace] Loc=<main.m:15:22>
plus '+'     [LeadingSpace] Loc=<main.m:15:24>
identifier 'b'   [LeadingSpace] Loc=<main.m:15:26>
plus '+'     [LeadingSpace] Loc=<main.m:15:28>
numeric_constant '30'    [LeadingSpace] Loc=<main.m:15:30 <Spelling=main.m:9:11>>
r_paren ')'     Loc=<main.m:15:31>
semi ';'        Loc=<main.m:15:32>
r_brace '}'  [StartOfLine] [LeadingSpace]   Loc=<main.m:16:5>
return 'return'  [StartOfLine] [LeadingSpace]   Loc=<main.m:17:5>
numeric_constant '0'     [LeadingSpace] Loc=<main.m:17:12>
semi ';'        Loc=<main.m:17:13>
r_brace '}'  [StartOfLine]  Loc=<main.m:18:1>
eof ''      Loc=<main.m:18:2>
ios@HJ TestDemo % 

3.3.2 語法分析
  • 語法分析是驗證語法是否正確
    在詞法分析的基礎上,將單詞序列組合成各類語法短語,如“程序”,“語句”,“表達式”等,然后將所有節點組成抽象語法樹(Abstract Syntax Tree,AST)。 語法分析程序判斷源程序結構上是否正確
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
  • 作用域類型運算方式十分清晰。( 語法樹一次只能處理一次計算。兩次運算,就得多分一層級。)
  • 語法分析,就是在生成語法樹完成檢測的。
  • 頭文件找不到時,可以指定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

3.4 生成中間代碼IR(Intermediate representation)

3.4.1 生成中間代碼
  • 完成以上步驟后,就開始生成中間代碼IR,代碼生成器(Code Generation)會將語法樹自頂向下遍歷逐步翻譯成LLVMIR

  • 便于理解,我們簡化代碼:

#import <stdio.h>

int test(int a, int b) {
    return a + b + 3;
}

int main(int argc, const char * argv[]) {
    int a = test(1,2);
    printf("%d",a);
    return 0;
}

通過下面命令生成.ll文本文件,查看IR代碼:

clang -S -fobjc-arc -emit-llvm main.m
  • IR基本語法
    @ 全局標識
    % 局部標識
    alloca 開辟空間
    align 內存對齊
    i32 32個bit,4個字節
    store 寫入內存
    load 讀取數據
    call 調用數據
    ret 返回
  • 使用VSCodeSublime Text可以打開代碼:(可以指定文件語言,讓代碼高亮色
image
  • Q:圖中為何多創建那么多局部變量?(如test函數內的a5、a6)
  • 因為在上一階段(編譯階段),我們將代碼編譯成了語法樹結構。而此時,我們只是沿語法樹進行讀取。 語法樹每一個層級,都需要一個臨時變量承接。再返回上一層級處理
  • 所以會產生那么多局部變量
3.4.2 IR優化
  • 我們可以在XcodeBuild Settings中搜索Optimization,可以看到優化級別。
    (Debug模式默認None [O0]無優化,Release模式默認Fastest,Smallest [Os]最快最小)
image
  • LLVM的優化級別分為 -O0-O1-O2-O3-Os(第一個字母是Optimization的O)。

  • 分別選擇O0Os兩個優化等級進行中間代碼的生成比較:

clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll      //  O0  無優化
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll  //  Os 最快最小
3.4.3 bitCode再優化

Xcode7之后開啟bitCode蘋果會再進一步優化,生成.bc中間代碼

優化體現:上傳APPstore的包,針對不同型號手機做了區分,不同型號手機下載時,大小不同

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

3.5 生成匯編代碼

  • 完成中間代碼的生成后,可以將代碼轉變匯編代碼了。

  • 此刻我們有4種不同程度的代碼(源代碼->無優化IR代碼->Os優化IR代碼 -> bitcode優化代碼):

    image
  • 分別對4種程度的代碼輸出匯編文件:

clang -S -fobjc-arc main.m -o main.s
clang -S -fobjc-arc main.ll -o mainO0.s
clang -S -fobjc-arc mainOs.ll -o mainOs.s
clang -S -fobjc-arc main.bc -o mainbc.s

生成匯編代碼時,只有選擇優化等級,才能減少匯編代碼量

3.6 生成目標文件(機器代碼)

  • 生成匯編文件后,匯編器匯編代碼作為輸入,將匯編代碼轉換機器代碼輸出目標文件(object file)
clang -fmodules -c main.s -o main.o
  • file對比一下main.s匯編代碼和main.o機器代碼:
file main3.m
file main.o 
image
  • xcrun執行nm命令查看main.o文件中的符號:
xcrun nm -nm main.o
image
  • 此時只是把當前文件編譯為了機器碼外部符號(如printf)無法識別。

undefined: 表示當前文件暫時找不到符號
external:表示這個符號外部可以訪問的。(實現不在我這,在外部某個地方

所以當前雖轉換成了機器代碼。但是只是目標文件,并不能直接執行,需要所有資源鏈接起來,才可以執行

3.7 生成可執行文件(鏈接)

  • 通過鏈接器把編譯產生的.o文件和.dylib.a文件鏈接關聯起來,生成真正的mach-o可執行文件
clang main.o -o main // 將目標文件轉成可執行文件
file main            // 查看文件
xcrun nm -nm main    // 查看main的符號

image
  • 對比main.o目標文件,此時生成的main文件:
  1. object文件變成了executable可執行文件
  2. 雖然都有undefined,但是可執行文件中指定了該符號來源庫。機器在運行時,會從相應的庫中取讀取符號(printf)

總結:

源代碼可執行文件整個流程

image

四. LLVM源碼 & 編譯流程

【注意】

  1. LLVM源碼2.29G編譯后文件30G,請確保電腦硬盤空間足夠
  2. 編譯時,電腦溫度會飆升90多度,請用空調伺候著,可能黑屏
  3. 編譯時間長達1個多小時,請合理安排時間。

如果以上3點,你確定能接受,那我們就開始吧。

4.1 LLVM下載

4.2 LLVM編譯

  • 最新的LLVM只支持cmake編譯,需要使用Homebrew安裝cmake:
4.2.1 安裝cmake
  • 查看brew列表,檢查是否安裝過cmake,如果有,就跳過此步驟
brew list
  • 如果沒有,就使用brew安裝:
brew install cmake

如果報權限錯誤,可sudo chown -Rwhoami:admin /usr/local/share放開權限

image
4.2.2 編譯llvm
  • cmake編譯成Xcode項目
cd llvm-project
mkdir build
cd build
cmake -G Xcode ../llvm   
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm 
  • 成功之后,可以看到生成的Xcode文件:

    image
  • 打開LLVM.xcodeproj,選擇自動創建Schemes

    image
  • 自動創建完成后,選擇ALL_BUILD進行編譯(耗時0.5-1小時,CPU滿負荷運轉)

    image
  • 編譯完成。接下來我們開始創建插件。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容