iOS LLVM編譯流程

1. LLVM概念

1.1 編譯器

LLVM與編譯器息息相關,究竟什么是編譯器呢?帶著疑問往下看吧。
編譯器就是將一種語言(通常為高級語言)翻譯為另一種語言(通常為低級語言的程序。一個現代編譯器的主要工作流程:源代碼(source code)預處理器(preprocessor)編譯器(compiler)目標代碼(object code)鏈接器(Linker)可執行程序(executables)

源代碼一般為高級語言(High-level language), 如CC++JavaObjective-C等或匯編語言,而目標則是機器語言的目標代碼(Object copy),有時也稱作機器代碼(Machine code)。

  • 解釋型語言
    引入一個Python代碼,見下圖:

    創建Python案例

    創建一個FirstDemo.py文件,里面只有一行代碼,print('Hello world for first time')。通過解釋器指令python,解釋這段代碼:
    解析Python案例

    通過上面的流程可以發現解釋型語言的運行流程:
    解釋型語言編譯流程

    解釋型語言特點:邊解釋,邊執行,運行速度慢,部分改動無需整體重新編譯,不可脫離解釋器環境運行

  • 編譯型語言
    引入一個C代碼,見下圖:

    創建C語言案例

    創建一個firstDemoForC.c文件,里面添加了一個main函數。首先通過clang去讀取這個代碼:
    讀取C代碼

    讀取之后發現代碼并沒有立刻執行,而是生成了一個a.out文件。這個文件就是可執行文件。通過./a.out執行這段代碼:
    a.out執行

    通過上面的流程可以發現編譯型語言的運行流程:
    編譯型語言運行流程

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

注意:編譯型語言編譯是先將代碼編譯成cpu可讀的懂的二進制才能執行

1.2 LLVM概述

LLVM構架編譯器(compiler)的框架系統,以C++編寫而成,用于優化以任意程序語言編寫的程序的編譯時間(compile-time)、鏈接時間(link-time)、運行時間(run-time)以及空閑時間(idle-time),對開發者保持開放,并兼容已有腳本。

目前LLVM已經被AppleMicrosoftGoogleFacebook等各大公司采用。

1.2.1 傳統編譯器的設計
傳統編譯器的設計
  • 編譯器前端(Frontend)
    編譯器前端的任務是解析源代碼。它會進行:詞法分析語法分析語義分析,檢查源代碼是否存在錯誤,然后構建抽象語法樹LLVM的前端會生成中間代碼IR

  • 優化器(Optimizer)
    優化器負責進行各種優化。改善運行時間,例如消除冗余計算等。

  • 后端(Backend)
    也可以叫代碼生成器(CodeGenerator),將代碼映射到目標指令集生成機器語言,并且進行機器相關的代碼優化

補充:隨著高級語言越來越多,終端類型種類的增加,所使用的的CPU架構等也不盡相同,所以為了適配多種環境,不得不設計不同的編譯器,而這些編譯器前端后端往往是捆綁在一起的。

1.2.2 LLVM的設計思路

LLVM的設計之初,即將編譯器前端(Frontend)和后端(Backend)進行了分離。將前端和后端針對不同的架構,按照獨立的項目進行研發,而它們均采用通用的代碼形式IR

前后端分離獨立適配架構

當編譯器決定支持多種語言或多種硬件架構時,LLVM最重要的地方就體現出來了,使用通用的代碼表示形式(IR),它是用來在編譯器中表示代碼的形式。所以LLVM可以為任何編程語言獨立編寫前端,并且可以為任意硬件架構獨立編寫后端。

1.2.3 iOS編譯架構

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

iOS編譯架構

1.3 Clang

ClangLLVM項目中的一個子項目。它是基于LLVM架構的輕量級編譯器,誕生之初是為了替代GCC,提供更快的編譯速度。它是負責編譯CC++Objective-C語言的編譯器,它屬于整個LLVM架構中,編譯器前端。對于開發者來說,研究Clang可以給我們帶來很多好處。

2. 編譯流程詳解

通過命令可以打印源碼的編譯階段。引入下面一個案例,main.m中添加代碼:

int main(int argc, const char * argv[]) {
    return 0;
}

通過指令clang -ccc-print-phases main.m,查看編譯流程:

通過指令查看編譯流程

流程說明:
0.輸入文件:找到源文件
1.預處理階段:這個過程處理包括宏的替換頭文件的導入
2.編譯階段:進行詞法分析語法分析、檢測語法是否正確,最終生成IR
3.后端:這里LLVM會通過一個一個的Pass節點)去優化,每個Pass做一些事情,最終生成匯編代碼
4.生成目標文件
5.鏈接:鏈接需要的動態庫靜態庫,生成可執行文件
6.通過不同的架構,生成對應的可行文件

2.1 預處理

執行如下指令:clang -E main.m,對源代碼進行預處理。見下面流程:

執行指令進行預處理

在預處理之后,輸出mainE.m文件,查看mainE.m文件:
mainE.m文件查看

打開mainE.m源文件會發現,其進行了宏的替換,如上面案例中宏c直接替換成了30;進行頭文件的導入。

