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

OC底層原理 學(xué)習(xí)大綱

本節(jié),我們給大家介紹一個(gè)偉大的架構(gòu)編譯器LLVM

  1. 什么是編譯器
  2. LLVM概述
  3. LLVM案例體驗(yàn)

1 什么是編譯器?

1.1 Python案例

  • 創(chuàng)建python文件夾,新建helloDemo.py文件,內(nèi)容如下:
print("hello")
  • 調(diào)用python helloDemo.py執(zhí)行文件,打印出python
    image.png

1.2 C 案例

  • vim創(chuàng)建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

發(fā)現(xiàn).out文件是:64位的Mach-O可執(zhí)行文件,當(dāng)前clang出來(lái)的是x86_64架構(gòu), mac電腦可讀。 所以可以./a.out直接執(zhí)行:

image.png

Q:解釋型語(yǔ)言與編譯型語(yǔ)言

  • python解釋型語(yǔ)言,一邊翻譯一邊執(zhí)行。和js一樣,機(jī)器可直接執(zhí)行。
  • C語(yǔ)言是編譯型語(yǔ)言,不能直接執(zhí)行,需要編譯器將其轉(zhuǎn)換機(jī)器識(shí)別語(yǔ)言。

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

編譯器的作用,就是將高級(jí)語(yǔ)言轉(zhuǎn)化為機(jī)器能夠識(shí)別語(yǔ)言(可執(zhí)行文件)。

Q:匯編有指令嗎?

  • 早期科學(xué)家,使用0、1編碼。 比如 00001111 對(duì)應(yīng) call, 00000111 對(duì)應(yīng)bl。有了對(duì)應(yīng)關(guān)系后。 再手敲0和1就有點(diǎn)難受了。于是寫個(gè)中間解釋器,我們只用輸入callbl這樣的標(biāo)記指令,經(jīng)過(guò)解釋器,變成0和1的組合,再交給機(jī)器去執(zhí)行。 這就是匯編的由來(lái)。

  • 而基于匯編往上,再映射封裝相關(guān)對(duì)應(yīng)關(guān)系。就跨時(shí)代性c語(yǔ)言,再往上層封裝,就出現(xiàn)了高級(jí)語(yǔ)言oc、swift等語(yǔ)言。所以匯編執(zhí)行快,因?yàn)樗?code>直接轉(zhuǎn)換為機(jī)器語(yǔ)言的。

  • 匯編指令集,是針對(duì)同一操作系統(tǒng)而言,它支持跨平臺(tái)機(jī)器指令cpu的在識(shí)別。早期的計(jì)算機(jī)廠家非常,雖然都用01組合,但相同組合背后卻是相應(yīng)不同指令。所以匯編無(wú)法跨平臺(tái),不同操作系統(tǒng)下,匯編指令不同的。

2. LLVM概述

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

2.1 傳統(tǒng)編譯器的設(shè)計(jì)

傳統(tǒng)編譯器的設(shè)計(jì)
- 編譯器前端(Frontend):

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

- 優(yōu)化器(Optimizer)

優(yōu)化器負(fù)責(zé)各種優(yōu)化改善代碼的運(yùn)行時(shí)間,如消除冗余計(jì)算

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

