編譯器與Clang編譯過程

前言

編譯的主要任務(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)度炫七。
傳統(tǒng)編譯器的架構(gòu)

這種架構(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è)編譯器乍桂。

編譯器架構(gòu)分析

這種傳統(tǒng)編譯器的架構(gòu)有三個(gè)成功的案例:

  1. Java.Net虛擬機(jī);它們都提供了對(duì)JIT編譯器和運(yùn)行時(shí)的支持效床,并且還定義了字節(jié)碼的格式(bytecode)睹酌,這意味著任何可以編譯為字節(jié)碼的語言,都可以復(fù)用優(yōu)化器和JIT(動(dòng)態(tài)編譯)和運(yùn)行時(shí)能力剩檀。
  2. 將輸入源轉(zhuǎn)換為C代碼(或其他某種語言)并通過現(xiàn)有的C編譯器編譯
  3. 這種模式的最終成功實(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-CJava秒际、Go等悬赏。

GCC編譯流程

使用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)

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)換為另一種SSAstatic 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ù)的集合茴丰。名稱LLVMLow 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起源于2000Vikram AdveChris 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)今macOSiOS開發(fā)工具的一部分。
  • LLVM對(duì)產(chǎn)業(yè)的貢獻(xiàn)茬射,2012年獲得了ACM軟件系統(tǒng)獎(jiǎng)鹦蠕。獲得該獎(jiǎng)項(xiàng)的有UnixJava在抛、TCP/IP钟病、DNSMach
  • 201910月開始刚梭,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流程

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架構(gòu)

LLVM廣義是指LLVM整個(gè)架構(gòu),狹義指Clang編譯器的后端晰搀。

Clang

ClangLLVM的子項(xiàng)目五辽,是CC++外恕、Objective C語言的編譯器的前端杆逗。Clang編譯Objective-C代碼時(shí)速度為GCC3倍。詳見維基百科鳞疲。

Clang編譯過程

下面是一個(gè)基于簡(jiǎn)單的OC工程罪郊,不依賴Xcode,而是使用終端編譯的例子尚洽。

編譯前工程源代碼主要分為main.mPerson.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è)削樊,有12兔毒、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_constantstring_literal鸡典;

  • Flags:詞法分析器和處理器跟蹤每個(gè)Token的基礎(chǔ),目前有四個(gè)Flag分別是:

  1. StartOfLine:表示這是每行開始的第一個(gè)Token枪芒;

  2. LeadingSpace:當(dāng)通過宏擴(kuò)展Token時(shí)彻况,在Token之前有一個(gè)空格字符。該標(biāo)志的定義是依據(jù)預(yù)處理器的字符串化要求而進(jìn)行的非常嚴(yán)格地定義舅踪。

  3. DisableExpand:該標(biāo)志在預(yù)處理器內(nèi)部使用纽甘,用來表示identifier令牌禁用宏擴(kuò)展。

  4. 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的示意圖(來源):

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

