OC底層原理三十一:LLVM入門

OC底層原理 學習大綱

本節,我們給大家介紹一個偉大的架構編譯器LLVM

  1. 什么是編譯器
  2. LLVM概述
  3. LLVM案例體驗

1 什么是編譯器?

1.1 Python案例

  • 創建python文件夾,新建helloDemo.py文件,內容如下:
print("hello")
  • 調用python helloDemo.py執行文件,打印出python
    image.png

1.2 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查看文件:
    image.png

發現.out文件是:64位的Mach-O可執行文件,當前clang出來的是x86_64架構, mac電腦可讀。 所以可以./a.out直接執行:

image.png

Q:解釋型語言與編譯型語言

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

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

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

Q:匯編有指令嗎?

  • 早期科學家,使用0、1編碼。 比如 00001111 對應 call00000111 對應bl。有了對應關系后。 再手敲0和1就有點難受了。于是寫個中間解釋器,我們只用輸入callbl這樣的標記指令,經過解釋器,變成0和1的組合,再交給機器去執行。 這就是匯編的由來

  • 而基于匯編往上,再映射封裝相關對應關系。就跨時代性c語言,再往上層封裝,就出現了高級語言ocswift等語言。所以匯編執行快,因為它是直接轉換機器語言的。

  • 匯編指令集,是針對同一操作系統而言,它支持跨平臺機器指令cpu的在識別。早期的計算機廠家非常,雖然都用01組合,但相同組合背后卻是相應不同指令。所以匯編無法跨平臺不同操作系統下,匯編指令不同的。

2. LLVM概述

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

2.1 傳統編譯器的設計

傳統編譯器的設計
- 編譯器前端(Frontend):

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

- 優化器(Optimizer)

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

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

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

iOS的編譯器架構:

Objective C / C / C++ 使用的編譯器前端ClangSwiftswift,后端都是LLVM

image.png

2.2 LLVM的設計

  • GCC是一個非常成功編譯器,但由于它作為整體應用程序設計的,用途受到了限制

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

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

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

LLVM的設計

2.3 Clang

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

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

3. LLVM案例體驗

  • 新建一個Mac OS命令行工程:
    image.png
  • 沒有改動代碼


    image.png

3.1 編譯流程

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

image.png

編譯流程分為以下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庫的代碼:

    image.png

  • 我們發現測試代碼中的宏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不會被替換
    image.png

安全拓展:

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

3.3 編譯階段

3.3.1 詞法分析
  • 編譯main.m文件:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

-詞法分析,就是根據空格括號這些將代碼拆分成一個個Token。標注了位置第幾行第幾個字符開始的。

image.png

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

    image.png

  • 語法分析,就是在生成語法樹完成檢測的。

  • 頭文件找不到時,可以指定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.png
  • Q:圖中為何多創建那么多局部變量?(如test函數內的a5、a6)
  • 因為在上一階段(編譯階段),我們將代碼編譯成了語法樹結構。而此時,我們只是沿語法樹進行讀取。 語法樹每一個層級,都需要一個臨時變量承接。再返回上一層級處理
  • 所以會產生那么多局部變量
3.4.2 IR優化
  • 我們可以在XcodeBuild Settings中搜索Optimization,可以看到優化級別。
    (Debug模式默認None [O0]無優化,Release模式默認Fastest,Smallest [Os]最快最小)
image.png
  • 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 最快最小
image.png

image.png
  • 優化后的代碼,舒服多了。之前那些冗余臨時局部變量,也都被優化代碼量減少很多。
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.png

  • 分別對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
image.png

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

【拓展】在生成中間代碼前后都可以進行優化

  • [嘗試一] 將main.m直接選擇Os級別優化生成.s匯編文件
clang -Os -S -fobjc-arc main.m -o mainOs.s
  • [嘗試二] 將main.m生成無優化main.s,再main.s選擇Os級別優化生成.s匯編文件
clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll
clang -Os -S -fobjc-arc mainO0.ll -o mainOoOs.s
  • [嘗試三] 將main.m選擇Os級別優化生成main.s,再main.s選擇無優化級別生成.s匯編文件
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll
clang -S -fobjc-arc mainOs.ll -o mainOsOo.s
  • [嘗試四] 將main.m選擇Os級別優化生成main.s,再main.s選擇Os級別優化生成.s匯編文件
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll
clang -Os -S -fobjc-arc mainOs.ll -o mainOsOs.s
  • 內容比較:
image.png

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

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

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

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

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

  • 通過鏈接器把編譯產生的.o文件和.dylib.a文件鏈接關聯起來,生成真正的mach-o可執行文件
clang main.o -o main // 將目標文件轉成可執行文件
file main            // 查看文件
xcrun nm -nm main    // 查看main的符號
image.png
  • 對比main.o目標文件,此時生成的main文件:
  1. object文件變成了executable可執行文件
  2. 雖然都有undefined,但是可執行文件中指定了該符號來源庫。機器在運行時,會從相應的庫中取讀取符號(printf)

至此,我們已完整分析:源代碼可執行文件整個流程

image.png

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

推薦閱讀更多精彩內容

  • 什么是LLVM LLVM項目是模塊化、可重用的編譯器以及工具鏈技術的集合 The LLVM Project is ...
    那位小姐閱讀 2,667評論 0 10
  • OC底層原理 學習大綱[http://www.lxweimin.com/p/9e19354c0266] 對象的本質...
    markhetao閱讀 1,210評論 11 1
  • 解釋性語言和編譯性語言的區別?解釋性語言可以通過解釋器直接執行相應的代碼,比如python語言;而編譯性語言要經過...
    半邊楓葉閱讀 870評論 0 1
  • 目錄 傳統編譯器設計 輸入源代碼(Obj-C, Swift, ...) → 編譯器處理 → 輸出機器碼(01010...
    小瞎_MarkDash閱讀 1,269評論 0 2
  • 什么是LLVM LLVM項目是模塊化、可重用的編譯器以及工具鏈技術的集合。 美國計算機協會 (ACM) 將其201...
    Coder_LRT閱讀 2,391評論 0 1