第四十一節(jié)—iOS用到的LLVM(二)

本文為L_Ares個人寫作,以任何形式轉(zhuǎn)載請表明原文出處。

本文接上一節(jié)——iOS用到的LLVM(一)。請對LLVMClang不熟悉的同學們移步上一節(jié),了解了基礎(chǔ)的信息之后再閱讀本節(jié)。

一、準備工作

步驟1 : 使用xcode新建一個空的macOS下的commond Line Tool命令行工具,下面稱之為工程1。
圖1.0.0

注意 :

  1. 這里因為用的是命令行(commond Line Tool),所以初創(chuàng)的情況下沒有對其他的框架造成依賴。
  2. 因為沒有依賴,所以以下的命令都是不引入其他iOS框架的(包括也沒有引入Foundation框架)。
  3. 如果想要引入其他的框架,那么就在clang命令上添加框架的地址。下面是舉例的一個命令,引入內(nèi)容按照自己要使用的框架的情況進行修改即可。

clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己的SDK路徑) -fmodules -fsyntax-only -Xclang -ast-dump main.m

步驟2 : 打開terminal終端,進入到剛創(chuàng)建的這個項目中main.m所在的文件夾下。
步驟3 : 在terminal終端中輸入clang查看詳細編譯步驟的指令。
 clang -ccc-print-phases main.m

圖片未必看的清楚,我把內(nèi)容拷貝下來了,下面稱之為內(nèi)容1。

0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image

解釋

  1. input、preprocessorcompiler、backendassembler、linkerbind-arch,這些東西表示的是編譯中的操作名稱。
  2. main.m、{0}.....{5},這些東西表示的是這一步操作中要讀取的文件,也就是上一步操作的結(jié)果文件。
  3. objective-c、objective-c-cpp-output、ir、assembler、object、image等這些東西就是本步操作完成后,生成的文件,也就是上面2中說的上一步操作的結(jié)果文件

對于這個工程,一共有0~6一共7個階段。這就是main.m這個文件從源碼到機器語言的總的流程。下面開始按照流程來說。

二、源碼的編譯流程

2.1 編譯總流程

命令 :

 clang -ccc-print-phases main.m

編譯總流程就是上面的內(nèi)容1。

先闡述總流程0~6中都是什么 :

0: 輸入文件   : "找到源文件"。
1: 預(yù)處理階段  : 這個階段處理了"宏的替換"和"頭文件的導(dǎo)入"。
2: 編譯階段   : 進行"詞法分析"、"語法分析","語義分析"。最重要的是要"
生成中間代碼IR"。
3: 后端       : LLVM在這里會"通過一個一個的Pass去優(yōu)化傳入的IR",每個Pass做一些事情,最終生成匯編代碼。
4: 生成匯編代碼。
5: 鏈接       : "鏈接需要的動態(tài)庫和靜態(tài)庫,生成可執(zhí)行文件"。
6: 最后一步,"通過不同的架構(gòu),生成對應(yīng)的可執(zhí)行文件"。

這個步驟與之前經(jīng)常提及的編譯流程,0~6步分別對應(yīng)著 :

源文件(0)-->預(yù)編譯(1)-->編譯(2)-->匯編(3,4)-->鏈接(5)-->生成可執(zhí)行文件(6)

2.2 預(yù)處理階段

在2.1的總流程中說過,預(yù)處理階段要做的事情有兩件 :

  • 宏的替換
  • 頭文件的導(dǎo)入

舉例

  1. 打開工程1,定義一個宏#define JD_NUM 10
  2. 因為Xcode自帶的頭文件引入#import <Foundation/Foundation.h>是導(dǎo)入Foundation框架,Foundation框架太大了而且現(xiàn)在我們不需要用,所以頭文件引用就換成#import <stdio.h>。
  3. commond + s保存一下。
  4. 打開terminal終端,進入main.m所在的文件夾下。
  5. 鍵入clang指令查看預(yù)處理階段的詳細步驟。命令如下 (詳細的Clang命令解釋可以看上一節(jié)中的Clang常用指令)。
  6. 操作圖如下圖2.2.0