2.2 編譯階段

2.2.1 詞法分析

預處理完成后就會進行詞法分析,這里會把代碼切成一個個Token,比如大小括號等于號以及字符串等。詞法分析指令為:

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

參考下面的案例:

詞法分析案例

通過指令的輸出可以看到,語法分析會將源碼進行切割并檢測。比如分號逗號int等。

2.2.2 語法分析

詞法分析完成之后就是語法分析,它的任務是驗證語法是否正確。在詞法分析的基礎上將單詞序列組合成各類語法短語,如:程序語句表達式等等,然后將所有節點組成抽象語法樹Abstract Syntax Tree,AST)。語法分析程序判斷源程序在結構上是否正確。語法分析指令:

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

參考下面的案例:

語法分析案例

通過上面的輸出可以發現,其是一個樹結構,比如下面的FunctionDecl,表示一個方法,在源碼的第五行,名稱為main,返回值為int,傳入兩個參數一個是int,一個是const char **。見下圖:

注意:一旦生成抽象語法樹,如果源碼中存在語法錯誤,就會報錯,而上面的預處理和詞法分析不會報錯。

如在源碼中設置一個語法錯誤,通過語法分析指令進行進行編譯,就會報錯,見下面案例:


語法錯誤案例
2.2.3 生成中間代碼IR(intermediate representation)

完成以上步驟后,就開始生成中間代碼IR了,代碼生成器(Code Generation)會將語法樹自頂向下遍歷逐步翻譯成LLVM IR。通過下面指令可以生成.ll的文本文件,查看IR代碼。

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

通過上面的指令獲取main.ll文件,其結構見下圖:

IR代碼查看

  • IR的基本語法

    • @ 全局標識
    • % 局部標識
    • alloca 開辟空間
    • align 內存對齊
    • i32 32bit4個字節
    • store 寫入內存
    • load 讀取數據
    • call 調用函數
    • ret返回
  • IR優化
    上面生成的IR代碼是沒有經過優化的,其實我們在平時閱讀代碼時,經常會看下面的一些定義:

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
  • fastpath:可以理解為快速流程,對更有可能執行的流程進行優化,提高運行速度;
    slowpath:基本流程,不被優化的。

XCode中也有相應的優化設置入口:

優化入口

LLVM的優化級別分別是-O0 -O1 -O2 -O3 -Os(第一個是大寫英文字母O)。可以通過下面的指令獲取優化后的IR代碼,也就是.ll文件:

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

通過以上指令生成優化后IR代碼如下:

生成優化后IR代碼

好明顯,相較于優化前,代碼精簡了很多。

這里需要注意的是,通常debug模式下,優化模式選擇None -O0,也就是不優化,避免一些保留代碼被屏蔽,從而影響調試。而release模式設置為Fastest,Smallest -Os

  • bitCode
    Xcode7以后開啟bitCode蘋果會做進一步的優化,生成.bc的中間代碼。我們通過優化后的IR代碼生成.bc代碼。對應指令為:
clang -emit-llvm -c main.ll -o main.bc

2.3 后端生成匯編代碼

我們通過最終的.bc或者.ll代碼生成匯編代碼

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

生成匯編代碼也可以進行優化:

    clang -Os -S -fobjc-arc main.m -o main.s

采用相同的案例,分別三種方式生成匯編代碼,可以看到其優化效果。在進行IR優化后生成的.ll文件,依然可以進行優化生成回應的匯編代碼。在不同的節點上都可以進行優化。見下圖:

匯編代碼的生成

2.4 生成目標文件(匯編器)

目標文件的生成,是匯編器以匯編代碼作為輸入,將匯編代碼轉換為機器代碼,最后輸出目標文件object file)。指令為:

clang -fmodules -c main.s -o main.o

生成目標文件,見下圖:

生成目標文件

其中main.o文件即為目標文件,但是此時生成的目標文件是不可執行的。通過nm命令,查看下main.o中的符號:

    $xcrun nm -nm main.o
            (undefined) external _printf
    0000000000000000 (__TEXT,__text) external _test
    000000000000000a (__TEXT,__text) external _main
  • _printf是一個undefifined external
  • undefifined表示在當前文件暫時找不到符號_printf
  • external表示這個符號是外部可以訪問的

此時就需要鏈接鏈接器把編譯生成的.o文件和(.dylib .a)文件鏈接生成一個mach-o文件。

    clang main.o -o main

生成對應的可執行文件,見下圖:


生成可執行文件

查看鏈接之后的符號:

$xcrun nm -nm main
        (undefined) external _printf (from libSystem)
        (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
000000100000f6d (__TEXT,__text) external _test
000000100000f77 (__TEXT,__text) external _main

可以發現此時的外部函數有2個,_printfdyld_stub_binder,它們都來自libSystem庫。dyld_stub_binder這個函數的作用是進行運行時綁定流程

鏈接是在編譯時,用來確定外部函數來自哪個動態庫;綁定是在運行時,將對應方法的實現地址與符號進行綁定。

可執行文件運行結果:


可執行文件運行結果
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容