將代碼映射目標(biāo)指令集,生成機(jī)器語(yǔ)言,并進(jìn)行機(jī)器相關(guān)代碼優(yōu)化 (目標(biāo)指不同操作系統(tǒng)

iOS的編譯器架構(gòu):

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

image.png

2.2 LLVM的設(shè)計(jì)

  • GCC是一個(gè)非常成功編譯器,但由于它作為整體應(yīng)用程序設(shè)計(jì)的,用途受到了限制。

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

  • LLVM可以為任何編程語(yǔ)言獨(dú)立編寫前端,也可以為任何硬件架構(gòu)獨(dú)立編寫后端.

  • 所以LLVM不是一個(gè)簡(jiǎn)單的編譯器,而是架構(gòu)編譯器,可以兼容所有前端后端。

LLVM的設(shè)計(jì)

2.3 Clang

ClangLLVM項(xiàng)目的一個(gè)子項(xiàng)目?;?code>LLVM架構(gòu)的輕量級(jí)編輯器,誕生之初就是為了替代GCC,提供更快編譯速度。 他是負(fù)責(zé)編譯C、C++、Objecte-C語(yǔ)言的編譯器,它屬于整個(gè)LLVM架構(gòu)中的編譯器前端

  • 對(duì)于開(kāi)發(fā)者而言,研究Clang可以給我們帶來(lái)很多好處。

3. LLVM案例體驗(yàn)

  • 新建一個(gè)Mac OS命令行工程:
    image.png
  • 沒(méi)有改動(dòng)代碼


    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
    預(yù)處理:宏的展開(kāi),頭文件的導(dǎo)入
  • 2: compiler, {1}, ir
    編譯:詞法、語(yǔ)法、語(yǔ)義分析,最終生成IR
  • 3: backend, {2}, assembler ()
    匯編: LLVM通過(guò)一個(gè)個(gè)的Pass去優(yōu)化,每個(gè)Pass做一些事,最后生成匯編代碼
  • 4: assembler, {3}, object
    目標(biāo)文件
  • 5: linker, {4}, image
    鏈接: 鏈接需要的動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù),生成可執(zhí)行文件
  • 6: bind-arch, "x86_64", {5}, image
    架構(gòu)可執(zhí)行文件:通過(guò)不同架構(gòu),生成對(duì)應(yīng)的可執(zhí)行文件

optimizer優(yōu)化沒(méi)有作為一個(gè)獨(dú)立階段,在編譯階段內(nèi)部完成

3.2 預(yù)處理階段

  • main.m中準(zhǔn)備測(cè)試代碼:
#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預(yù)編譯輸出main2.m文件:
clang -E main.m >> main2.m
  • 打開(kāi)main2.m,有575行。其中大部分是stdio庫(kù)的代碼:

    image.png

  • 我們發(fā)現(xiàn)測(cè)試代碼中的宏C,在預(yù)編譯階段完成了替換,變成了30

預(yù)編譯階段: 1. 導(dǎo)入頭文件 2.替換宏

  • 修改測(cè)試代碼,給int類型取個(gè)別名HT_INT_64,再次預(yù)編譯處理
#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;
}
  • 發(fā)現(xiàn)typedef不會(huì)被替換
    image.png

安全拓展:

  1. 使用define重要方法名稱進(jìn)行替換。比如#define Pay XXXTest這樣開(kāi)發(fā)者使用宏P(guān)ay開(kāi)發(fā)舒服,但是被hank時(shí),實(shí)際代碼是XXXTest,不容易被察覺(jué)。
    #define真實(shí)內(nèi)容,不應(yīng)該寫成亂碼,會(huì)讓人有此地?zé)o銀三百兩的感覺(jué),最好弄成系統(tǒng)類似名稱或其他不經(jīng)意名稱。這樣才容易忽視,安全級(jí)別更高 ??)
    typedef沒(méi)有這個(gè)偷梁換柱的效果。define只影響預(yù)處理期。

3.3 編譯階段

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

-詞法分析,就是根據(jù)空格括號(hào)這些將代碼拆分成一個(gè)個(gè)Token。標(biāo)注了位置第幾行第幾個(gè)字符開(kāi)始的。

image.png

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

    image.png

  • 語(yǔ)法分析,就是在生成語(yǔ)法樹(shù)時(shí)完成檢測(cè)的。

  • 頭文件找不到時(shí),可以指定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 生成中間代碼
  • 完成以上步驟后,就開(kāi)始生成中間代碼IR,代碼生成器(Code Generation)會(huì)將語(yǔ)法樹(shù)自頂向下遍歷逐步翻譯成LLVMIR。

  • 便于理解,我們簡(jiǎn)化代碼:

#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;
}

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

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

  • 分別選擇O0Os兩個(gè)優(yōu)化等級(jí)進(jìn)行中間代碼的生成比較:

clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll      //  O0  無(wú)優(yōu)化
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll  //  Os 最快最小
image.png

image.png
  • 優(yōu)化后的代碼,舒服多了。之前那些冗余臨時(shí)局部變量,也都被優(yōu)化,代碼量減少很多。