圖2.2.0
Clang命令 :
clang -E main.m >> main2.m

解釋 :

現(xiàn)在main.m的文件夾下就會出現(xiàn)main2.m文件,它就是經(jīng)過預(yù)處理階段操作之后的結(jié)果。如下圖2.2.1。

圖2.2.1
  1. 打開main2.m文件,拉到文件的最后,找到main函數(shù)入口。結(jié)果如下圖2.2.2
圖2.2.2
問 : typedef是不是預(yù)處理階段進行的處理?

其實這里通過對#definetypedef本身的概念了解就知道是不一樣的,typedef本身是存儲類關(guān)鍵字,本質(zhì)上并不屬于宏或頭文件。預(yù)處理階段并不會對關(guān)鍵字做解釋。

簡單驗證一下 :

  1. 工程1中加入typedef int JD_USE_INT,將int類型創(chuàng)建別名為JD_USE_INT。以后工程1改叫工程2。
  2. commons + s保存工程2的代碼。
  3. 依然使用clang -E main.m >> main2.m指令,得到main2.m。
  4. 打開main2.m直接找到文本最后的main函數(shù)入口。
  5. 操作圖如下圖2.2.3
  6. 結(jié)果圖如下圖2.2.4。
圖2.2.3
圖2.2.4

2.3 編譯階段

編譯階段的主要任務(wù)有3個 :

  • 詞法分析 : 將預(yù)處理階段傳過來的源碼的字符序列一個一個的讀入源程序,然后根據(jù)構(gòu)詞規(guī)則轉(zhuǎn)換成單詞序列(Token)。
  • 語法分析 : 在詞法分析的基礎(chǔ)上,將單詞序列組合成各類語法短句。例如 : 程序、語句、表達式等。然后將所有的語句節(jié)點抽象出來,生成抽象語法樹(AST),再檢查源程序的結(jié)構(gòu)是否符合語法規(guī)則。
  • 生成中間代碼IR : 完成上述步驟以后,代碼生成器會將抽象語法樹(AST)自上而下的遍歷,逐步將其轉(zhuǎn)換成LLVM IR。

舉例

1. 詞法分析

  1. terminal終端cd進入新的工程2main.m所在文件夾下。
  2. 輸入以下clang指令,查看詞法分析。
  3. 源代碼圖為上圖2.2.3,clang結(jié)果圖為下圖2.3.0
  4. 這里注意,空格也算一個位置。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
圖2.3.0.png

2. 語法分析

  1. terminal終端cd進入工程2main.m所在文件夾下。
  2. 輸入以下clang指令,查看語法分析。
  3. 結(jié)果如下圖2.3.1
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
圖2.3.1
  1. FunctionDecl : 方法節(jié)點

    • <line:7:1, line:13:1> :
      方法節(jié)點的代碼范圍是第7行第1個字符第13行第1個字符。
    • line:7:5 main 'int (int, const char **)' :
      第7行第5個字符的位置開始,是main方法的位置,第一個int表示main方法的返回值類型,(int, const char **)表示main方法的參數(shù)類型。
  2. ParmVarDecl : 參數(shù)節(jié)點

    • <col:10, col:14> :
      參數(shù)節(jié)點因為與main方法在同一行,所以不再說明是第7行
      直接說明第一個參數(shù)的位置是第10個字符開始,到第14個字符為止。
    • argc 'int' :
      參數(shù)名稱是argc,參數(shù)類型是int。
    • 上述是第一個參數(shù)的解釋,下面的第二個參數(shù)相同,不再贅述。
  3. CompoundStmt : 圍欄,也可以說是范圍,代表的就是main方法的{ }函數(shù)塊區(qū)域。

  4. ObjCAutoreleasePoolStmt : 自動釋放池。

  5. VarDecl : 變量節(jié)點。內(nèi)容比較簡單,可以自行理解一下。

  6. CallExpr : 調(diào)用函數(shù),這一行后面的int代表這個函數(shù)的返回值的類型是int。這里借助一下圖片,如下圖2.3.2。

