LLVM與Clang
我們可以把LLVM認為是一個完整的編譯器架構,或者是一個用於開發編譯器、直譯器的庫。
理解LLVM時,我們可以分為狹義的LLVM 和 廣義的LLVM
- 廣義的LLVM : 指整個LLVM編譯器架構,包括前端、優化器、後端、函式庫
- 狹義的LLVM:後端功能(程式碼優化、生成)的一系列模組和庫
這裡我們先擺出一個操作檔案main.m
#include <stdio.h>
#import <Foundation/Foundation.h>
int main() {
NSLog(@"%@", [@5 description]);
return 0;
}
一、前端clang
Xcode 的預設編譯器是 clang,clang 的功能是首先對 Objective-C 程式碼做預處理,分析檢查,然後將其轉換為低階的類彙編程式碼:LLVM Intermediate Representation(LLVM 中間表達碼)。
1.預處理:
每當編源譯檔案的時候,編譯器首先做的是一些預處理工作。比如前處理器會處理原始檔中的巨集定義,將程式碼中的巨集用其對應定義的具體內容進行替換。
執行可檢視巨集展開:
clang -E max.m
這個過程的處理包括巨集的替換,標頭檔案的匯入。下面這些程式碼也會在這步處理。
“#define”
“#include”
“#indef”
註釋
“#pragma”
2.詞法語義分析:
詞法分析:
預處理完成以後,每一個 .m 原始檔裡都有一堆的宣告和定義。這些程式碼文字都會從 string 轉化成特殊的標記流
利用 clang 命令 clang -Xclang -dump-tokens hello.m 來將上面程式碼的標記流匯出,類似:
int 'int' [StartOfLine] Loc=<hello.m:4:1>
identifier 'main' [LeadingSpace] Loc=<hello.m:4:5>
l_paren '(' Loc=<hello.m :4:9>
r_paren ')' Loc=<hello.m:4:10>
l_brace '{' [LeadingSpace] Loc=<hello.m:4:12>
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<hello.m:5:3>
每一個標記都包含了對應的原始碼內容和其在原始碼中的位置,注意這裡的位置是巨集展開之前的位置,這樣一來,如果編譯過程中遇到什麼問題,clang 能夠在原始碼中指出出錯的具體位置。
語義分析:
詞法無錯誤後,標記流將會被解析成一棵抽象語法樹 (abstract syntax tree – AST)。
可執行clang -Xclang -ast-dump -fsyntax-only hello.m 檢視,類似:
@interface World- (void) hello;
@end
@implementation World
- (void) hello (CompoundStmt 0x10372ded0 <hello.m:8:15, line:10:1>
(CallExpr 0x10372dea0 <line:9:3, col:24> 'void'
(ImplicitCastExpr 0x10372de88 <col:3> 'void (*)(NSString *, ...)' <FunctionToPointerDecay>
(DeclRefExpr 0x10372ddd8 <col:3> 'void (NSString *, ...)' Function 0x1023510d0 'NSLog' 'void (NSString *, ...)'))
(ObjCStringLiteral 0x10372de38 <col:9, col:10> 'NSString *'
(StringLiteral 0x10372de00 <col:10> 'char [13]' lvalue "hello, world"))))
@end
一旦編譯器把原始碼生成了抽象語法樹,編譯器可以對這棵樹做分析處理,以找出程式碼中的錯誤,比如型別檢查、訊息傳送等分析檢查
3.程式碼生成
clang 完成程式碼的標記,解析和分析後,將 AST 轉換為更低階的中間碼 (LLVM IR)
CodeGen 會負責將語法樹自頂向下遍歷逐步翻譯成 LLVM IR,IR 是編譯過程的前端的輸出後端的輸入。
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
二、優化器
這裡 LLVM 會去做些優化工作,設定優化級別-01,-03,-0s,如下,輸出中間碼(絕大多數情況下是二進位制碼格式):
clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.bc
接著可用另一個命令來檢視剛剛生成的二進位制檔案:
llvm-dis < main.bc | less
如果開啟了 bitcode 蘋果會做進一步的優化,有新的後端架構還是可以用這份優化過的 bitcode 去生成。
clang -emit-llvm -c main.m -o main.bc
三、生成目標程式
彙編器將可讀的彙編程式碼轉換為機器程式碼。它會建立一個目標物件檔案,一般簡稱為 物件檔案。這些檔案以 .o 結尾。如果用 Xcode 構建應用程式,可以在工程的 derived data 目錄中,Objects-normal 資料夾下找到這些檔案。
生成彙編
clang -S -fobjc-arc main.m -o main.s
生成目標物件檔案
clang -fmodules -c main.m -o main.o
一個可執行檔案包含多個段,也就是多個 section。可執行檔案不同的部分將載入進不同的 section,並且每個 section 會轉換進某個 segment 裡。這個概念對於所有的可執行檔案都是成立的。
來看看 main.o 二進位制中的 section。我們可以使用 size 工具來觀察:
$ size -x -l -m main.o
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x37 (addr 0x100000f30 offset 3888)
Section __stubs: 0x6 (addr 0x100000f68 offset 3944)
Section __stub_helper: 0x1a (addr 0x100000f70 offset 3952)
Section __cstring: 0xe (addr 0x100000f8a offset 3978)
Section __unwind_info: 0x48 (addr 0x100000f98 offset 3992)
Section __eh_frame: 0x18 (addr 0x100000fe0 offset 4064)
total 0xc5
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000
如上有 4 個 segment。有些 segment 中有多個 section。
當執行一個可執行檔案時,虛擬記憶體 (VM - virtual memory) 系統將 segment 對映到程序的地址空間上。對映完全不同於我們一般的認識,如果你對虛擬記憶體系統不熟悉,可以簡單的想象虛擬記憶體系統將整個可執行檔案載入進記憶體 – 雖然在實際上不是這樣的。VM 使用了一些技巧來避免全部載入。
__TEXT
segment:包含了被執行的程式碼。它被以只讀和可執行的方式對映。程序被允許執行這些程式碼,但是不能修改。這些程式碼也不能對自己做出修改,因此這些被對映的頁從來不會被改變
__text
section:包含了編譯所得到的機器碼__stubs
和__stub_helper
:是給動態連結器 (dyld) 使用的
__DATA
segment:包含了將會被更改的資料,以可讀寫和不可執行的方式對映。
_nl_symbol_ptr
和__la_symbol_ptr
:它們分別是 non-lazy 和 lazy 符號指標,用於可執行檔案中呼叫未定義的函式
__PAGEZERO
segment :它的大小為 4GB。這 4GB 並不是檔案的真實大小,但是規定了程序地址空間的前 4GB 被對映為 不可執行、不可寫和不可讀。這就是為什麼當讀寫一個 NULL 指標或更小的值時會得到一個 EXC_BAD_ACCESS
錯誤。這是作業系統在嘗試防止引起系統崩潰
生成可執行檔案,這樣就能夠執行看到輸出結果
clang main.o -o main
執行生成檔案可直接執行出計算打印出結果
./main
連結器
連結器解決了多個目標檔案和庫之間的連結。
比如現在還有有一個類檔案Obj.m
,執行 clang -c Obj.m
編譯出目標檔案 Obj.o
為了生成一個可執行檔案,我們需要將這兩個目標檔案和 Foundation framework 連結起來
xcrun clang main.o Obj.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
這裡我們可輸出最終可執行檔案 a.out
可直接執行
$ ./a.out
PS:Xcode相關
xcrun
來看一些基礎性的東西:這裡使用了一個名為 xcrun 的命令列工具。看起來可能會有點奇怪,不過它非常的出色。這個小工具用來呼叫別的一些工具。原先,我們在終端執行如下命令:
$ clang -v
現在我們用下面的命令代替:
$ xcrun clang -v
在這裡 xcrun 做的是定位到 clang,並執行它,附帶輸入 clang 後面的引數。
我們為什麼要這樣做呢?看起來沒有什麼意義。不過 xcode 允許我們:
(1) 使用多個版本的 Xcode,以及使用某個特定 Xcode 版本中的工具。
(2) 針對某個特定的 SDK (software development kit) 使用不同的工具。如果你有 Xcode 4.5 和 Xcode 5,通過 xcode-select 和 xcrun 可以選擇使用 Xcode 5 中 iOS SDK 的工具,或者 Xcode 4.5 中的 OS X 工具。在許多其它平臺中,這是不可能做到的。查閱 xcrun 和 xcode-select 的主頁內容可以瞭解到詳細內容。不用安裝 Command Line Tools,就能使用命令列中的開發者工具。
下面是Xcode完整步驟:
編譯資訊寫入輔助檔案,建立檔案架構 .app 檔案
處理檔案打包資訊
執行 CocoaPod 編譯前指令碼,checkPods Manifest.lock
編譯.m檔案,使用 CompileC 和 clang 命令
連結需要的 Framework
編譯 xib
拷貝 xib ,資原始檔
編譯 ImageAssets
處理 info.plist
執行 CocoaPod 指令碼
拷貝標準庫
建立 .app 檔案和簽名