Clang LLVM 簡介


本文將簡單介紹 Clang LLVM 相關的知識,然后介紹一下代碼是如何一步步的編譯運行的,以及可以利用 clang 能做些什么。

簡介

編譯器就是語言翻譯器,把高級語言翻譯成計算機能夠執行的機器語言。

語言翻譯主要工作流程:
源代碼 (source code) → 預處理器 (preprocessor) → 編譯器 (compiler) → 目標代碼 (object code) → 鏈接器 (linker) → 可執行程序 (executables)

LLVM (Low Level Virtual Machine) 是一個開源的編譯器架構。Clang 是 LLVM 的一個編譯器前端,它目前支持 C, C++, Objective-C 等編程語言。

Clang 對源程序進行預處理、詞法分析、語法分析,并將分析結果轉換為 Abstract Syntax Tree ( 抽象語法樹 ) ,最后使用 LLVM 作為編譯器后端代碼的生成器。

Clang 的開發目標是提供一個可以替代 GCC 的前端編譯器。Apple 對 Objective-C 新增很多特性,想做的很多功能,比如更好的IDE支持,GCC 不能很好的支持,于是,蘋果開發了 Clang 與 LLVM 來完全取代GCC。Clang作為編譯器前端,LLVM作為編譯器后端。

與 GCC 相比,Clang 是一個重新設計的編譯器前端,具有一系列優點,例如模塊化,代碼簡單易懂,占用內存小以及容易擴展和重用等。由于 Clang 在設計上的優異性,使得 Clang 非常適合用于設計源代碼級別的分析和轉化工具。Clang 也已經被應用到一些重要的開發領域,如 Static Analysis 是一個基于 Clang 的靜態代碼分析工具。


用 Clang 編譯 OC 程序

當用 Xcode 創建了項目,然后點擊 run 運行的時候,可以在 Xcode 中看到編譯的信息:

看一下編譯 main.m 文件的信息,相當于執行了一長串的命令,其中命令的參數就是你在 Build Settings 里面設置的一些選項,拼接成了這一串命令,主要的就是調用 Clang 編譯的命令:

clang -x objective-c -fobjc-arc ... main.m -o main.o


clang 命令:

在命令行中 clang 相當于一個黑盒的驅動,里面封裝了編譯管線、前段命令、LLVM 命令、Toolchain 命令等。


clang 編譯的過程

下面通過編譯 main.m 文件,來看一下編譯的過程。main.m 中是很簡單的打印代碼:

#import <Foundation/Foundation.h>
int main() {
    @autoreleasepool {
        NSLog(@"Hello world!");
    }
    return 0;
}

命令行輸入命令:
clang -fobjc-arc -framework Foundation main.m -o main

-fobjc-arc 表示編譯器需要支持 ARC 特性
-framework Foundation 表示引用Foundation框架

上面的命令會生成可執行文件 main,然后命令行輸入執行文件 main:
./main
看到運行結果:
Hello world!

實質上,上述編譯過程是分為四個階段進行的,即預處理(Preprocess)、編譯(Compilation)、匯編 (Assembly)和連接(Linking)。


1、預處理(Preprocess)

這個步驟會進行,import 頭文件的處理,宏定義的展開,#開頭的預處理指令等,的處理。
預處理的命令:
clang -E main.m 或者 clang -E main.m -o test.i

查看文件可看到頭部,十幾萬行的預處理


-fmodules 參數可以把那些庫打包導入,import Foundation,這樣每次編譯都不用展開那么多東西
clang -E -fmodules main.m


2、詞法分析(Lexical Analysis)

詞法分析,將預處理過的代碼轉化成一個個的 Token,對應的命令為:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

處理后的結果為:


可看到代碼,拆分成了一個個的 Token


3、語法分析(Semantic Analysis)

語法分析,在 clang 中由 Parser 和 Sema 兩個模塊配合完成,來驗證語法是否正確。
提示代碼哪里寫錯了,少了冒號、括號等一些提示。
根據當前語言的語法,生成語義節點,并將所有節點組合成抽象語法樹(AST: Abstract Syntax Tree),對應的命令為:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

處理后,生成的語法樹:


FunctionDecl,表示函數名 main
CompoundStmt,表示大括號 {}
ObjCAutoreleasePoolStmt,表示 @autoreleasepool

靜態分析:
編譯器把源碼生成了抽象語法樹,編譯器可以對這棵樹做分析處理,以找出代碼中的錯誤,比如類型檢查:即檢查程序中是否有類型錯誤。例如:如果代碼中給某個對象發送了一個消息,編譯器會檢查這個對象是否實現了這個消息(函數、方法)。此外,clang 對整個程序還做了其它更高級的一些分析,以確保程序沒有錯誤。

還有一種類型檢查:動態分析
動態的在運行時做檢查,靜態的在編譯時做檢查。編寫代碼時可以向任意對象發送任何消息,在運行時,才會檢查對象是否能夠響應這些消息。


4、IR 代碼生成 (CodeGen)

clang 完成代碼的標記,解析和分析后,接著就會生成 LLVM 代碼。將上一步的語法樹從頂至下遍歷,翻譯成 LLVM IR。
LLVM IR 是編譯前端的輸出,也是 LLVM 后端的輸入,是前后端橋接語言。