圖2.3.2
  1. BinaryOperator : 函數(shù)的第二個參數(shù),這叫字節(jié)運算符。這一行表示這個字節(jié)運算符是加法運算。表明函數(shù)的第二個參數(shù)是一個加法運算的結(jié)果。再看下圖2.3.3
圖2.3.3
  1. ReturnStmt : 返回節(jié)點。
  2. IntegerLiteral : 整型。
  3. 語法分析階段會對代碼中的錯誤進行提示。例如將工程1的代碼去掉一個;,重新運行語法分析的clang指令,結(jié)果如下圖2.3.4,明顯提示少了一個;,在第10行的第42個字符處。
圖2.3.4

3. 生成中間代碼LLVM IR

IR的基本語法
語法 釋義
; 注釋
@ 全局標識
% 局部標識
alloca 開辟空間
align 內(nèi)存對齊
i32 32bit,4字節(jié)
store 寫入內(nèi)存
load 載入內(nèi)存
call 調(diào)用函數(shù)
ret 返回
操作
  1. 修改工程2的代碼,多添加一個函數(shù),方便把所有的IR代碼的語法都了解一遍。新的工程命名為工程3。工程3代碼如下 :
#import <stdio.h>

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

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

  1. commond + S保存工程3的代碼。
  2. terminal終端cd進入工程3所在文件夾下。
  3. 輸入以下clang指令,生成IR文件。
  4. clang指令執(zhí)行完成后,會在main.m文件所在的文件夾下生成main.ll文件。
  5. 生成main.ll的結(jié)果如下圖2.3.5。
clang -S -fobjc-arc -emit-llvm main.m
圖2.3.5.png
  1. 可以利用Sublime Text打開main.ll文件,并將Sunlime Text軟件右下角的Plain Text改成Objective-C的格式。結(jié)果如下圖2.3.6。
圖2.3.6.png

2.4 優(yōu)化器

我們知道了ClangLLVM的前端,Clang做了2.2預(yù)處理階段2.3編譯階段的事情,那么從哪里開始算是LLVM的后端?

優(yōu)化器(Optimizer)和代碼生成器(CodeGenerator)都可以算作LLVM后端。

  • 后端的作用 :
    (1). 優(yōu)化。
    2.3編譯階段最后生成的LLVM IR代碼傳入一個一個的Pass進行IR優(yōu)化,每個Pass都會對傳入的IR進行本Pass要做的優(yōu)化。
    (2). 生成匯編代碼。
    完成所有所需Pass優(yōu)化的IR將會變成匯編代碼。
  • 什么是Pass?
    (1). 首先,Pass是節(jié)點。是LLVM優(yōu)化過程中的優(yōu)化邏輯所在之處。
    (2). 其次,Pass是屬于LLVM的后端(Backend)的。
    (3). 最后,LLVM的優(yōu)化是以節(jié)點(Pass)來完成的,是一個節(jié)點一個節(jié)點去完成的,所有節(jié)點一起合作之后,才完成了LLVM的優(yōu)化的轉(zhuǎn)化。
    例如 : 有的節(jié)點是負責運算之后將冗余的代碼減去的,有的節(jié)點則是負責跳轉(zhuǎn)之后再減去冗余代碼的。
  • 什么是bitCode?
    (1). 蘋果在xcode7之后可以開啟bitCode,在iOS中,我們說bitCode是蘋果對LLVM在編譯階段生成的IR的一種特殊形式,本質(zhì)上bitCode也是IR,也是中間代碼,它以二進制形式存在,蘋果推出bitCode就是一種官方的優(yōu)化方式。
    (2). 在經(jīng)過bitCode的優(yōu)化之后,IR代碼文件會轉(zhuǎn)化成.bc文件格式的中間代碼。