ClangAST是從TranslationUnitDecl節(jié)點(diǎn)開始進(jìn)行遞歸遍歷的糕再;AST中許多重要的Node,繼承自Type玉转、Decl突想、DeclContextStmt冤吨。

  • 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í)間比較長(zhǎng)姻氨,可能會(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

  • ModuleLLVM程序是由Module組成的功戚,每個(gè)Module是輸入程序的翻譯單元娶眷。每個(gè)Module都是由functionsglobal variablessymbol table entries組成啸臀。Module會(huì)通過LLVM鏈接器組合到一起届宠,鏈接器會(huì)合并函數(shù)以及全局變量的定義,解決前置聲明以及合并符號(hào)表乘粒。

  • Target DatalayoutModule需要以字符串的形式指定特定于目標(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)旦棉。

    1. 元數(shù)據(jù)字符串:用""引起來的字符串齿风,以!作為前綴诉探。如:!"clang version 12.0.0"
    2. 元數(shù)據(jù)節(jié)點(diǎn):用{}括起來竹宋,使用,隔開多個(gè)元素仍源,以!作為前綴哗总。如:!{i32 7, !"PIC Level", i32 2}
  • 命名元數(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中的:

    1. external:module外部可用
    2. private:module內(nèi)部可用
    3. appending:僅應(yīng)用于數(shù)組類型的全局變量的指針脸候。當(dāng)兩個(gè)使用了appending的全局變量鏈接到一起的時(shí)候穷娱,這兩個(gè)全局的數(shù)組會(huì)被拼接到一起。
    4. 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ù)屬性:

    1. nounwind:表示函數(shù)不會(huì)拋出異常
    2. nonlazybind:阻止函數(shù)中某些符號(hào)的延遲綁定母谎。
  • 參數(shù)屬性:函數(shù)的返回類型以及每個(gè)參數(shù)都有與之關(guān)聯(lián)的參數(shù)屬性集合。被用來傳遞一個(gè)函數(shù)的返回值與參數(shù)的附加信息京革。參數(shù)屬性是函數(shù)的一部分奇唤,而不是函數(shù)類型,所以有不同參數(shù)屬性的函數(shù)可以有相同的參數(shù)類型匹摇。上述IR中用到最多的參數(shù)屬性:

    1. nocapture:表示函數(shù)調(diào)用不會(huì)捕獲參數(shù)的指針咬扇,這個(gè)屬性對(duì)于返回值是無效的,僅適用于參數(shù)廊勃。
    2. readnone:應(yīng)用于參數(shù)表示函數(shù)不會(huì)取消對(duì)此參數(shù)指針的引用懈贺。
  • 標(biāo)識(shí)符

    1. @為全局標(biāo)識(shí)符。以其開頭標(biāo)識(shí)函數(shù)坡垫,全局變量梭灿;
    2. %為本地標(biāo)識(shí)符。以其開頭標(biāo)識(shí)寄存器名稱冰悠,類型堡妒;
    3. 標(biāo)識(shí)符的不同的格式:
      1. 命名值,表示為以上述標(biāo)識(shí)符為前綴的字符串屿脐,如:%struct._ivar_t涕蚤、@.str
      2. 未命名值宪卿,表示為以上述標(biāo)識(shí)為前綴的無符號(hào)的數(shù)值,如:%0万栅、%1
  • 結(jié)構(gòu)體的定義:

    #語法
    %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 }
    
  • 數(shù)組的定義:

    #語法
    [<# 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
    
    1. global constant:表示該變量的內(nèi)容將永遠(yuǎn)不會(huì)被修改。
    2. unnamed_addr:表示該變量的地址并不重要烦粒,僅指示內(nèi)容休溶。
    3. local_unnamed_addr:表示變量的地址在module內(nèi)并不重要。
  • Runtime Preemption Specifiers:運(yùn)行時(shí)搶占說明符扰她。全局變量兽掰,函數(shù)和別名可以具有一個(gè)可選的運(yùn)行時(shí)搶占說明符。如果未明確指定搶占說明符徒役,則假定該符號(hào)為dso_preemptable孽尽。

    1. dso_preemptable:表示函數(shù)或者變量在運(yùn)行時(shí)會(huì)被外部的鏈接單元替換
    2. 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 ]
    
    1. 可選的tailmusttail標(biāo)記優(yōu)化器應(yīng)執(zhí)行尾部調(diào)用優(yōu)化

    2. 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...tobitcastvalue 的類型轉(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

    1. .cfi_def_cfa_offset OFFSETcfi_def_cfa_offset指令用來修改計(jì)算CFA的規(guī)則买雾。注意:OFFSET是絕對(duì)偏移量把曼,它會(huì)被加到幀指針寄存%ebp或者%rbp上杨帽,重新計(jì)算CFA的地址。

    2. .cfi_def_cfa REGISTER, OFFSETcfi_def_cfa這個(gè)指令從寄存器中獲取地址并且加上這個(gè)OFFSET 嗤军。

    3. .cfi_def_cfa_register REGISTERcfi_def_cfa_register這個(gè)指令讓%ebp%rbp被設(shè)置為新值且偏移量保持不變注盈。

      上述設(shè)置只是為了用來輔助調(diào)試的,比如打斷點(diǎn)叙赚,獲取調(diào)用堆棧信息老客。

  • CFI

    調(diào)用幀信息,英文全稱:Call Frame Information震叮。

    1. cfi_startproc胧砰,表示函數(shù)或過程開始。
    2. .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.operson.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í)行文件

執(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)容本鸣。

  1. 使用nm終端工具疫衩,先觀察一下mian.operson.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)未找到闷煤。

  2. 使用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文件特定SectionSegment的內(nèi)容久锥。

  1. 可執(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

  2. .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
  3. 查看系統(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

http://www.yinwang.org/blog-cn/2015/09/19/parser

https://objccn.io/issue-6-3/

https://llvm.org/docs/LangRef.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市别智,隨后出現(xiàn)的幾起案子宗苍,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讳窟,死亡現(xiàn)場(chǎng)離奇詭異让歼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)丽啡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門谋右,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人补箍,你說我怎么就攤上這事改执。” “怎么了坑雅?”我有些...
    開封第一講書人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵辈挂,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我裹粤,道長(zhǎng)终蒂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任遥诉,我火速辦了婚禮拇泣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘矮锈。我一直安慰自己霉翔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開白布苞笨。 她就那樣靜靜地躺著早龟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪猫缭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評(píng)論 1 304
  • 那天壹店,我揣著相機(jī)與錄音猜丹,去河邊找鬼。 笑死硅卢,一個(gè)胖子當(dāng)著我的面吹牛射窒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播将塑,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼脉顿,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了点寥?” 一聲冷哼從身側(cè)響起艾疟,我...
    開封第一講書人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蔽莱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弟疆,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年盗冷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丐枉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片少漆。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出颂碘,到底是詐尸還是另有隱情,我是刑警寧澤臀稚,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布疫粥,位于F島的核電站,受9級(jí)特大地震影響鸠天,放射性物質(zhì)發(fā)生泄漏讼育。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一稠集、第九天 我趴在偏房一處隱蔽的房頂上張望奶段。 院中可真熱鬧,春花似錦剥纷、人聲如沸痹籍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹲缠。三九已至,卻和暖如春悠垛,著一層夾襖步出監(jiān)牢的瞬間线定,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來泰國打工确买, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斤讥,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓湾趾,卻偏偏與公主長(zhǎng)得像芭商,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子搀缠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容