# iOS的編譯、鏈接工具 — Clang/LLVM
## LLVM的誕生
## LLVM及其子項目
### Clang:Clang做了哪些事?Clang提供了哪些功能?
## Clang-LLVM架構
## 應用
# 從源碼到可執行文件 — iOS應用編譯、靜態鏈接過程
## Clang常用命令與參數
## 1. 預處理(Preprocess)
## 2. 詞法分析 (Lexical Analysis)
## 3. 語法分析 (Semantic Analysis)
## 3.1 靜態分析(Static Analyzer)
## 4. IR代碼生成 (CodeGen)
## 5. 生成字節碼 (LLVM Bitcode)
## 6. 生成相關匯編
## 7. 生成目標文件
## 8. 生成可執行文件
# 小結:iOS從編碼到打包
# 參考鏈接
# iOS的編譯、鏈接工具 — Clang/LLVM
- The LLVM Project is a collection of modular and reusable compiler and toolchain technologies(LLVM項目是一系列分模塊、可重用的編譯工具鏈). Despite its name, LLVM has little to do with traditional virtual machines. The name "LLVM" itself is not an acronym; it is the full name of the project.
- Clang is an "LLVM native" C/C++/Objective-C compiler.
## LLVM的誕生
2000年,伊利諾伊大學厄巴納-香檳分校(University of Illinois at Urbana-Champaign 簡稱UIUC)這所享有世界聲望的一流公立研究型大學的克里斯·拉特納(Chris Lattner,twitter為 clattner_llvm) 開發了一個叫作 Low Level Virtual Machine 的編譯器開發工具套件,后來涉及范圍越來越大,可以用于常規編譯器,JIT編譯器,匯編器,調試器,靜態分析工具等一系列跟編程語言相關的工作,于是就把簡稱 LLVM 這個簡稱作為了正式的名字。
2005年,由于GCC 對于 Objective-C 的支持比較差,效率和性能都沒有辦法達到蘋果公司的要求,而且它還難以推動 GCC 團隊。于是,蘋果公司決定自己來掌握編譯相關的工具鏈,于是將Chris Lattner招入麾下,發起了 Clang 軟件項目。
- Clang 作為 LLVM 編譯器工具集的前端(front-end),目的是輸出代碼對應的抽象語法樹(Abstract Syntax Tree, AST),并將代碼編譯成LLVM Bitcode。接著在后端(back-end)使用LLVM編譯成平臺相關的機器語言。Clang支持C、C++、Objective C。
- 測試證明Clang編譯Objective-C代碼時速度為GCC的3倍,還能針對用戶發生的編譯錯誤準確地給出建議。
- 此后,蘋果使用的 GCC 全面替換成了 LLVM。
2010年,Chris Lattner開始主導開發 Swift 語言。這也使得 Swift 這門集各種高級語言特性的語言,能夠在非常高的起點上,出現在開發者面前。
2012年,LLVM 獲得美國計算機學會 ACM 的軟件系統大獎,和 UNIX,WWW,TCP/IP,Tex,JAVA 等齊名。
## LLVM及其子項目
llvm特點:
- 模塊化
- 統一的中間代碼IR,而前端、后端可以不一樣。而GCC的前端、后端耦合在了一起,所以支持一門新語言或者新的平臺,非常困難。
- 功能強大的Pass系統,根據依賴性自動對Pass(包括分析、轉換和代碼生成Pass)進行排序,管道化以提高效率。
llvm有廣義和狹義兩種定義:
- 在廣義中,llvm特指一整個編譯器框架,是一個模塊化和可重用的編譯器和工具鏈技術的集合,由前端、優化器、后端組成,clang只是用于c/c++的一種前端,llvm針對不同的語言可以設計不同的前端,同樣的針對不同的平臺架構(amd,arm,misp),也會有不同后端設計
- 在狹義中 ,特指llvm后端,指優化器(pass)對IR進行一系列優化直到目標代碼生成的過程
簡單羅列LLVM幾個主要的子項目,詳見官網:
LLVM Core libraries:LLVM核心庫提供了一個獨立于源和目標架構的現代優化器optimizer,以及對許多流行cpu(以及一些不太常見的cpu)的代碼生成(code generation)支持。這些庫是圍繞一種被稱為LLVM中間表示(“LLVM IR”)的良好指定的代碼表示構建的。
Clang:一個 C/C++/Objective-C 編譯器,提供高效快速的編譯效率,比 GCC 快3倍,其中的 clang static analyzer 主要是進行語法分析,語義分析和生成中間代碼,當然這個過程會對代碼進行檢查,出錯的和需要警告的會標注出來。(見下文詳述)
lld: 是LLVM開發一個內置的,平臺獨立的鏈接器,去除對所有第三方鏈接器的依賴。在2017年5月,lld已經支持ELF、PE/COFF、和Mach-O。在lld支持不完全的情況下,用戶可以使用其他項目,如 GNU ld 鏈接器。
lld支持鏈接時優化。當LLVM鏈接時優化被啟用時,LLVM可以輸出bitcode而不是本機代碼,而本機代碼生成由鏈接器優化處理。LLDB:基于 LLVM 和 Clang提供的庫構建的一個優秀的本地調試器,使用了 Clang ASTs、表達式解析器、LLVM JIT、LLVM 反匯編器等。
### Clang
從Clang的源碼目錄中可以大致看出Clang提供的功能:
Clang提供了哪些功能?
Clang 為一些需要分析代碼語法、語義信息的工具提供了基礎設施。分別是:
LibClang。LibClang提供了一個穩定的高級 C 接口,Xcode 使用的就是 LibClang。LibClang 可以訪問 Clang 的上層高級抽象的能力,比如獲取所有 Token、遍歷語法樹、代碼補全等。由于 API 很穩定,Clang 版本更新對其 影響不大。但是,LibClang 并不能完全訪問到 Clang AST 信息。
Clang Plugins??梢栽?AST 上做些操作,這些操作能夠集成到編譯中,成為編譯的一部分。插件是在運 行時由編譯器加載的動態庫,方便集成到構建系統中。
使用 Clang Plugins 一般都是希望能夠完全控制 Clang AST,同時能夠集成在編譯流程中,可以影響編譯的過程,進行中斷或者提示。
應用:實現命名規范、代碼規范等一些擴展功能LibTooling。是一個 C++ 接口,所寫的工具不依賴于構建系統,可以作為一個命令單獨使用。與 Clang Plugins 相比,LibTooling 無法影響編譯過程;與 LibClang 相比,LibTooling 的接口沒有那么穩定。
應用:做代碼轉換,比如把 OC 轉 JavaScript 或 Swift;代碼檢查。
Clang的優點
Clang 是 C、C++、Objective-C 的編譯前端,而 Swift 有自己的編譯前端 (也就是 Swift 前端多出的 SIL optimizer)。Clang 有哪些優勢?
- 對于使用者來說,Clang 編譯的速度非??欤瑢却娴氖褂寐史浅5?,并且兼容 GCC。
- 對于代碼診斷來說, Clang 也非常強大,Xcode 也是用的 Clang。使用 Clang 編譯前端,可以精確地顯示出問題所在的行和具體位置,并且可以確切地說明出現這個問題的原因,并指出錯誤的類型是什么,使得我們可以快速掌握問題的細節。這樣的話,我們不用看源碼,僅通過 Clang 突出標注的問題范圍也能夠了解到問題的情況。
- Clang 對 typedef 的保留和展開也處理得非常好。typedef 可以縮寫很長的類型,保留 typedef 對于粗粒度診斷分析很有幫助。但有時候,我們還需要了解細節,對 typedef 進行展開即可。
- Fix-it 提示也是 Clang 提供的一種快捷修復源碼問題的方式。在宏的處理上,很多宏都是深度嵌套的, Clang 會自動打印實例化信息和嵌套范圍信息來幫助你進行宏的診斷和分析。
- Clang 的架構是模塊化的。除了代碼靜態分析外,利用其輸出的接口還可以開發用于代碼轉義、代碼生成、代碼重構的工具,方便與 IDE 進行集成。
Clang 是基于 C++ 開發的,如果你想要了解 Clang 的話,需要有一定的 C++ 基礎。但是,Clang 源碼本身質量非常高,有很多值得學習的地方,比如說目錄清晰、功能解耦做得很好、分類清晰方便組合和復用、代碼風格統一而且規范、注釋量大便于閱讀等。
## Clang-LLVM架構
Clang-LLVM架構,即用Clang作為前端的LLVM(編譯工具集)。
LLVM架構的主要組成部分:
前端:前端用來獲取源代碼然后將它轉變為某種中間表示,我們可以選擇不同的編譯器來作為LLVM的前端,如gcc,clang(Clang-LLVM)。
LLVM支持三種表達形式:人類可讀的匯編(.ll
后綴,是LLVM IR文件,其有自己的語法)、在C++中對象形式、序列化后的bitcode形式(.bc
后綴)。Pass(v.通過/傳遞/變化 n.經過/通行證/道路/流程/階段) :是 LLVM 優化(optimize)工作的一個節點,一個節點做些事,一起加起來就構成了 LLVM 完整的優化和轉化。
Pass用來將程序的中間表示之間相互變換。一般情況下,Pass可以用來優化代碼,這部分通常是我們關注的部分。我們可以自己編寫Pass,做一些代碼混淆優化等操作。后端:后端用來生成實際的機器碼。至3.4版本的LLVM已經支持多種后端指令集,比如主流的x86、x86-64、z/Architecture、ARM和PowerPC等
雖然如今大多數編譯器都采用的是這種架構,但是LLVM不同的就是對于不同的語言它都提供了同一種中間表示。傳統的編譯器的架構如下:
LLVM的架構如下:
當編譯器需要支持多種源代碼和目標架構時,基于LLVM的架構,設計一門新的語言只需要去實現一個新的前端就行了,支持新的后端架構也只需要實現一個新的后端,其它部分完成可以復用,不用重新設計。在基于LLVM進行代碼混淆時,只需要關注中間層代碼(IR)表示。
## 應用:
- iOS 開發中 Objective-C 是 Clang / LLVM 來編譯的。
- swift 是 Swift / LLVM,其中 Swift 前端會多出 SIL optimizer,它會把 .swift 生成的中間代碼 .sil 屬于 High-Level IR, 因為 swift 在編譯時就完成了方法綁定直接通過地址調用屬于強類型語言,方法調用不再是像OC那樣的消息發送,這樣編譯就可以獲得更多的信息用在后面的后端優化上。
- Gallium3D 中使用 LLVM 進行 JIT 優化
- Xorg 中的 pixman 也有考慮使用 LLVM 優化執行速度
- LLVM-Lua 用LLVM 來編譯 lua 代碼
- gpuocelot 使用 LLVM 可以讓 CUDA 程序無需重新編譯就能夠在多種 CPU 機器上跑。
下面,通過具體的代碼、命令,來看一下iOS中源代碼詳細的編譯、鏈接過程
# 從源碼到可執行文件 — iOS應用編譯、靜態鏈接過程
我們在開發的時候的時候,如果想要生成一個可執行文件或應用,我們點擊run就完事了,那么在點擊run之后編譯器背后又做了哪些事情呢?
我們先來一個例子:
#include <stdio.h>
#define DEFINEEight 8
int main(){
int eight = DEFINEEight;
int six = 6;
int rank = eight + six;
printf("%d\n",rank);
return 0;
}
上面這個文件,我們可以通過命令行直接編譯,然后鏈接:
xcrun -sdk iphoneos clang -arch armv7 -F Foundation -fobjc-arc -c main.m -o main.o
xcrun -sdk iphoneos clang main.o -arch armv7 -fobjc-arc -framework Foundation -o main
然后將該可執行文件copy到手機目錄 /usr/bin 下面:
xx-iPhone:/usr/bin root# ./main
14
下面深入剖析其中的過程。
## Clang常用命令與參數
// 查看編譯的步驟
clang -ccc-print-phases main.m
// Rewrite Objective-C source to C++,將OC源代碼重寫為C++(僅供參考,與真正的運行時代碼還是有細微差別的)
// 如果想了解真正的代碼,可以使用-emit-llvm參數查看.ll中間代碼
clang -rewrite-objc main.m
// 查看操作內部命令
clang -### main.m -o main
// 直接生成可執行文件
clang main.m // 默認生成的文件名為a.out
/*
參數:
-cc1:Clang編譯器前端具有幾個額外的Clang特定功能,這些功能不通過GCC兼容性驅動程序接口公開。 -cc1參數表示將使用編譯器前端,而不是驅動程序。 clang -cc1功能實現了核心編譯器功能。
-E:只進行預編譯處理(preprocessor)
-S:只進行預編譯、編譯工作
-c:只進行預處理、編譯、匯編工作
-fmodules:允許modules的語言特性。
在使用#include、#import時,會看到預處理時已經把宏替換了,并且導入了頭文件。但是這樣的話會引入很多不會去改變的系統庫比如Foundation。
所以有了pch預處理文件,可以在這里去引入一些通用的頭文件。
后來Xcode新建的項目里面去掉了pch文件,引入了moduels的概念,把一些通用的庫打成modules的形式,然后導入。現在Xcode中默認是打開的,即編譯源碼時會加上-fmodules參數。也是因為modules機制的出現,pch不再默認自動創建。
使用了該參數,在導入庫的地方,只需要 @import Foundation; 就行
可以看到使用了@import之后,clang -fmodules xx 生成的文件中,不再有上萬行的系統庫的代碼引入,精簡了很多。
-fsyntax-only:防止編譯器生成代碼,只是語法級別的說明和修改
-Xclang <arg>:向clang編譯器傳遞參數
-dump-tokens:運行預處理器,拆分內部代碼段為各種token
-ast-dump:構建抽象語法樹AST,然后對其進行拆解和調試
-fobjc-arc:為OC對象生成retain和release的調用
-emit-llvm:使用LLVM描述匯編和對象文件
-o <file>:輸出到目標文件
*/
查看更多的`clang`使用方法可以在終端輸入`clang --hep`查看,也可以點擊下面的鏈接:https://link.jianshu.com/?t=https://gist.github.com/masuidrive/5231110
## 1. 預處理(Preprocess)
預編譯過程主要處理源代碼文件中的以"#"開頭的預編譯指令,不檢查語法錯誤。規則如下:
- 將所有的 “#define” 刪除,并且展開所有的宏定義。
- 處理所有條件預編譯指令,比如 “#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
- 處理 “#include” 預編譯指令,將被包含的文件內容插入到(全部復制到)該預編譯指令的位置。注意,這個過程是遞歸進行的,也就是說被包含的文件可能還包含其他文件。#include 可以導入任何(合法/不合法)文件,都能展開。
- 刪除所有的注釋“//”和“/* */”,會變成空行。
- 保留所有的 #pragma 編譯器指令,因為編譯器須要使用它們。
- 添加行號和文件名標識,比如# 2 "main.m" 2,以便于編譯時編譯器產生調試用的行號信息及用于編譯時產生編譯錯誤或警告時能夠顯示行號。
格式是“# 行號 文件名 標志
”,參數解釋如下:- 行號與文件名:表示從它后一行開始的內容來源于哪一個文件的哪一行
- 標志:可以是1,2,3,4四個數字,每個數字的含義如下:
1:表示新文件的開始
2:表示從一個被包含的文件中返回
3:表示后面的內容來自系統頭文件
4:表示后面的內容應當被當做一個隱式的extern 'C'塊
經過預編譯后的.i 文件
不包含任何宏定義,因為所有的宏已經被展開,并且包含的文件也已經被插入到 .i 文件中。所以當我們無法判斷宏定義是否正確或頭文件包含是否正確時,可以查看預編譯后的文件來確定問題。
可以通過執行以下命令,-E
表示只進行預編譯:
clang -E main.m 或者 clang -E -fmodules main.m(后者需要源碼中改為@import)
執行完這個命令之后,我們會發現導入了很多的頭文件內容。
......
# 408 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 2 3 4
# 2 "main.m" 2
int main(){
int eight = 8;
int six = 6;
int rank = eight + six;
printf("%d\n",rank);
return 0;
}
可以看到上面的預處理已經把宏替換了,并且導入了頭文件。
## 2. 詞法分析 (Lexical Analysis)
預處理之后,就是編譯。編譯過程就是把預處理完的文件進行一系列詞法分析、語法分析、語義分析及優化后生產相應的匯編代碼文件,這個過程往往是我們所說的整個程序構建的核心部分,也是最復雜的部分之一。
首先,Clang 會對代碼進行詞法分析,將代碼切分成 Token。你可以在這個鏈接
中,看到 Clang 定義的所有 Token 類型。我們可以把這些 Token 類型,分為下面這 4 類。
- 關鍵字:語法中的關鍵字,比如 if、else、while、for 等;
- 標識符:變量名;
- 字面量:值、數字、字符串;
- 特殊符號:加減乘除、左右括號等符號。
clang -fsyntax-only -Xclang -dump-tokens main.m
每一個標記都包含了對應的源碼內容和其在源碼中的位置。注意這里的位置是宏展開之前的位置,這樣一來,如果編譯過程中遇到什么問題,clang 能夠在源碼中指出出錯的具體位置。
int 'int' [StartOfLine] Loc=<main.m:4:1>
identifier 'main' [LeadingSpace] Loc=<main.m:4:5>
l_paren '(' Loc=<main.m:4:9>
r_paren ')' Loc=<main.m:4:10>
l_brace '{' Loc=<main.m:4:11>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:5:5>
identifier 'eight' [LeadingSpace] Loc=<main.m:5:9>
equal '=' [LeadingSpace] Loc=<main.m:5:15>
numeric_constant '8' [LeadingSpace] Loc=<main.m:5:17 <Spelling=main.m:2:21>>
semi ';' Loc=<main.m:5:28>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:6:5>
identifier 'six' [LeadingSpace] Loc=<main.m:6:9>
equal '=' [LeadingSpace] Loc=<main.m:6:13>
numeric_constant '6' [LeadingSpace] Loc=<main.m:6:15>
semi ';' Loc=<main.m:6:16>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:7:5>
identifier 'rank' [LeadingSpace] Loc=<main.m:7:9>
equal '=' [LeadingSpace] Loc=<main.m:7:14>
identifier 'eight' [LeadingSpace] Loc=<main.m:7:16>
plus '+' [LeadingSpace] Loc=<main.m:7:22>
identifier 'six' [LeadingSpace] Loc=<main.m:7:24>
semi ';' Loc=<main.m:7:27>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:8:5>
l_paren '(' Loc=<main.m:8:11>
string_literal '"%d\n"' Loc=<main.m:8:12>
comma ',' Loc=<main.m:8:18>
identifier 'rank' Loc=<main.m:8:19>
r_paren ')' Loc=<main.m:8:23>
semi ';' Loc=<main.m:8:24>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:9:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:9:12>
semi ';' Loc=<main.m:9:13>
r_brace '}' [StartOfLine] Loc=<main.m:10:1>
eof '' Loc=<main.m:10:2>
## 3. 語法、語義分析
這個階段有兩個模塊Parser(語法syntax分析器)、Sema(語義分析Semantic)配合完成:
- Parser:遍歷每個Token做詞句分析,根據當前語言的語法,驗證語法是否正確,最后生成一個 節點(Nodes)并記錄相關的信息。
- Semantic:在Lex 跟 syntax Analysis之后, 已經確保 詞 句已經是正確的形式,semantic 接著做return values, size boundaries, uninitialized variables 等檢查,如果發現語義上有錯誤給出提示;如果沒有錯誤就會將 Token 按照語法組合成語義,生成 Clang 語義節點(Nodes),然后將這些節點按照層級關系構成抽象語法樹(AST)。
AST可以說是Clang的核心,大部分的優化, 判斷都在AST處理(例如尋找Class, 替換代碼...等)。此步驟會將 Clang Attr 轉換成 AST 上的 AttributeList,能在clang插件上透過 Decl::getAttr<T>
獲取
Clang Attributes:是 Clang 提供的一種源碼注解,方便開發者向編譯器表達某種要求,參與控制如 Static Analyzer、Name Mangling、Code Generation 等過程, 一般以
__attribute__(xxx)
的形式出現在代碼中, Ex:NS_CLASS_AVAILABLE_IOS(9_0)
結構跟其他Compiler的AST相同。與其他編譯器不同的是 Clang的AST是由C++構成類似Class、Variable的層級表示,其他的則是以匯編語言編寫。這代表著AST也能有對應的api,這讓AST操作, 獲取信息都比較容易,甚至還夾帶著地址跟代碼位置。
AST Context: 存儲所有AST相關資訊, 且提供ASTMatcher等遍歷方法
在 Clang的定義中,節點主要分成:Type(類型),Decl(聲明),Stmt(陳述),其他的都是這三種的派生。Type具體到某個語言的類型時便可以派生出 PointerType(指針類型)、ObjCObjectType(objc對象類型)、BuiltinType(內置基礎數據類型)這些表示。通過這三者的聯結、重復或選擇(alternative)就能構成一門編程語言。舉個例子,下圖的一段代碼:詳細可以看了解 Clang AST
FunctionDecl、ParmVarDecl 都是基于 Decl派生的類,CompoundStmt、ReturnStmt、DeclStmt都是基于 Stmt派生的類。)
從上圖中可以看到:
- 一個FunctionDecl(函數的實現)由一個 ParmVarDecl聯結 CompoundStmt組成。
- 函數的 CompoundStmt 由 DeclStmt和 ReturnStmt聯結組成。
- 還可以發現這段代碼的ParmVarDecl由 BuiltinType 和一個標識符字面量聯結組成。
很明顯一門編程語言中還有很多其他形態,我們都可以用這種方式描述出來。所以說從抽象的角度看,擁有無限種形態的編程語言便可以用有限的形式來表示。
clang -fsyntax-only -Xclang -ast-dump main.m
......
`-FunctionDecl 0x7fcbb9947b20 <main.m:4:1, line:10:1> line:4:5 main 'int ()'
`-CompoundStmt 0x7fcbb9947fc8 <col:11, line:10:1>
|-DeclStmt 0x7fcbb9947c50 <line:5:5, col:28>
| `-VarDecl 0x7fcbb9947bd0 <col:5, line:2:21> line:5:9 used eight 'int' cinit
| `-IntegerLiteral 0x7fcbb9947c30 <line:2:21> 'int' 8
|-DeclStmt 0x7fcbb9947d00 <line:6:5, col:16>
| `-VarDecl 0x7fcbb9947c80 <col:5, col:15> col:9 used six 'int' cinit
| `-IntegerLiteral 0x7fcbb9947ce0 <col:15> 'int' 6
|-DeclStmt 0x7fcbb9947e20 <line:7:5, col:27>
| `-VarDecl 0x7fcbb9947d30 <col:5, col:24> col:9 used rank 'int' cinit
| `-BinaryOperator 0x7fcbb9947e00 <col:16, col:24> 'int' '+'
| |-ImplicitCastExpr 0x7fcbb9947dd0 <col:16> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fcbb9947d90 <col:16> 'int' lvalue Var 0x7fcbb9947bd0 'eight' 'int'
| `-ImplicitCastExpr 0x7fcbb9947de8 <col:24> 'int' <LValueToRValue>
| `-DeclRefExpr 0x7fcbb9947db0 <col:24> 'int' lvalue Var 0x7fcbb9947c80 'six' 'int'
|-CallExpr 0x7fcbb9947f20 <line:8:5, col:23> 'int'
| |-ImplicitCastExpr 0x7fcbb9947f08 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7fcbb9947e38 <col:5> 'int (const char *, ...)' Function 0x7fcbb9932e70 'printf' 'int (const char *, ...)'
| |-ImplicitCastExpr 0x7fcbb9947f68 <col:12> 'const char *' <NoOp>
| | `-ImplicitCastExpr 0x7fcbb9947f50 <col:12> 'char *' <ArrayToPointerDecay>
| | `-StringLiteral 0x7fcbb9947e98 <col:12> 'char [4]' lvalue "%d\n"
| `-ImplicitCastExpr 0x7fcbb9947f80 <col:19> 'int' <LValueToRValue>
| `-DeclRefExpr 0x7fcbb9947eb8 <col:19> 'int' lvalue Var 0x7fcbb9947d30 'rank' 'int'
`-ReturnStmt 0x7fcbb9947fb8 <line:9:5, col:12>
`-IntegerLiteral 0x7fcbb9947f98 <col:12> 'int' 0
在抽象語法樹中的每個節點都標注了其對應源碼中的位置,如果產生了什么問題,clang 可以定位到問題所在處的源碼位置。
語法樹直觀圖:
## 3.1 靜態分析 (Static Analyzer)
一旦編譯器把源碼生成了抽象語法樹,編譯器可以對這棵樹做分析處理,以找出代碼中的錯誤,比如類型檢查:即檢查程序中是否有類型錯誤。例如:如果代碼中給某個對象發送了一個消息,編譯器會檢查這個對象是否實現了這個消息(函數、方法)。此外,clang 對整個程序還做了其它更高級的一些分析,以確保程序沒有錯誤。
OVERVIEW: Clang Static Analyzer Checkers List
USAGE: -analyzer-checker <CHECKER or PACKAGE,...>
CHECKERS:
alpha.clone.CloneChecker Reports similar pieces of code.
alpha.core.BoolAssignment Warn about assigning non-{0,1} values to Boolean variables
alpha.core.CallAndMessageUnInitRefArg Check for logical errors for function calls and Objective-C message expressions (e.g., uninitialized arguments, null function pointers, and pointer to undefined variables)
alpha.core.CastSize Check when casting a malloc'ed type T, whether the size is a multiple of the size of T
...
scan-build 是用于靜態分析代碼的工具,它包含在 clang 的源碼包中。使用scan-build可以從命令行運行分析器,比如:
roten@localhost scan-build % ./scan-build --use-analyzer=xcode xcodebuild -project Demo123.xcodeproj // 需要設置 --use-analyzer指定 clang 的路徑
scan-build: Using '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang' for static analysis
Build settings from command line:
CLANG_ANALYZER_EXEC = /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang
CLANG_ANALYZER_OTHER_FLAGS =
CLANG_ANALYZER_OUTPUT = plist-html
CLANG_ANALYZER_OUTPUT_DIR = /var/folders/1r/n7kwlmgn74l3pvvht646f6fm0000gp/T/scan-build-2020-09-01-140523-22105-1
RUN_CLANG_STATIC_ANALYZER = YES
note: Using new build system
note: Planning build
note: Constructing build description
Build system information
....
** BUILD SUCCEEDED **
scan-build: Removing directory '/var/folders/1r/n7kwlmgn74l3pvvht646f6fm0000gp/T/scan-build-2020-09-01-140523-22105-1' because it contains no reports.
scan-build: No bugs found.
關于靜態分析更多可以查看 :Clang 靜態分析器
clang 完成代碼的標記,解析和分析后,接著就會生成 LLVM 代碼。
## 4. IR代碼生成 (CodeGen)
CodeGen負責將語法樹從頂至下遍歷,翻譯成LLVM IR,LLVM IR是Frontend的輸出,也是LLVM Backerend的輸入,橋接前后端。
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
@.str = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1
; Function Attrs: noinline optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 0, i32* %1, align 4
store i32 8, i32* %2, align 4
store i32 6, i32* %3, align 4
%5 = load i32, i32* %2, align 4
%6 = load i32, i32* %3, align 4
%7 = add nsw i32 %5, %6
store i32 %7, i32* %4, align 4
%8 = load i32, i32* %4, align 4
%9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0), i32 %8)
ret i32 0
}
declare i32 @printf(i8*, ...) #1
attributes #0 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 10, i32 15]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 11.0.0 (clang-1100.0.33.12)"}
## 4.1 中間代碼優化 (Optimize)
可以在中間代碼層次去做一些優化工作,我們在Xcode的編譯設置里面也可以設置優化級別-O1
,-O3
,-Os
對應著不同的入參,有比如類似死代碼清理,內聯化,表達式重組,循環變量移動這樣的 Pass。Pass就是LLVM系統轉化和優化的工作的一個節點,每個節點做一些工作,這些工作加起來就構成了LLVM整個系統的優化和轉化。
我們還可以去寫一些自己的Pass,官方有比較完整的 Pass 教程: Writing an LLVM Pass — LLVM 5 documentation。
## 5. 生成字節碼 (LLVM Bitcode)
我們在Xcode7中默認生成bitcode就是這種的中間形式存在,開啟了bitcode,那么蘋果后臺拿到的就是這種中間代碼,蘋果可以對bitcode做一個進一步的優化,如果有新的后端架構,仍然可以用這份bitcode去生成。
Bitcode是編譯后的程序的中間表現,包含Bitcode并上傳到App Store Connect的Apps會在App Store上編譯和鏈接。包含Bitcode可以在不提交新版本App的情況下,允許Apple在將來的時候再次優化你的App 二進制文件。
對于iOS Apps,Enable bitcode 默認為YES,是可選的(可以改為NO)。對于WatchOS和tvOS,bitcode是強制的。如果你的App支持bitcode,App Bundle(項目中所有的target)中的所有的Apps和frameworks都需要包含Bitcode。
clang -emit-llvm -c main.m -o main.bc
## 6. 生成相關匯編
clang -S -fobjc-arc main.m -o main.s
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movl $0, -4(%rbp)
movl $8, -8(%rbp)
movl $6, -12(%rbp)
movl -8(%rbp), %eax
addl -12(%rbp), %eax
movl %eax, -16(%rbp)
movl -16(%rbp), %esi
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %esi, %esi
movl %eax, -20(%rbp) ## 4-byte Spill
movl %esi, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "%d\n"
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
## 7. 生成目標文件
編譯階段完成,接下來就是匯編階段。匯編器是將匯編代碼轉變成機器可以執行的指令,每一個匯編語句幾乎都對應一條機器指令。所以匯編器的匯編過程相對于編譯器來講比較簡單,它沒有復雜的語法,也沒有語義,也不需要做指令優化,只是根據匯編指令和機器指令的對照表一一翻譯就可以了。
這些文件以 .o 結尾。如果用 Xcode 構建應用程序,可以在工程的 derived data 目錄中,Objects-normal 文件夾下找到這些文件。
clang -fmodules -c main.m -o main.o
## 8. 生成可執行文件
clang main.o -o main // 生成可執行文件
./main //執行 可執行文件 代碼
打印結果:14
## 記錄一個Clang命令報錯:
/usr/local/include/stdint.h:59:11: error: #include nested too deeply
# include <stdint.h>
^
/usr/local/include/stdint.h:82:11: error: #include nested too deeply
# include <inttypes.h>
^
...
解決方案:
1、可能是xcode-select 沒裝,于是執行xcode-select --install 進行工具安裝。
2、如果問題還在。brew doctor一下就行了
mkdir /tmp/includes
brew doctor 2>&1 | grep "/usr/local/include" | awk '{$1=$1;print}' | xargs -I _ mv _ /tmp/includes
參考鏈接:https://github.com/SOHU-Co/kafka-node/issues/881
# 小結:iOS從編碼到打包
- 首先我們編寫完成代碼之后,會通過LLVM編譯器預處理我們的代碼,比如將宏放在指定的位置
- 預處理結束之后,LLVM會對代碼進行詞法分析和語法分析,生成AST。AST是抽象語法樹,主要用來進行快速遍歷,實現靜態代碼檢查的功能。
- AST會生成IR,IR是一種更加接近機器碼的語言,通過IR可以生成不同平臺的機器碼。對于iOS平臺,IR生成的可執行文件就是Mach-O.
- 然后通過鏈接器將符號和地址綁定在一起,并且將項目中的多個Mach-O文件(目標文件)合并成一個Mach-O文件(可執行文件)。(關于Mach-O、鏈接下一節講)
- 將可執行文件與資源文件、storyboard、xib等打包,最后通過簽名等操作生成.app文件,然后對.app文件進行壓縮就生成了我們可以安裝的ipa包。
- 當然,ipa包的安裝途徑有兩種:
- 通過開發者賬號上傳到App Store,然后在App Store上下載安裝。
- 通過PP助手、iFunBox、Xcode等工具來安裝
# 參考鏈接
-
關于LLVM,這些東西你必須知道! 本篇文章大部分來自此文章。按照自己的理解記憶方式刪減、添加了一些知識。原文中還補充有:
- Clang的三大基礎設施(libclang、LibTooling、ClangPlugin)的應用、代碼示例
- 動手寫Pass的代碼示例
- 深入剖析 iOS 編譯 Clang / LLVM — 戴銘
- 《程序員的自我修養》
- (Xcode) 編譯器小白筆記 - LLVM前端Clang