舉例

很明顯,通過2.3編譯階段生成的IR在閱讀理解上是很冗余的,短短的幾行簡單的代碼都變得很長,所以LLVM中存在對IR代碼進行一些適當?shù)膬?yōu)化,當然這個優(yōu)化在xcode上面是可選擇的。還是選擇以工程3為基本,如圖2.4.0。

圖2.4.0.png

xcode是帶有對IR代碼是否進行優(yōu)化的可視化界面的,一般情況下,Debug模式下默認都是沒有開啟代碼優(yōu)化,而Release模式下,則開啟了優(yōu)化。

4.1 LLVM的優(yōu)化級別
級別 釋義
O0 None,不進行IR優(yōu)化
O1 Fast
O2 Faster
O3 Fastest
Os Fastest , Smallest
Ofast 比Os還要更近一步的優(yōu)化
Oz 讓IR代碼體積最小的優(yōu)化

注釋 : 級別的中的O是英文字母,不是數(shù)字0。

4.2 利用命令行對IR進行優(yōu)化的舉例

還是利用工程3,我們就不直接利用xcode的優(yōu)化了,為了看到優(yōu)化的IR代碼,利用終端的命令行對IR代碼進行優(yōu)化。

  1. 利用終端,進入到工程3main.m所在文件夾下。
  2. 終端中輸入以下clang命令
clang -Os -S -fobjc-arc -emit-llvm main.m
  1. 依然利用Sublime Text打開main.ll文件,調(diào)整成OC的語法格式。
  1. 結(jié)果如下圖2.4.1所示。
圖2.4.1.png
4.3 bitCode的生成

還是利用工程3。

  1. 利用終端進入工程3main.m所在的文件夾下。
  2. 終端中輸入以下clang命令,先生成IRmain.ll文件。
clang -S -fobjc-arc -emit-llvm main.m
  1. 再在終端中輸入以下clang命令,利用main.ll文件生成main.bc文件。
clang -emit-llvm -c main.ll -o main.bc
  1. 生成的結(jié)果如下圖2.4.2所示。
圖2.4.2.png

2.5 匯編

2.5.1 直接生成匯編

直接利用上面圖2.4.2中的3個文件。

  1. .m格式的源文件轉(zhuǎn)化為匯編代碼,利用下述命令。
clang -S -fobjc-arc main.m -o main.s
  1. .ll格式的IR代碼文件轉(zhuǎn)化為匯編代碼,利用下述命令。
clang -S -fobjc-arc main.ll -o main1.s
  1. .bc格式的bitCode優(yōu)化后的文件轉(zhuǎn)化為匯編代碼,利用下述命令。
clang -S -fobjc-arc main.bc -o main2.s

結(jié)果如下圖2.5.0和2.5.1所示

圖2.5.0.png
圖2.5.1.png

2.5.2 生成匯編可進行優(yōu)化

生成匯編進行的優(yōu)化是對機器語言的優(yōu)化。

我們已經(jīng)知道,源碼變成匯編的過程要經(jīng)過 : 源碼 --> IR --> bitcode --> 匯編,其實除了在源碼 --> IR的時候可以進行優(yōu)化,在生成匯編的時候,系統(tǒng)還是會進行一步優(yōu)化,我們在上一節(jié)的傳統(tǒng)優(yōu)化器的設(shè)計中說過后端/代碼生成器也有優(yōu)化能力。

還是利用工程3的源碼。并且優(yōu)化的級別統(tǒng)一選定為最高級別Os,其他的級別自行更換嘗試。

  1. 源碼直接生成匯編的優(yōu)化
clang -Os -S -fobjc-arc main.m -o main3.s

對比main.m未經(jīng)過優(yōu)化和經(jīng)過優(yōu)化分別生成的匯編main.smain3.s :