3.4.3 bitCode再優(yōu)化
  • Xcode7之后,開(kāi)啟bitCode蘋果會(huì)再進(jìn)一步優(yōu)化,生成.bc中間代碼。

優(yōu)化體現(xiàn):上傳APPstore的包,針對(duì)不同型號(hào)手機(jī)做了區(qū)分,不同型號(hào)手機(jī)下載時(shí),大小不同。

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

3.5 生成匯編代碼

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

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

    image.png

  • 分別對(duì)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

可以看到在生成匯編代碼時(shí),只有選擇優(yōu)化等級(jí),才能減少匯編代碼量。

【拓展】在生成中間代碼前后都可以進(jìn)行優(yōu)化。

  • [嘗試一] 將main.m直接選擇Os級(jí)別優(yōu)化生成.s匯編文件
clang -Os -S -fobjc-arc main.m -o mainOs.s
  • [嘗試二] 將main.m生成無(wú)優(yōu)化main.s,再main.s選擇Os級(jí)別優(yōu)化生成.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級(jí)別優(yōu)化生成main.s,再main.s選擇無(wú)優(yōu)化級(jí)別生成.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級(jí)別優(yōu)化生成main.s,再main.s選擇Os級(jí)別優(yōu)化生成.s匯編文件
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll
clang -Os -S -fobjc-arc mainOs.ll -o mainOsOs.s
  • 內(nèi)容比較:
image.png

3.6 生成目標(biāo)文件(機(jī)器代碼)

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

undefined: 表示當(dāng)前文件暫時(shí)找不到符號(hào)。
external:表示這個(gè)符號(hào)外部可以訪問(wèn)的。(實(shí)現(xiàn)不在我這,在外部某個(gè)地方

所以當(dāng)前雖轉(zhuǎn)換成了機(jī)器代碼。但是只是目標(biāo)文件,并不能直接執(zhí)行,需要所有資源鏈接起來(lái),才可以執(zhí)行。

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

  • 通過(guò)鏈接器把編譯產(chǎn)生的.o文件和.dylib、.a文件鏈接關(guān)聯(lián)起來(lái),生成真正的mach-o可執(zhí)行文件
clang main.o -o main // 將目標(biāo)文件轉(zhuǎn)成可執(zhí)行文件
file main            // 查看文件
xcrun nm -nm main    // 查看main的符號(hào)
image.png
  • 對(duì)比main.o目標(biāo)文件,此時(shí)生成的main文件:
  1. object文件變成了executable可執(zhí)行文件
  2. 雖然都有undefined,但是可執(zhí)行文件中指定了該符號(hào)來(lái)源庫(kù)。機(jī)器在運(yùn)行時(shí),會(huì)從相應(yīng)的庫(kù)中取讀取符號(hào)(printf)

至此,我們已完整分析:源代碼可執(zhí)行文件整個(gè)流程

image.png

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 什么是LLVM LLVM項(xiàng)目是模塊化、可重用的編譯器以及工具鏈技術(shù)的集合 The LLVM Project is ...
    那位小姐閱讀 2,686評(píng)論 0 10
  • OC底層原理 學(xué)習(xí)大綱[http://www.lxweimin.com/p/9e19354c0266] 對(duì)象的本質(zhì)...
    markhetao閱讀 1,216評(píng)論 11 1
  • 解釋性語(yǔ)言和編譯性語(yǔ)言的區(qū)別?解釋性語(yǔ)言可以通過(guò)解釋器直接執(zhí)行相應(yīng)的代碼,比如python語(yǔ)言;而編譯性語(yǔ)言要經(jīng)過(guò)...
    半邊楓葉閱讀 879評(píng)論 0 1
  • 目錄 傳統(tǒng)編譯器設(shè)計(jì) 輸入源代碼(Obj-C, Swift, ...) → 編譯器處理 → 輸出機(jī)器碼(01010...
    小瞎_MarkDash閱讀 1,286評(píng)論 0 2
  • 什么是LLVM LLVM項(xiàng)目是模塊化、可重用的編譯器以及工具鏈技術(shù)的集合。 美國(guó)計(jì)算機(jī)協(xié)會(huì) (ACM) 將其201...
    Coder_LRT閱讀 2,413評(píng)論 0 1