與 OC Runtime 橋接:

  • Class、Meta Class、Protocol、Category 內存結構生成,并存放在指定 section 中(如 Class:_DATA, _objc_classrefs)

  • Method、Ivar、Property 內存結構生成

  • 組成 method_list、ivar_list、property_list 并填入 Class

  • Non-Fragile ABI:為每個 Ivar 合成 OBJC_IVAR_$_ 偏移值常量

  • 存取 Ivar 的語句(_ivar = 123; int a = _ivar;)
    轉寫成 base + OBJC_IVAR_$_的形式

  • 將語法樹中的 ObjCMessageExpr 翻譯成相應版本的 obj_msgSend,對 super 關鍵字的調用翻譯成 objc_msgSendSuper

  • 根據修飾符 strong、weak、copy、atomic 合成 @property 自動實現的 setter、getter

  • 處理 @synthesize

  • 生成 block_layout 的數據結構

  • 變量的 capture(__block __weak)

  • 生成 _block_invoke 函數

  • ARC 分析對象引用關系,將 obj_storeStrong/objc_storeWeak 等 ARC 代碼插入

  • 將 ObjcCAutoreleasePoolStmt 轉譯成 objc_autoreleasePoolPush/Pop

  • 實現自動調用 [super dealloc]

  • 為每個擁有 ivar 的 Class 合成 .cxx_destructor 方法來自動釋放類的成員變量,代替 MRC 時代的 self.xxx = nil

可以使用下面的命令,查看生成 IR 代碼:
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
-S 編譯到匯編層面
-fobjc-arc 開啟ARC
-emit-llvm 生成中間的LLVM語言

執行命令后可得到文件 main.ll:

define i32 @main() #0 {
 %1 = alloca i32, align 4
 store i32 0, i32* %1, align 4
 %2 = call i8* @llvm.objc.autoreleasePoolPush() #1
 notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
 call void @llvm.objc.autoreleasePoolPop(i8* %2)
 ret i32 0
}

這里 LLVM 會去做些優化工作,在 Xcode 的編譯設置里也可以設置優化級別 -O1,-O3,-Os :
clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll

下面是優化后的中間LLVM代碼

define i32 @main() local_unnamed_addr #0 {
  %1 = tail call i8* @llvm.objc.autoreleasePoolPush() #1
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*)), !clang.arc.no_objc_arc_exceptions !9
  tail call void @llvm.objc.autoreleasePoolPop(i8* %1) #1
  ret i32 0
}

生成字節碼 LLVM Bitcode
clang -emit-llvm -c main.m -o main.bc
將上面生成的 IR 代碼生成二進制碼格式


5、生成 Target 相關匯編(Assemble)

生成對應機器相關的匯編語言:
clang -S -fobjc-arc main.m -o main.s

生成 Target 相關 Mach-O 文件
clang -fmodules -c main.m -o main.o

Mach-O(Mach object)文件,是一種用于記錄可執行文件、對象代碼、共享庫、動態加載代碼和內存轉儲的文件格式,是macOS/iOS上程序以及庫的標準格式。


6、生成可執行文件

鏈接生成可執行文件 main。
將程序的目標文件與所需的所有附加的目標文件(靜態連接庫和動態連接庫)連接起來,最終生成可執行文件。
clang main.o -o main
執行就可以看到程序運行
./main


7、編譯過程小結
  • 預處理
    • 符號化 (Tokenization)
    • 宏定義的展開
    • #include 的展開
  • 語法和語義分析
    • 將符號化后的內容轉化為一棵解析樹 (parse tree)
    • 解析樹做語義分析
    • 輸出一棵抽象語法樹(Abstract Syntax Tree* (AST))
  • 生成代碼和優化
    • 將 AST 轉換為更低級的中間碼 (LLVM IR)
    • 對生成的中間碼做優化
    • 生成特定目標代碼
    • 輸出匯編代碼
  • 匯編器
    • 將匯編代碼轉換為目標對象文件。
  • 鏈接器
    • 將多個目標對象文件合并為一個可執行文件 (或者一個動態庫)


clang 的應用

上面介紹了,用 clang 編譯的過程,從預處理到詞法分析、語法分析,再到生成匯編語言。
那么我們能用 clang 做些什么?

libclang
libclang 是一個 C 的類庫,提供了簡潔的 API,可以對 C 和 clang 做橋接,并可以用它對所有的源碼做分析處理,如獲取 Tokens、遍歷語法樹、代碼補全、獲取診斷信息等。
libclang api 穩定,不受 clang 源碼更新影響,但是只能訪問上層語法樹,不能獲取全部信息。
推薦使用 ClangKit 它是基于 clang 提供的功能,用 Objective-C 進行封裝的一個庫。

clang 還提供了一個直接使用 LibTooling 的 C++ 類庫。它能夠發揮 clang 的強大功能,可以對源碼做任意類型的分析,甚至重寫程序,對語法樹有完全的控制權。如果你想要給 clang 添加一些自定義的分析、創建自己的重構器 (refactorer)、或者需要基于現有代碼做出大量修改,甚至想要基于工程生成相關圖形或者文檔,那么可以使用 LibTooling。


將 OC 代碼轉換為 C/C++

當需要查看 OC 代碼底層實現時,可以利用 clang 將 OC 代碼轉換為 C/C++ 代碼。
下面命令可以進行轉換:

clang -rewrite-objc main.m

//-fobjc-arc 表示ARC環境。-fobjc-runtime 表示當前運行時環境。
clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.15 main.m

執行后,可在當前文件夾下生成 main.cpp 文件。

當 .m 文件包含系統頭文件時,會報錯找不到頭文件,可以指定SDK解決:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

或者

xcrun -sdk iphonesimulator13.2 clang -rewrite-objc main.m
//其中的 sdk iphonesimulator13.2 可以通過命令 xcodebuild -showsdks 來查看

或者使用下面命令,指定了 SDK 和架構,轉換后的代碼會少一些:

xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc main.m


References

https://llvm.org/
http://clang.llvm.org/docs/index.html
https://objccn.io/issue-6-2/
https://objccn.io/issue-6-3/
https://www.cnblogs.com/wfwenchao/p/5543595.html
https://www.ibm.com/developerworks/cn/opensource/os-cn-clang/index.html

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