圖2.5.2.png
  1. IR生成匯編的優(yōu)化
clang -Os -S -fobjc-arc main.ll -o main4.s
圖2.5.3.png
  1. bc生成匯編的優(yōu)化
clang -Os -S -fobjc-arc main.bc -o main5.s
圖2.5.4.png

因為我們的源碼只有最簡單的11行,所以優(yōu)化的效果不會有那么的大,但也可以看得出來優(yōu)化的效果還是很好的。

但是?。?!這里我們正常的情況下是不可以手動的進行調(diào)節(jié)的。

對比IR的優(yōu)化來看,IR的優(yōu)化我們可以在xcode中就可以進行配置,就是上面的圖2.4.0,而生成匯編的時候進行的優(yōu)化,我們沒有辦法人工的干預(yù)。

2.6 生成目標文件和生成可執(zhí)行文件(鏈接)

以下所有的操作都是以工程3為基礎(chǔ)的。

2.6.1 生成目標文件

目標文件的生成是匯編器以匯編代碼作為輸入,將匯編代碼轉(zhuǎn)換成機器代碼,最后輸出目標文件(object file)

常用命令是 :

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

命令結(jié)果 :

圖2.6.0.png

查看目標文件main.o的符號的命令 :

xcrun nm -nm main.o

命令結(jié)果 :

圖2.6.1.png
  1. undefined : 表示在當前文件,暫時找不到某個符號,比如在上圖2.6.1中就是說找不到_printf這個符號,也就是找不到printf這個方法。

  2. external : 表示這個符號是外部可以訪問的。比如上圖的2.6.1中找不到的_printf這個符號是可以在外部訪問的到的,也就是說printf這個方法不是本文件的方法,但是是可以經(jīng)過外部的文件找得到的方法。

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

我們知道,可執(zhí)行文件的生成就是由很多的.o文件來完成的。這些.o文件要集合在一起需要要存在一些的聯(lián)系,而這個聯(lián)系就是由鏈接(linker)來做到的。

連接器把編譯產(chǎn)生的.o文件和.dylib.a文件生成一個mach-o文件。

用下述命令生成可執(zhí)行文件 :

clang main.o -o main

生成可執(zhí)行文件的結(jié)果 :

圖2.6.2.png

鏈接之后,我們再查看可執(zhí)行文件的符號,對比目標文件來看。

查看可執(zhí)行文件的符號的命令 :

xcrun nm -nm main

結(jié)果圖 :

圖2.6.3.png

從圖3.6.3中可以看到,雖然undefined標識是依然存在的,但是后面的括號中已經(jīng)告訴我們_printf符號是來自于libSystem的。

那為什么要有這個from libSystem呢?

因為當這個可執(zhí)行文件main要被執(zhí)行的時候,main內(nèi)部有一個符號_printf是來自于外部,當要調(diào)用這個_printf的時候,dyld會在加載的時候進行綁定,而如何綁定呢?就會根據(jù)符號提供的位置,也就是(from libSystem)來確定_printf符號是來自于libSystem的,這時iOS的操作系統(tǒng)中的libSystem動態(tài)庫就會把_printf的地址告訴dyld,然后進行符號的綁定。

所以說,這個符號是在運行的時候動態(tài)綁定的。這也是為什么fishhook可以去hook一些外部函數(shù)的原因。

main這個可執(zhí)行文件生成之后,我們就可以直接執(zhí)行這個main,命令行如下 :

./main

結(jié)果如下圖 :

圖2.6.4.png

也可以查看一下main的基本信息,比如它的格式、版本信息、運行所需的系統(tǒng)要求等,命令行 :

file main

結(jié)果如下圖 :

圖2.6.5.png

可以看到main的文件格式是Mach-O,是64位的x86架構(gòu)下可運行的,也就是說main是一個單一架構(gòu)的文件不是胖二進制文件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容