前言
編譯的主要任務(wù)是將源代碼文件作為輸入,最終輸出目標(biāo)文件,這期間發(fā)生了什么?便是我們本篇文章要介紹的。在開始之前我們先了解一下編譯器。
編譯器
編譯器(
compiler
)是一種計(jì)算機(jī)程序,它會(huì)將某種編程語言寫成的源代碼(原始語言)轉(zhuǎn)換成另一種編程語言(目標(biāo)語言)。引自維基百科
傳統(tǒng)編譯器的架構(gòu),一般分三部分:
- 前端(
Frontend
):解析源代碼,檢查源代碼是否有錯(cuò)誤,并構(gòu)建特定語言的抽象語法樹(Abstract Syntax Tree
縮寫:AST
)來表示輸入的代碼。也負(fù)責(zé)選擇性的地將AST
轉(zhuǎn)換為新的表示形式以進(jìn)行優(yōu)化。 - 優(yōu)化器(
Optimizer
):負(fù)責(zé)進(jìn)行各種轉(zhuǎn)換,以嘗試改善代碼的運(yùn)行時(shí)間,例如消除冗余計(jì)算,并且通常或多或少地獨(dú)立于編程語言和目標(biāo)代碼。 - 后端(
Backend
):也稱代碼生成器,將代碼映射到目標(biāo)架構(gòu)的指令集上;其常見部分有:指令選擇,寄存器分配,指定調(diào)度。
這種架構(gòu)的優(yōu)勢(shì)在于解耦合,實(shí)現(xiàn)一種編程語言,只需要實(shí)現(xiàn)它的前端,對(duì)于優(yōu)化器與后端部分是可以復(fù)用的;支持新的目標(biāo)架構(gòu),只需要實(shí)現(xiàn)它的后端即可;如果編譯器不是這種架構(gòu),三部分未分開,那么實(shí)現(xiàn)N
個(gè)編程語言,去支持M
個(gè)目標(biāo)架構(gòu),就需要實(shí)現(xiàn)N*M
個(gè)編譯器。
這種傳統(tǒng)編譯器的架構(gòu)有三個(gè)成功的案例:
-
Java
和.Net
虛擬機(jī);它們都提供了對(duì)JIT
編譯器和運(yùn)行時(shí)的支持,并且還定義了字節(jié)碼的格式(bytecode
),這意味著任何可以編譯為字節(jié)碼的語言,都可以復(fù)用優(yōu)化器和JIT
(動(dòng)態(tài)編譯)和運(yùn)行時(shí)能力。 - 將輸入源轉(zhuǎn)換為
C
代碼(或其他某種語言)并通過現(xiàn)有的C
編譯器編譯 - 這種模式的最終成功實(shí)施是
GCC
,GCC
支持許多前端和后端,并擁有活躍而廣泛的貢獻(xiàn)者社區(qū)。
GCC
GCC
的概述
Xcode5
之前的版本中使用的是GCC
編譯器,由于GCC
,歷史悠久,體系結(jié)構(gòu)相對(duì)復(fù)雜,功能模塊化復(fù)用難度大且不受蘋果公司的約束,很難滿足蘋果系統(tǒng)的發(fā)展需求。因此在Xcode5
中拋棄了GCC
,采用Clang/LLVM
進(jìn)行編譯。
GCC
:是GNU Compiler Collection
的縮寫,指GNU
編譯器套裝。Linux
系統(tǒng)的核心組成部分就有GNU
工具鏈,GCC
也是GNU
工具鏈的重要組成部分,因此GCC
也是作為Linux
系統(tǒng)的標(biāo)準(zhǔn)編譯器。GCC
可處理的語言有C
、C++
、Objective-C
、Java
、Go
等。
使用GCC
命令gcc -ccc-print-phases main.m
查看編譯OC
的步驟:
*deMacBook-Pro:Mach-O *$ gcc -ccc-print-phases main.m
+- 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
GCC
的架構(gòu)
前端讀取源文件將其轉(zhuǎn)化為AST
,由于每種語言生成的AST
是有差異的,所以需要需要轉(zhuǎn)換為通用的與語言無關(guān)的統(tǒng)一形式GENERIC
。
中端將GENERIC
,利用gimplifier
技術(shù),簡(jiǎn)化GENERIC
的復(fù)雜結(jié)構(gòu),將其轉(zhuǎn)換為一種中間表示形式稱為:GIMPLE
,再轉(zhuǎn)換為另一種SSA
(static single assignment
)的表示形式也是用于優(yōu)化的,GCC
對(duì)SSA
樹執(zhí)行20
多種不同的優(yōu)化。經(jīng)過SSA
優(yōu)化后,該樹將轉(zhuǎn)換回GIMPLE
形式,用來生成一個(gè)RTL
樹,RTL
寄存器轉(zhuǎn)換語言,全稱(register-transfer language
);RTL
是基于硬件的表示形式,與抽象的目標(biāo)架構(gòu)相對(duì)應(yīng),處理寄存器分配、指令調(diào)度等。RTL
優(yōu)化過程以RTL
形式對(duì)樹進(jìn)行優(yōu)化。
后端使用RTL
表示形式生成目標(biāo)架構(gòu)的匯編代碼。如:x86
后端。
LLVM
LLVM
的概述
LLVM
項(xiàng)目是模塊化和可重用的編譯器及工具鏈技術(shù)的集合。名稱LLVM
是Low Level Virtual Machine
的縮寫,盡管名稱如此,但是LLVM
與傳統(tǒng)虛擬機(jī)關(guān)系不大,它是LLVM
項(xiàng)目的全名。
The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. 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. 引自LLVM官網(wǎng)
LLVM
有許多的子項(xiàng)目,比如Clang
,LLDB
,MLIR
等。
LLVM
的歷史
-
LLVM
起源于2000
年Vikram Adve
與Chris Lattner
的研究,目的:為所有的靜態(tài)語言(C
/C++
)和動(dòng)態(tài)語言(運(yùn)行時(shí)改變其結(jié)構(gòu)的語言,如:OC
/JavaScript
)創(chuàng)造出動(dòng)態(tài)編譯技術(shù)。 - 蘋果公司
2005
雇傭Chris Lattner
與他的團(tuán)隊(duì)為蘋果電腦開發(fā)應(yīng)用程序系統(tǒng),LLVM
為現(xiàn)今macOS
與iOS
開發(fā)工具的一部分。 - 因
LLVM
對(duì)產(chǎn)業(yè)的貢獻(xiàn),2012
年獲得了ACM軟件系統(tǒng)獎(jiǎng)。獲得該獎(jiǎng)項(xiàng)的有Unix
、Java
、TCP/IP
、DNS
、Mach
-
2019
年10
月開始,LLVM
項(xiàng)目的代碼托管正式遷移到了GitHub
。
LLVM
的架構(gòu)
LLVM
最重要的設(shè)計(jì)是中間表示LLVM Intermediate Representation
(IR
),它是在編譯器中表示代碼的一種形式。優(yōu)化器使用LLVM IR
作中間的轉(zhuǎn)換與分析處理。LLVM IR
本身就是具有良好語義定義的一流語言。
在基于LLVM
的編譯器中,Frontend
負(fù)責(zé)對(duì)輸入的代碼進(jìn)行解析,校驗(yàn)和分析錯(cuò)誤,然后將解析后的代碼轉(zhuǎn)換為LLVM IR
(通常情況,是將構(gòu)建的抽象語法樹AST
轉(zhuǎn)換為LLVM IR
,但不總是這樣的)。可以選擇通過一系列分析和優(yōu)化過程來傳遞LLVM IR
,以改進(jìn)代碼,然后將其發(fā)送到代碼生成器(Backend
)中,生成原始的機(jī)器碼。
LLVM IR
不僅是完整的代碼表示,而且也是優(yōu)化器optimizer
的唯一接口。這意味著寫一個(gè)LLVM
的前端只需要知道LLVM IR
即可,這是LLVM
的一個(gè)新穎的特性,也是LLVM
成功地被廣泛應(yīng)用的一個(gè)主要原因。反觀GCC
編譯器,寫一個(gè)前端需要知道生成的GCC
樹的數(shù)據(jù)結(jié)構(gòu)以及使用GIMPLE
去寫GCC
的前端,GCC
后端需要知道RTL
是如何工作的。
LLVM IR
是前端輸出,后端的輸入:
LLVM
廣義是指LLVM
整個(gè)架構(gòu),狹義指Clang
編譯器的后端。
Clang
Clang
是LLVM
的子項(xiàng)目,是C
、C++
、Objective C
語言的編譯器的前端。Clang
編譯Objective-C
代碼時(shí)速度為GCC
的3
倍。詳見維基百科。
Clang
編譯過程
下面是一個(gè)基于簡(jiǎn)單的OC
工程,不依賴Xcode
,而是使用終端編譯的例子。
編譯前工程源代碼主要分為main.m
和Person.m
類,代碼如下:
///main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#define SomeDefine @"你好,世界"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 注釋
NSLog(@"Hello, World!");
#pragma mark 我是注釋
NSLog(@"%@",SomeDefine);
/// MARK: 我也是注釋
Person *instance = [[Person alloc]init];
[instance share];
}
return 0;
}
///Person.m
#import "Person.h"
@implementation Person
- (void)share {
NSLog(@"持之以恒");
}
@end
首先我們運(yùn)行clang -ccc-print-phases main.m
查看整體的編譯過程:
*deMacBook-Pro:Mach-O *$ clang -ccc-print-phases main.m
+- 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
接下來,基于這個(gè)例子,我們使用終端逐步編譯,生成我們的可執(zhí)行文件,并最終控制臺(tái)打印我們的信息。
預(yù)處理
基于輸入,通過預(yù)處理器執(zhí)行一系列的文本轉(zhuǎn)換與文本處理。預(yù)處理器是在真正的編譯開始之前由編譯器調(diào)用的獨(dú)立程序。
終端命令:
# 編譯階段選擇參數(shù): -E 運(yùn)行預(yù)處理這一步
clang -E main.m
# 預(yù)處理結(jié)果輸出到main.mi文件中
clang -E main.m -o main.mi
輸出結(jié)果:
# 193 "/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 9 "main.m" 2
# 1 "./Person.h" 1
# 10 "./Person.h"
#pragma clang assume_nonnull begin
@interface Person : NSObject
- (void)share;
@end
#pragma clang assume_nonnull end
# 10 "main.m" 2
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
NSLog(@"%@",@"你好,世界");
Person *instance = [[Person alloc]init];
[instance share];
}
return 0;
}
最終C
輸出.i
文件,C++
輸出.ii
文件,Objective-C
輸出.mi
文件,Objective-C ++
輸出.mii
文件。
預(yù)處理的任務(wù):
將輸入文件讀到內(nèi)存,并斷行;
替換注釋為單個(gè)空格;
Tokenization
將輸入轉(zhuǎn)換為一系列預(yù)處理Tokens
;處理
#import
、#include
將所引的庫,以遞歸的方式,插入到#import
或#include
所在的位置;替換宏定義;
條件編譯,根據(jù)條件包括或排除程序代碼的某些部分;
插入行標(biāo)記;
在預(yù)處理的輸出中,源文件名和行號(hào)信息會(huì)以# linenum filename flags
形式傳遞,這被稱為行標(biāo)記,代表著接下來的內(nèi)容開始于源文件filename
的第linenum
行,而flags
則會(huì)有0
或者多個(gè),有1
、2
、3
、4
;如果有多個(gè)flags
時(shí),彼此使用分號(hào)隔開。詳見此處。
每個(gè)標(biāo)識(shí)的表示內(nèi)容如下:
-
1
表示一個(gè)新文件的開始 -
2
表示返回文件(包含另一個(gè)文件后) -
3
表示以下文本來自系統(tǒng)頭文件,因此應(yīng)禁止某些警告 -
4
表示應(yīng)將以下文本視為包裝在隱式extern "C"
塊中。
比如# 10 "main.m" 2
,表示導(dǎo)入Person.h
文件后回到main.m
文件的第10行。
詞法分析
詞法分析屬于預(yù)處理部分,詞法分析的整個(gè)過程,主要是按照:標(biāo)識(shí)符、 數(shù)字、字符串文字、 標(biāo)點(diǎn)符號(hào),將我們的代碼分割成許多字符串序列,其中每個(gè)元素我們稱之為Token
,整個(gè)過程稱為Tokenization
。
終端輸入:
# -fmodules: Enable the 'modules' language feature
# -fsyntax-only, Run the preprocessor, parser and type checking stages
#-Xclang <arg>: Pass <arg> to the clang compiler
# -dump-tokens: Run preprocessor, dump internal rep of tokens
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
-fmodules
:啟用“模塊”語言功能。關(guān)于Modules
特性,詳見此處,大意為使用import
代替include
,編譯速度快。
-fsyntax-only
:運(yùn)行預(yù)處理器,解析器和類型檢查階段。
-Xclang <arg>
:傳遞參數(shù)到clang
的編譯器。
dump-tokens
:運(yùn)行預(yù)處理器,轉(zhuǎn)儲(chǔ)Token
的內(nèi)部表示。
更多關(guān)于Clang
參數(shù)的描述,請(qǐng)前往此處。
輸出結(jié)果:
....
int 'int' [StartOfLine] Loc=<main.m:11:1>
identifier 'main' [LeadingSpace] Loc=<main.m:11:5>
l_paren '(' Loc=<main.m:11:9>
int 'int' Loc=<main.m:11:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:11:14>
comma ',' Loc=<main.m:11:18>
const 'const' [LeadingSpace] Loc=<main.m:11:20>
char 'char' [LeadingSpace] Loc=<main.m:11:26>
star '*' [LeadingSpace] Loc=<main.m:11:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:11:33>
l_square '[' Loc=<main.m:11:37>
r_square ']' Loc=<main.m:11:38>
r_paren ')' Loc=<main.m:11:39>
l_brace '{' [LeadingSpace] Loc=<main.m:11:41>
at '@' [StartOfLine] [LeadingSpace] Loc=<main.m:12:5>
identifier 'autoreleasepool' Loc=<main.m:12:6>
l_brace '{' [LeadingSpace] Loc=<main.m:12:22>
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<main.m:14:9>
l_paren '(' Loc=<main.m:14:14>
at '@' Loc=<main.m:14:15>
string_literal '"Hello, World!"' Loc=<main.m:14:16>
r_paren ')' Loc=<main.m:14:31>
semi ';' Loc=<main.m:14:32>
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<main.m:16:9>
l_paren '(' Loc=<main.m:16:14>
at '@' Loc=<main.m:16:15>
string_literal '"%@"' Loc=<main.m:16:16>
comma ',' Loc=<main.m:16:20>
at '@' Loc=<main.m:16:21 <Spelling=main.m:10:20>>
string_literal '"你好,世界"' Loc=<main.m:16:21 <Spelling=main.m:10:21>>
r_paren ')' Loc=<main.m:16:31>
semi ';' Loc=<main.m:16:32>
identifier 'Person' [StartOfLine] [LeadingSpace] Loc=<main.m:18:9>
star '*' [LeadingSpace] Loc=<main.m:18:16>
identifier 'instance' Loc=<main.m:18:17>
equal '=' [LeadingSpace] Loc=<main.m:18:26>
l_square '[' [LeadingSpace] Loc=<main.m:18:28>
l_square '[' Loc=<main.m:18:29>
identifier 'Person' Loc=<main.m:18:30>
identifier 'alloc' [LeadingSpace] Loc=<main.m:18:37>
r_square ']' Loc=<main.m:18:42>
identifier 'init' Loc=<main.m:18:43>
r_square ']' Loc=<main.m:18:47>
semi ';' Loc=<main.m:18:48>
l_square '[' [StartOfLine] [LeadingSpace] Loc=<main.m:19:9>
identifier 'instance' Loc=<main.m:19:10>
identifier 'share' [LeadingSpace] Loc=<main.m:19:19>
r_square ']' Loc=<main.m:19:24>
semi ';' Loc=<main.m:19:25>
r_brace '}' [StartOfLine] [LeadingSpace] Loc=<main.m:20:5>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:21:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:21:12>
semi ';' Loc=<main.m:21:13>
r_brace '}' [StartOfLine] Loc=<main.m:22:1>
eof '' Loc=<main.m:22:2>
詞法分析中Token
包含信息(詳請(qǐng)見此處):
Sourece Location
:表示Token
開始的位置,比如:Loc=<main.m:11:5>
;Token Kind
:表示Token
的類型,比如:identifier
、numeric_constant
、string_literal
;Flags
:詞法分析器和處理器跟蹤每個(gè)Token
的基礎(chǔ),目前有四個(gè)Flag
分別是:
StartOfLine
:表示這是每行開始的第一個(gè)Token
;LeadingSpace
:當(dāng)通過宏擴(kuò)展Token
時(shí),在Token
之前有一個(gè)空格字符。該標(biāo)志的定義是依據(jù)預(yù)處理器的字符串化要求而進(jìn)行的非常嚴(yán)格地定義。DisableExpand
:該標(biāo)志在預(yù)處理器內(nèi)部使用,用來表示identifier
令牌禁用宏擴(kuò)展。NeedsCleaning
:如果令牌的原始拼寫包含三字符組或轉(zhuǎn)義的換行符,則設(shè)置此標(biāo)志。
語法分析(Parsing
)與語義分析
此階段對(duì)輸入文件進(jìn)行語法分析,將預(yù)處理器生成的Tokens
轉(zhuǎn)換為語法分析樹;一旦生成語法分析樹后,將會(huì)進(jìn)行語義分析,執(zhí)行類型檢查和代碼格式檢查。這個(gè)階段負(fù)責(zé)生成大多數(shù)編譯器警告以及語法分析過程的錯(cuò)誤。最終輸出AST
(抽象語法樹)。
Parser
的意義與作用
所謂 parser,一般是指把某種格式的文本(字符串)轉(zhuǎn)換成某種數(shù)據(jù)結(jié)構(gòu)的過程。最常見的 parser,是把程序文本轉(zhuǎn)換成編譯器內(nèi)部的一種叫做“抽象語法樹”(AST)的數(shù)據(jù)結(jié)構(gòu)。摘自對(duì) Parser 的誤解-王垠
AST
的示意圖(來源):
終端輸入:
# -fmodules: Enable the 'modules' language feature
# -fsyntax-only, Run the preprocessor, parser and type checking stages
#-Xclang <arg>: Pass <arg> to the clang compiler
# -ast-dump: Build ASTs and then debug dump them
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
輸出結(jié)果:
TranslationUnitDecl 0x7f80ea01c408 <<invalid sloc>> <invalid sloc> <undeserialized declarations>
|-TypedefDecl 0x7f80ea01cca0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7f80ea01c9a0 '__int128'
#...
# cutting out internal declarations of clang
#...
|-ImportDecl 0x7f80ea27d9d8 <main.m:8:1> col:1 implicit Foundation
|-ImportDecl 0x7f80ea27da18 <./Person.h:8:1> col:1 implicit Foundation
|-ObjCInterfaceDecl 0x7f80ea294ff8 <line:12:1, line:14:2> line:12:12 Person
| |-super ObjCInterface 0x7f80ea27db18 'NSObject'
| `-ObjCMethodDecl 0x7f80ea2951f0 <line:13:1, col:14> col:1 - share 'void'
`-FunctionDecl 0x7f80ea295620 <main.m:11:1, line:22:1> line:11:5 main 'int (int, const char **)'
|-ParmVarDecl 0x7f80ea2953b0 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7f80ea2954d0 <col:20, col:38> col:33 argv 'const char **':'const char **'
`-CompoundStmt 0x7f80ea29e5b8 <col:41, line:22:1>
|-ObjCAutoreleasePoolStmt 0x7f80ea29e570 <line:12:5, line:20:5>
| `-CompoundStmt 0x7f80ea29e540 <line:12:22, line:20:5>
| |-CallExpr 0x7f80ea2a26f0 <line:14:9, col:31> 'void'
| | |-ImplicitCastExpr 0x7f80ea2a26d8 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
| | | `-DeclRefExpr 0x7f80ea2a25e0 <col:9> 'void (id, ...)' Function 0x7f80ea295760 'NSLog' 'void (id, ...)'
| | `-ImplicitCastExpr 0x7f80ea2a2718 <col:15, col:16> 'id':'id' <BitCast>
| | `-ObjCStringLiteral 0x7f80ea2a2660 <col:15, col:16> 'NSString *'
| | `-StringLiteral 0x7f80ea2a2638 <col:16> 'char [14]' lvalue "Hello, World!"
| |-CallExpr 0x7f80ea298298 <line:16:9, col:31> 'void'
| | |-ImplicitCastExpr 0x7f80ea298280 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
| | | `-DeclRefExpr 0x7f80ea2a2730 <col:9> 'void (id, ...)' Function 0x7f80ea295760 'NSLog' 'void (id, ...)'
| | |-ImplicitCastExpr 0x7f80ea2982c8 <col:15, col:16> 'id':'id' <BitCast>
| | | `-ObjCStringLiteral 0x7f80ea2a27a8 <col:15, col:16> 'NSString *'
| | | `-StringLiteral 0x7f80ea2a2788 <col:16> 'char [3]' lvalue "%@"
| | `-ObjCStringLiteral 0x7f80ea298260 <line:10:20, col:21> 'NSString *'
| | `-StringLiteral 0x7f80ea298238 <col:21> 'char [16]' lvalue "\344\275\240\345\245\275\357\274\214\344\270\226\347\225\214"
| |-DeclStmt 0x7f80ea29e4a8 <line:18:9, col:48>
| | `-VarDecl 0x7f80ea298320 <col:9, col:47> col:17 used instance 'Person *' cinit
| | |-ObjCMessageExpr 0x7f80ea2988d0 <col:28, col:47> 'Person *' selector=init
| | | `-ObjCMessageExpr 0x7f80ea298658 <col:29, col:42> 'Person *' selector=alloc class='Person'
| | `-FullComment 0x7f80ea2a3900 <line:17:12, col:33>
| | `-ParagraphComment 0x7f80ea2a38d0 <col:12, col:33>
| | `-TextComment 0x7f80ea2a38a0 <col:12, col:33> Text=" MARK: 我也是注釋"
| `-ObjCMessageExpr 0x7f80ea29e510 <line:19:9, col:24> 'void' selector=share
| `-ImplicitCastExpr 0x7f80ea29e4f8 <col:10> 'Person *' <LValueToRValue>
| `-DeclRefExpr 0x7f80ea29e4c0 <col:10> 'Person *' lvalue Var 0x7f80ea298320 'instance' 'Person *'
`-ReturnStmt 0x7f80ea29e5a8 <line:21:5, col:12>
`-IntegerLiteral 0x7f80ea29e588 <col:12> 'int' 0
Clang
的AST
是從TranslationUnitDecl節(jié)點(diǎn)開始進(jìn)行遞歸遍歷的;AST
中許多重要的Node
,繼承自Type
、Decl
、DeclContext
、Stmt
。
-
Type :表示類型,比如
BuiltinType
-
Decl :表示一個(gè)聲明
declaration
或者一個(gè)定義definition
,比如:變量,函數(shù),結(jié)構(gòu)體,typedef
; -
DeclContext :用來聲明表示上下文的特定
decl
類型的基類; -
Stmt :表示一條陳述
statement
; -
Expr:在
Clang
的語法樹中也表示一條陳述statements
;
代碼優(yōu)化和生成
這個(gè)階段主要任務(wù)是將AST
轉(zhuǎn)換為底層中間的代碼LLVM IR
,并且最終生成機(jī)器碼;期間負(fù)責(zé)生成目標(biāo)架構(gòu)的代碼以及優(yōu)化生成的代碼。最終輸出.s
文件(匯編文件)。
LLVM IR
有三種格式:
- 文本格式:
.ll
文件 - 內(nèi)存中用以優(yōu)化自身時(shí),執(zhí)行檢查和修改的數(shù)據(jù)結(jié)構(gòu)(編譯過程中載入內(nèi)存的形式)
- 磁盤二進(jìn)制(
BitCode
)格式:.bc
文件
LLVM
提供了.ll
與.bc
相互轉(zhuǎn)換的工具:
-
llvm-as
:可將.ll
轉(zhuǎn)為.bc
-
llvm-dis
:可將.bc
轉(zhuǎn)為.ll
終端輸入:
# -S : Run LLVM generation and optimization stages and target-specific code generation,producing an assembly file
# -fobjc-arc : Synthesize retain and release calls for Objective-C pointers
# -emit-llvm : Use the LLVM representation for assembler and object files
# -o <file> : Write output to <file>
# 匯編表示成.ll文件 -fobjc-arc 可忽略,不作代碼優(yōu)化
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
# 目標(biāo)文件表示成 .bc 文件
# -c : Only run preprocess, compile, and assemble steps
clang -emit-llvm -c main.m -o main.bc
#.ll與.bc的相互轉(zhuǎn)換
llvm-as main.ll -o main.bc
llvm-dis main.bc -o main.ll
此處使用了參數(shù)-emit-llvm
,來查看LLVM IR
。
輸出結(jié)果:
# 此處只貼main函數(shù)部分
define i32 @main(i32 %0, i8** %1) #1 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
%6 = alloca %0*, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%7 = call i8* @llvm.objc.autoreleasePoolPush() #2
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.2 to i8*), %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.4 to %1*))
%8 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%9 = bitcast %struct._class_t* %8 to i8*
%10 = call i8* @objc_alloc_init(i8* %9)
%11 = bitcast i8* %10 to %0*
store %0* %11, %0** %6, align 8
%12 = load %0*, %0** %6, align 8
%13 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9
%14 = bitcast %0* %12 to i8*
call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %14, i8* %13)
%15 = bitcast %0** %6 to i8**
call void @llvm.objc.storeStrong(i8** %15, i8* null) #2
call void @llvm.objc.autoreleasePoolPop(i8* %7)
ret i32 0
}
代碼優(yōu)化
Clang
代碼優(yōu)化參數(shù)有-O0
、 -O1
、 -O2
、 -O3
、 -Ofast
、-Os
、 -Oz
、-Og
、 -O
、-O4
-
-O0
:表示沒有優(yōu)化;編譯速度最快并生成最可調(diào)試的代碼 -
-O1
:優(yōu)化程度介于-O0
~-O2
之間。 -
-O2
:適度的優(yōu)化水平,可實(shí)現(xiàn)最優(yōu)化 -
-O3
:與-O2
相似,不同之處在于它優(yōu)化的時(shí)間比較長,可能會(huì)生成更大的代碼 -
-O4
:當(dāng)前等效于-O3
-
-Ofast
:啟用-O3
中的所有優(yōu)化并且可能啟用一些激進(jìn)優(yōu)化 -
-Os
:與-O2
一樣,具有額外的優(yōu)化功能以減少代碼大小 -
-Oz
:類似于-Os
,進(jìn)一步減小了代碼大小 -
-Og
:類似-O1
-
-O
:相當(dāng)于-O2
終端輸入:
clang -S -O2 -fobjc-arc -emit-llvm main.m -o main.ll
輸出結(jié)果:
#LLVM IR文件頭信息
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
#結(jié)構(gòu)體的定義
%0 = type opaque
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
%struct._class_t = type { %struct._class_t*, %struct._class_t*, %struct._objc_cache*, i8* (i8*, i8*)**, %struct._class_ro_t* }
%struct._objc_cache = type opaque
%struct._class_ro_t = type { i32, i32, i32, i8*, i8*, %struct.__method_list_t*, %struct._objc_protocol_list*, %struct._ivar_list_t*, i8*, %struct._prop_list_t* }
%struct.__method_list_t = type { i32, i32, [0 x %struct._objc_method] }
%struct._objc_method = type { i8*, i8*, i8* }
%struct._objc_protocol_list = type { i64, [0 x %struct._protocol_t*] }
%struct._protocol_t = type { i8*, i8*, %struct._objc_protocol_list*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct._prop_list_t*, i32, i32, i8**, i8*, %struct._prop_list_t* }
%struct._ivar_list_t = type { i32, i32, [0 x %struct._ivar_t] }
%struct._ivar_t = type { i64*, i8*, i8*, i32, i32 }
%struct._prop_list_t = type { i32, i32, [0 x %struct._prop_t] }
%struct._prop_t = type { i8*, i8* }
# 全局變量、私有/外部/內(nèi)部常量的定義或聲明
@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [14 x i8] c"Hello, World!\00", section "__TEXT,__cstring,cstring_literals", align 1
# --全局結(jié)構(gòu)體定義與初始化
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i64 13 }, section "__DATA,__cfstring", align 8 #0
@.str.1 = private unnamed_addr constant [3 x i8] c"%@\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_.2 = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i32 0, i32 0), i64 2 }, section "__DATA,__cfstring", align 8 #0
@.str.3 = private unnamed_addr constant [6 x i16] [i16 20320, i16 22909, i16 -244, i16 19990, i16 30028, i16 0], section "__TEXT,__ustring", align 2
@_unnamed_cfstring_.4 = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 2000, i8* bitcast ([6 x i16]* @.str.3 to i8*), i64 5 }, section "__DATA,__cfstring", align 8 #0
@"OBJC_CLASS_$_Person" = external global %struct._class_t
@"OBJC_CLASSLIST_REFERENCES_$_" = internal global %struct._class_t* @"OBJC_CLASS_$_Person", section "__DATA,__objc_classrefs,regular,no_dead_strip", align 8
@OBJC_METH_VAR_NAME_ = private unnamed_addr constant [6 x i8] c"share\00", section "__TEXT,__objc_methname,cstring_literals", align 1
@OBJC_SELECTOR_REFERENCES_ = internal externally_initialized global i8* getelementptr inbounds ([6 x i8], [6 x i8]* @OBJC_METH_VAR_NAME_, i64 0, i64 0), section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip", align 8
@llvm.compiler.used = appending global [3 x i8*] [i8* bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8*), i8* getelementptr inbounds ([6 x i8], [6 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), i8* bitcast (i8** @OBJC_SELECTOR_REFERENCES_ to i8*)], section "llvm.metadata"
# main函數(shù)的入口:`dso_local`:main函數(shù)解析為統(tǒng)一鏈接單元的符號(hào),而非外部替換的符號(hào)
; Function Attrs: ssp uwtable
define dso_local i32 @main(i32 %0, i8** nocapture readnone %1) local_unnamed_addr #1 {
%3 = tail call i8* @llvm.objc.autoreleasePoolPush() #2
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*)), !clang.arc.no_objc_arc_exceptions !8
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.2 to i8*), %0* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.4 to %0*)), !clang.arc.no_objc_arc_exceptions !8
%4 = load i8*, i8** bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8**), align 8
%5 = tail call i8* @objc_alloc_init(i8* %4), !clang.arc.no_objc_arc_exceptions !8
%6 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
tail call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %5, i8* %6), !clang.arc.no_objc_arc_exceptions !8
tail call void @llvm.objc.release(i8* %5) #2, !clang.imprecise_release !8
tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #2
ret i32 0
}
#函數(shù)聲明
; Function Attrs: nounwind
declare i8* @llvm.objc.autoreleasePoolPush() #2
declare void @NSLog(i8*, ...) local_unnamed_addr #3
declare i8* @objc_alloc_init(i8*) local_unnamed_addr
; Function Attrs: nonlazybind
declare i8* @objc_msgSend(i8*, i8*, ...) local_unnamed_addr #4
; Function Attrs: nounwind
declare void @llvm.objc.release(i8*) #2
; Function Attrs: nounwind
declare void @llvm.objc.autoreleasePoolPop(i8*) #2
#屬性組
attributes #0 = { "objc_arc_inert" }
attributes #1 = { ssp uwtable "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { nounwind }
attributes #3 = { "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #4 = { nonlazybind }
#該`module`的元數(shù)據(jù)
##命名元數(shù)據(jù)
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.ident = !{!7}
##未命名的元數(shù)據(jù)
!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!3 = !{i32 1, !"Objective-C Garbage Collection", i8 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"clang version 12.0.0"}
!8 = !{}
淺析 LLVM IR
Module:
LLVM
程序是由Module
組成的,每個(gè)Module
是輸入程序的翻譯單元。每個(gè)Module
都是由functions
、global variables
和symbol table entries
組成。Module
會(huì)通過LLVM
鏈接器組合到一起,鏈接器會(huì)合并函數(shù)以及全局變量的定義,解決前置聲明以及合并符號(hào)表。Target Datalayout:
Module
需要以字符串的形式指定特定于目標(biāo)架構(gòu)的數(shù)據(jù)布局方式,該字符串指定如何在內(nèi)存中布局?jǐn)?shù)據(jù)。如:target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
。-
元數(shù)據(jù):
LLVM IR
允許元數(shù)據(jù)被附加到能夠傳遞代碼額外信息給優(yōu)化器和代碼生成器的程序指令上。所有元數(shù)據(jù)在語法上均由!
標(biāo)識(shí)。元數(shù)據(jù)的兩個(gè)原語:元數(shù)據(jù)字符串和元數(shù)據(jù)節(jié)點(diǎn)。- 元數(shù)據(jù)字符串:用
""
引起來的字符串,以!
作為前綴。如:!"clang version 12.0.0"
- 元數(shù)據(jù)節(jié)點(diǎn):用
{}
括起來,使用,
隔開多個(gè)元素,以!
作為前綴。如:!{i32 7, !"PIC Level", i32 2}
- 元數(shù)據(jù)字符串:用
-
命名元數(shù)據(jù):是元數(shù)據(jù)節(jié)點(diǎn)的集合
; Some unnamed metadata nodes, which are referenced by the named metadata. !0 = !{!"zero"} !1 = !{!"one"} !2 = !{!"two"} ; A named metadata. !name = !{!0, !1, !2}
-
Linkage Types:所有全局變量和函數(shù)都具有鏈接類型,如上述
IR
中的:-
external
:module
外部可用 -
private
:module
內(nèi)部可用 -
appending
:僅應(yīng)用于數(shù)組類型的全局變量的指針。當(dāng)兩個(gè)使用了appending
的全局變量鏈接到一起的時(shí)候,這兩個(gè)全局的數(shù)組會(huì)被拼接到一起。 -
internal
:與private
相似,但該值在目標(biāo)文件中顯示為本地符號(hào)。與C
語言中static
關(guān)鍵字的概念相對(duì)應(yīng)。
-
屬性組:屬性組是
IR
中的對(duì)象(函數(shù)、全局變量)引用的屬性組合。它們對(duì)于保持.ll文件的可讀性很重要,因?yàn)樵S多函數(shù)將使用相同的屬性集。如上述IR
中的#0~#4
。-
函數(shù)屬性:被用來傳遞一個(gè)函數(shù)附加信息。函數(shù)屬性被認(rèn)為是函數(shù)的一部分,而不是函數(shù)類型,所以不同的函數(shù)屬性可以有相同的函數(shù)類型。上述
IR
中用到最多的函數(shù)屬性:-
nounwind
:表示函數(shù)不會(huì)拋出異常 -
nonlazybind
:阻止函數(shù)中某些符號(hào)的延遲綁定。
-
-
參數(shù)屬性:函數(shù)的返回類型以及每個(gè)參數(shù)都有與之關(guān)聯(lián)的參數(shù)屬性集合。被用來傳遞一個(gè)函數(shù)的返回值與參數(shù)的附加信息。參數(shù)屬性是函數(shù)的一部分,而不是函數(shù)類型,所以有不同參數(shù)屬性的函數(shù)可以有相同的參數(shù)類型。上述
IR
中用到最多的參數(shù)屬性:-
nocapture
:表示函數(shù)調(diào)用不會(huì)捕獲參數(shù)的指針,這個(gè)屬性對(duì)于返回值是無效的,僅適用于參數(shù)。 -
readnone
:應(yīng)用于參數(shù)表示函數(shù)不會(huì)取消對(duì)此參數(shù)指針的引用。
-
-
-
@
為全局標(biāo)識(shí)符。以其開頭標(biāo)識(shí)函數(shù),全局變量; -
%
為本地標(biāo)識(shí)符。以其開頭標(biāo)識(shí)寄存器名稱,類型; - 標(biāo)識(shí)符的不同的格式:
- 命名值,表示為以上述標(biāo)識(shí)符為前綴的字符串,如:
%struct._ivar_t
、@.str
- 未命名值,表示為以上述標(biāo)識(shí)為前綴的無符號(hào)的數(shù)值,如:
%0
、%1
- 命名值,表示為以上述標(biāo)識(shí)符為前綴的字符串,如:
-
-
#語法 %T1 = type { <type list> } ; Identified normal struct type %T2 = type <{ <type list> }> ; Identified packed struct type #表示結(jié)構(gòu)體的對(duì)齊方式為1字節(jié) #示例 {i32, i32} %mytype = type { %mytype*, i32 }
-
#語法 [<# elements> x <elementtype>] #語義 `elements`是個(gè)`integer`的值;`elementtype`是任意有大小的類型 #示例 [40 x i32] Array of 40 32-bit integer values
-
#語法 @<GlobalVarName> = [Linkage] [PreemptionSpecifier] [Visibility] [DLLStorageClass] [ThreadLocal] [(unnamed_addr|local_unnamed_addr)] [AddrSpace] [ExternallyInitialized] <global | constant> <Type> [<InitializerConstant>] [, section "name"] [, comdat [($name)]] [, align <Alignment>] (, !name !N)* #示例 @G = external global i32 #just declare @G = external global i32 8 #InitializerConstant
-
global constant
:表示該變量的內(nèi)容將永遠(yuǎn)不會(huì)被修改。 -
unnamed_addr
:表示該變量的地址并不重要,僅指示內(nèi)容。 -
local_unnamed_addr
:表示變量的地址在module
內(nèi)并不重要。
-
-
Runtime Preemption Specifiers:運(yùn)行時(shí)搶占說明符。全局變量,函數(shù)和別名可以具有一個(gè)可選的運(yùn)行時(shí)搶占說明符。如果未明確指定搶占說明符,則假定該符號(hào)為
dso_preemptable
。-
dso_preemptable
:表示函數(shù)或者變量在運(yùn)行時(shí)會(huì)被外部的鏈接單元替換 -
dso_local
:表示函數(shù)或變量將解析為同一鏈接單元中的符號(hào)。即使定義不在此編譯單元內(nèi),也將生成直接訪問
-
-
call:代表一個(gè)簡(jiǎn)單的函數(shù)調(diào)用;
#語法 <result> = [tail | musttail | notail ] call [fast-math flags] [cconv] [ret attrs] [addrspace(<num>)] <ty>|<fnty> <fnptrval>(<function args>) [fn attrs] [ operand bundles ]
可選的
tail
和musttail
標(biāo)記優(yōu)化器應(yīng)執(zhí)行尾部調(diào)用優(yōu)化notail
標(biāo)記用于防止執(zhí)行尾部調(diào)用優(yōu)化
-
ret:該指令表示函數(shù)返回
#語法 ret <type> <value> ; Return a value from a non-void function ret void ; Return from void function #示例 ret i32 5 ; Return an integer value of 5 ret void ; Return from a void function ret { i32, i8 } { i32 4, i8 2 } ; Return a struct of values 4 and 2
-
bitcast...to:
bitcast
將value
的類型轉(zhuǎn)換為類型ty2
而不改變它的任何位bits
#語法 <result> = bitcast <ty> <value> to <ty2> ; yields ty2
其他:
i32
:代表32-bit
的整數(shù),i8
:代表8-bit
的整數(shù);
代碼生成
生成目標(biāo)架構(gòu)的匯編代碼。
終端輸入:
#生成目標(biāo)架構(gòu)的匯編代碼
clang -S -fobjc-arc main.m -o main.s
輸出結(jié)果:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15, 6
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp #將%rbp的內(nèi)容壓棧,保存棧幀到%rsp中
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp # 將棧指針傳送至%rbp中,設(shè)置當(dāng)前棧幀
.cfi_def_cfa_register %rbp
subq $32, %rsp # 棧指針 - 32 (申請(qǐng)32個(gè)字節(jié)的空間)
movl $0, -4(%rbp)# 將 0 傳送至存儲(chǔ)器中,存儲(chǔ)器位置為: M[-4 + %rbp]
movl %edi, -8(%rbp) # 將%edi的內(nèi)容 傳送至存儲(chǔ)器中,存儲(chǔ)器位置為: M[-8 + %rbp]
movq %rsi, -16(%rbp)# 將%rsi的內(nèi)容 傳送至存儲(chǔ)器中,存儲(chǔ)器位置為: M[-16 + %rbp]
callq _objc_autoreleasePoolPush #調(diào)用_objc_autoreleasePoolPush
leaq L__unnamed_cfstring_(%rip), %rcx #將`L__unnamed_cfstring_(%rip)`的有效地址寫入`%rcx`中
movq %rcx, %rdi # 將%rcx的內(nèi)容 傳送至寄存器%rdi
movq %rax, -32(%rbp) ## 8-byte Spill # 將%rax的內(nèi)容 傳送至存儲(chǔ)器中,存儲(chǔ)器位置為: M[-32 + %rbp]
movb $0, %al # 將立即數(shù)0 傳送至寄存器的低八位的單字節(jié)寄存器`%al`中
callq _NSLog #調(diào)用 _NSLog
leaq L__unnamed_cfstring_.2(%rip), %rcx
leaq L__unnamed_cfstring_.4(%rip), %rdx
movq %rcx, %rdi # 將%rcx的內(nèi)容 傳送至寄存器%rdi
movq %rdx, %rsi #將%rdx的內(nèi)容 傳送至寄存器%rsi
movb $0, %al # 將立即數(shù)0 傳送至寄存器的低八位的單字節(jié)寄存器`%al`中
callq _NSLog #調(diào)用 _NSLog
movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx
movq %rcx, %rdi
callq _objc_alloc_init
movq %rax, -24(%rbp)
movq -24(%rbp), %rax
movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq %rax, %rdi
callq *_objc_msgSend@GOTPCREL(%rip)
xorl %r8d, %r8d # 使用異或?qū)拇嫫鱜%r8d`清0
movl %r8d, %esi
leaq -24(%rbp), %rax
movq %rax, %rdi
callq _objc_storeStrong
movq -32(%rbp), %rdi ## 8-byte Reload
callq _objc_autoreleasePoolPop
xorl %eax, %eax # 使用異或?qū)拇嫫鱜%eax`清0
addq $32, %rsp
popq %rbp #將%rbp的內(nèi)容彈出棧
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello, World!"
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_
L__unnamed_cfstring_:
.quad ___CFConstantStringClassReference
.long 1992 ## 0x7c8
.space 4
.quad L_.str
.quad 13 ## 0xd
.section __TEXT,__cstring,cstring_literals
L_.str.1: ## @.str.1
.asciz "%@"
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_.2
L__unnamed_cfstring_.2:
.quad ___CFConstantStringClassReference
.long 1992 ## 0x7c8
.space 4
.quad L_.str.1
.quad 2 ## 0x2
.section __TEXT,__ustring
.p2align 1 ## @.str.3
l_.str.3:
.short 20320 ## 0x4f60
.short 22909 ## 0x597d
.short 65292 ## 0xff0c
.short 19990 ## 0x4e16
.short 30028 ## 0x754c
.short 0 ## 0x0
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_.4
L__unnamed_cfstring_.4:
.quad ___CFConstantStringClassReference
.long 2000 ## 0x7d0
.space 4
.quad l_.str.3
.quad 5 ## 0x5
.section __DATA,__objc_classrefs,regular,no_dead_strip
.p2align 3 ## @"OBJC_CLASSLIST_REFERENCES_$_"
_OBJC_CLASSLIST_REFERENCES_$_:
.quad _OBJC_CLASS_$_Person
.section __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_: ## @OBJC_METH_VAR_NAME_
.asciz "share"
.section __DATA,__objc_selrefs,literal_pointers,no_dead_strip
.p2align 3 ## @OBJC_SELECTOR_REFERENCES_
_OBJC_SELECTOR_REFERENCES_:
.quad L_OBJC_METH_VAR_NAME_
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
匯編指令
所有以.
開頭的行,都是指導(dǎo)編譯器與鏈接器的命令。
-
.section
指定匯編器將生成的匯編代碼,寫入對(duì)應(yīng)的區(qū)section
。語法:
.section segname , sectname [[[ , type ] , attribute ] , sizeof_stub ]
示例:
#`regular`類型 表示該區(qū)存放程序指令或初始化數(shù)據(jù) #`pure_instructions`屬性 表示此區(qū)僅包含機(jī)器指令 .section __TEXT,__text,regular,pure_instructions #`cstring_literals`類型 表示該區(qū)存放以null結(jié)尾的c字符串 .section __TEXT,__cstring,cstring_literals
.global symbol_name
標(biāo)記符號(hào)為外部符號(hào);-
.align
對(duì)齊指令,指定匯編代碼的對(duì)齊方式語法:
.align align_expression [ , 1byte_fill_expression [,max_bytes_to_fill]] .p2align align_expression [ , 1byte_fill_expression [,max_bytes_to_fill]] .p2alignw align_expression [ , 2byte_fill_expression [,max_bytes_to_fill]] .p2alignl align_expression [ , 4byte_fill_expression [,max_bytes_to_fill]] .align32 align_expression [ , 4byte_fill_expression [,max_bytes_to_fill]]
示例:
# 以16(2^4)字節(jié)的方式對(duì)齊,不足的使用0x90補(bǔ)齊 .p2align 4, 0x90
-
CFA
在棧上分配的內(nèi)存區(qū)域,稱為“調(diào)用幀”。調(diào)用幀由棧上的地址標(biāo)識(shí)。我們將此地址稱為CFA(
Canonical Frame Address
)。通常,將CFA
定義為前一幀調(diào)用者上的棧指針的值(可能與當(dāng)前幀的值不同)。An area of memory that is allocated on a stack called a “call frame.” The call frame is identified by an address on the stack. We refer to this address as the Canonical Frame Address or CFA. Typically, the CFA is defined to be the value of the stack pointer at the call site in the previous frame (which may be different from its value on entry to the current frame).引自DWARF規(guī)范-6.4
.cfi_def_cfa_offset OFFSET
:cfi_def_cfa_offset
指令用來修改計(jì)算CFA
的規(guī)則。注意:OFFSET
是絕對(duì)偏移量,它會(huì)被加到幀指針寄存%ebp
或者%rbp
上,重新計(jì)算CFA
的地址。.cfi_def_cfa REGISTER, OFFSET
:cfi_def_cfa
這個(gè)指令從寄存器中獲取地址并且加上這個(gè)OFFSET
。-
.cfi_def_cfa_register REGISTER
:cfi_def_cfa_register
這個(gè)指令讓%ebp
或%rbp
被設(shè)置為新值且偏移量保持不變。上述設(shè)置只是為了用來輔助調(diào)試的,比如打斷點(diǎn),獲取調(diào)用堆棧信息。
-
CFI
調(diào)用幀信息,英文全稱:
Call Frame Information
。-
cfi_startproc
,表示函數(shù)或過程開始。 -
.cfi_endproc
,表示函數(shù)或過程結(jié)束。
-
更多細(xì)節(jié)請(qǐng)查看蘋果官網(wǎng)
匯編器
這個(gè)階段主要任務(wù)是運(yùn)行目標(biāo)架構(gòu)的匯編程序(匯編器),將編譯器的輸出轉(zhuǎn)換為目標(biāo)架構(gòu)的目標(biāo)(object
)文件,即:.o
文件。
終端輸入:
# -c : Run all of the above, plus the assembler, generating a target ".o" object file.
# -o : write to file
clang -c main.m -o main.o
clang -c Person.m -o person.o
輸出結(jié)果:
#使用命令查看生成文件
#file main.o person.o
#輸出
main.o: Mach-O 64-bit object x86_64
person.o: Mach-O 64-bit object x86_64
通過匯編器將可讀的匯編代碼,轉(zhuǎn)換為目標(biāo)架構(gòu)的目標(biāo)文件,最終輸出.o
文件,也稱機(jī)器碼。
鏈接器
這個(gè)階段會(huì)運(yùn)行目標(biāo)架構(gòu)的鏈接器,將多個(gè)object
文件合并成一個(gè)可執(zhí)行文件或動(dòng)態(tài)庫。最終的輸出a.out
、.dylib
或.so
。
在上述OC
代碼示例中,Main
函數(shù)中引用了Person
類,因此若要生成可執(zhí)行的文件,需要將main.o
與person.o
進(jìn)行鏈接
終端輸入:
# no stage selection option
# If no stage selection option is specified, all stages above are run, and the
# linker is run to combine the results into an executable or shared library.
clang main.o person.o -o main
輸出結(jié)果:
"_NSLog", referenced from:
_main in main.o
-[Person share] in person.o
"_OBJC_CLASS_$_NSObject", referenced from:
_OBJC_CLASS_$_Person in person.o
"_OBJC_METACLASS_$_NSObject", referenced from:
_OBJC_METACLASS_$_Person in person.o
"___CFConstantStringClassReference", referenced from:
CFString in main.o
CFString in main.o
CFString in main.o
CFString in person.o
"__objc_empty_cache", referenced from:
_OBJC_METACLASS_$_Person in person.o
_OBJC_CLASS_$_Person in person.o
"_objc_alloc_init", referenced from:
_main in main.o
"_objc_autoreleasePoolPop", referenced from:
_main in main.o
"_objc_autoreleasePoolPush", referenced from:
_main in main.o
"_objc_msgSend", referenced from:
_main in main.o
ld: symbol(s) not found for architecture x86_64
clang-12: error: linker command failed with exit code 1 (
鏈接器未找到上述的符號(hào),原因是我們代碼引入了Foundation
庫,在生成可執(zhí)行文件時(shí),未進(jìn)行鏈接。
在解決這個(gè)問題之前先介紹一下工具xcrun
,使用xcrun
可以從命令行定位和調(diào)用開發(fā)者工具
#--show-sdk-path : show selected SDK install path
xcrun --show-sdk-path
# 輸出`MacOSX.sdk`的路徑
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
基于此路徑鏈接我們的Foundation
庫:
# -Wl,<arg> Pass the comma separated arguments in <arg> to the linker #傳參給鏈接器
# `xcrun --show-sdk-path` 等同 $(xcrun --show-sdk-path) 視為命令替換
clang main.o person.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation -o main
最終輸出如下圖:
執(zhí)行這個(gè)可執(zhí)行文件:
#執(zhí)行
./main
#輸出
2021-05-08 17:40:45.134 main[30561:1257231] Hello, World!
2021-05-08 17:40:45.135 main[30561:1257231] 你好,世界
2021-05-08 17:40:45.135 main[30561:1257231] 持之以恒
main
文件查看:
file main
#輸出
main: Mach-O 64-bit executable x86_64
符號(hào)對(duì)比
符號(hào)表查看工具nm
,允許我們查看Object
文件的符號(hào)表內(nèi)容。
-
使用
nm
終端工具,先觀察一下mian.o
和person.o
#輸入 nm -nm main.o person.o #輸出 (undefined) external _NSLog (undefined) external _OBJC_CLASS_$_Person (undefined) external ___CFConstantStringClassReference (undefined) external _objc_alloc_init (undefined) external _objc_autoreleasePoolPop (undefined) external _objc_autoreleasePoolPush (undefined) external _objc_msgSend 0000000000000000 (__TEXT,__text) external _main 00000000000000e8 (__TEXT,__ustring) non-external l_.str.3 00000000000000f8 (__DATA,__objc_classrefs) non-external _OBJC_CLASSLIST_REFERENCES_$_ 0000000000000108 (__DATA,__objc_selrefs) non-external _OBJC_SELECTOR_REFERENCES_ (undefined) external _NSLog (undefined) external _OBJC_CLASS_$_NSObject (undefined) external _OBJC_METACLASS_$_NSObject (undefined) external ___CFConstantStringClassReference (undefined) external __objc_empty_cache 0000000000000000 (__TEXT,__text) non-external -[Person share] 0000000000000024 (__TEXT,__ustring) non-external l_.str 0000000000000058 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_Person 00000000000000a0 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_Person 00000000000000c0 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_Person 0000000000000108 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person 0000000000000130 (__DATA,__objc_data) external _OBJC_CLASS_$_Person
external
表示該符號(hào)針對(duì)當(dāng)前目標(biāo)文件不是私有的,與non-external
相反。undefined
表示該符號(hào)未找到。 -
使用
nm
觀察一下可執(zhí)行文件main
的符號(hào)表#輸入 nm -nm main #輸出 (undefined) external _NSLog (from Foundation) (undefined) external _OBJC_CLASS_$_NSObject (from libobjc) (undefined) external _OBJC_METACLASS_$_NSObject (from libobjc) (undefined) external ___CFConstantStringClassReference (from CoreFoundation) (undefined) external __objc_empty_cache (from libobjc) (undefined) external _objc_alloc_init (from libobjc) (undefined) external _objc_autoreleasePoolPop (from libobjc) (undefined) external _objc_autoreleasePoolPush (from libobjc) (undefined) external _objc_msgSend (from libobjc) (undefined) external dyld_stub_binder (from libSystem) 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000100003e80 (__TEXT,__text) external _main #私有符號(hào) 0000000100003f00 (__TEXT,__text) non-external -[Person share] 0000000100008020 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_Person 0000000100008068 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_Person 0000000100008088 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_Person #非私有 00000001000080e0 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person 0000000100008108 (__DATA,__objc_data) external _OBJC_CLASS_$_Person #私有符號(hào) 0000000100008130 (__DATA,__data) non-external __dyld_private
可以發(fā)現(xiàn)在經(jīng)過鏈接器處理后,為每個(gè)符號(hào)增加了來源。當(dāng)我們運(yùn)行可執(zhí)行文件時(shí),會(huì)由動(dòng)態(tài)鏈接器dyld
通過這些來源對(duì)處于undifined
的符號(hào)進(jìn)行解析,比如_NSLog
,來自Foundation
,在運(yùn)行時(shí)會(huì)在Foundation
中找到指向它的函數(shù)地址,并最終調(diào)用執(zhí)行。
系統(tǒng)符號(hào)
目標(biāo)文件的顯示工具otool
,可以查看Mach-O
文件特定Section
和Segment
的內(nèi)容。
-
可執(zhí)行文件是知道它需要鏈接那些庫的
# -L :display the names and version numbers of the shared libraries that the object file uses otool -L main # 輸出 main: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1677.104.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1) /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1677.104.0) /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
上述輸出我們發(fā)現(xiàn)在鏈接器生成可執(zhí)行文件時(shí),我們通過
-Wl
傳遞給鏈接器的Foundation
的路徑與可執(zhí)行文件最終鏈接的Foundation
路徑不一致。參數(shù)路徑下的文件內(nèi)容:
image.png -
.tbd
文件the .tbd files are new "text-based stub libraries", that provide a much more compact version of the stub libraries for use in the SDK, and help to significantly reduce its download size. 引自stackoverflow
.tbd
是個(gè)文本文件,提供的是SDK
的更簡(jiǎn)潔版本,明顯的降低Xcode
的下載大小,具體內(nèi)容:.tbd文件內(nèi)容.tbd
文件包含了與文件本身相關(guān)的元數(shù)據(jù),與架構(gòu)相關(guān)的信息,還有Foundation
庫針對(duì)特定架構(gòu)的symbols
,以及該庫所依賴的庫。并指定了Foundation
庫的最終安裝路徑。Foundation -
查看系統(tǒng)符號(hào)
#輸入 nm -nm /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep '_NSLog' #輸出NSlog的調(diào)用地址 000000000004ce6e (__TEXT,__text) external _NSLog
總結(jié)
OC
代碼編譯時(shí),首先會(huì)經(jīng)過預(yù)處理,接著進(jìn)行詞法分析將文本字符串Token
化, 再通過語法與語義分析檢查代碼的類型與格式,最終生成AST
,并在代碼優(yōu)化與生成階段,將AST
轉(zhuǎn)換為底層的中間代碼LLVM IR
,并最終生成目標(biāo)架構(gòu)的匯編代碼,交給匯編器進(jìn)行處理后,將可讀的匯編代碼轉(zhuǎn)換為目標(biāo)架構(gòu)的機(jī)器碼,即:.O
文件,通過鏈接器,解決.O
文件與庫的鏈接問題,最終根據(jù)特定的機(jī)器架構(gòu)生成可執(zhí)行文件。
參考資料
http://www.aosabook.org/en/llvm.html
https://en.m.wikibooks.org/wiki/GNU_C_Compiler_Internals/GNU_C_Compiler_Architecture