在 iOS 開發(fā)的過程中睁本,Xcode 為我們提供了非常完善的編譯能力兴泥,正常情況下麦备,我們只需要 Command + R 就可以將應(yīng)用運(yùn)行到設(shè)備上局骤,即使打包也是一個(gè)相對愉快的過程攀圈。
但正如我們寫代碼無法避開 Bug 一樣,項(xiàng)目在編譯的時(shí)候也會出現(xiàn)各種各樣的錯(cuò)誤庄涡,最痛苦的莫過于處理這些錯(cuò)誤量承。其中的各種報(bào)錯(cuò)都不是我們在日常編程中所能接觸的,而我們無法快速精準(zhǔn)的定位錯(cuò)誤并解決的唯一原因就是我們根本不知道在編譯的時(shí)候都做了些什么,都需要些什么撕捍。就跟使用一個(gè)新的類拿穴,如果不去查看其代碼,永遠(yuǎn)也無法知道它到底能干什么一樣忧风。
這篇文章將從由簡入繁的講解 iOS App 在編譯的時(shí)候到底干了什么默色。一個(gè) iOS 項(xiàng)目的編譯過程是比較繁瑣的,針對源代碼狮腿、xib腿宰、framework 等都將進(jìn)行一定的編譯和操作,再加上使用 Cocoapods缘厢,會讓整個(gè)過程更加復(fù)雜吃度。這篇文章將以 Swift 和 Objective-C 的不同角度來分析。
1. 什么是編譯
在開始之前贴硫,我們必須知道什么是編譯椿每?為什么要進(jìn)行編譯?
CPU 由上億個(gè)晶體管組成英遭,在運(yùn)行的時(shí)候间护,單個(gè)晶體管只能根據(jù)電流的流通或關(guān)閉來確認(rèn)兩種狀態(tài),我們一般說 0 或 1挖诸,根據(jù)這種狀態(tài)汁尺,人類創(chuàng)造了二進(jìn)制,通過二進(jìn)制編碼我們可以表示所有的概念多律。但是痴突,CPU 依然只能執(zhí)行二進(jìn)制代碼。我們將一組二進(jìn)制代碼合并成一個(gè)指令或符號菱涤,創(chuàng)造了匯編語言苞也,匯編語言以一種相對好理解的方式來編寫,然后通過匯編過程生成 CPU 可以運(yùn)行的二進(jìn)制代碼并運(yùn)行在 CPU 上粘秆。
但是使用匯編語言開發(fā)仍然是一個(gè)相對痛苦的過程如迟,于是通過上述方式,c攻走、c++殷勘、Java 等語言就一層一層的被發(fā)明出來。Objective-c 和 Swift 就是這樣一個(gè)過程昔搂,他們的基礎(chǔ)都是 c 和 c++玲销。
當(dāng)我們使用 Objective-c 和 Swift 編寫代碼后,想要代碼能運(yùn)行在 CPU 上摘符,我們必須進(jìn)行編譯贤斜,將我們寫好的代碼編譯為機(jī)器可以理解的二進(jìn)制代碼策吠。
1.1 LLVM
有了上面的簡單介紹,可以發(fā)現(xiàn)瘩绒,編譯其實(shí)是一個(gè)用代碼解釋代碼的過程猴抹。在 Objective-c 和 Swift 的編譯過程中,用來解釋代碼的锁荔,就是 LLVM蟀给。點(diǎn)擊可以看到 LLVM 的官方網(wǎng)站,在 Overview 的第一行就說明了 LLVM 到底是什么:
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 項(xiàng)目是一個(gè)模塊化阳堕、可重用的編譯器跋理、工具鏈技術(shù)的集合。盡管它的名字叫 LLVM恬总,但它與傳統(tǒng)虛擬機(jī)的關(guān)系并不大前普。“LLVM”這個(gè)名字本身不是一個(gè)縮略詞; 它的全稱是這個(gè)項(xiàng)目越驻。
// LLVM 命名最早源自于底層虛擬機(jī)(Low Level Virtual Machine)的縮寫汁政。
LVVM 的作者寫了一篇關(guān)于什么是 LLVM 的文章道偷,詳細(xì)的描述了 LLVM 的使用的技術(shù)點(diǎn):LLVM缀旁。
簡單的說,LLVM 是一個(gè)項(xiàng)目勺鸦,其作用就是提供一個(gè)廣泛的工具并巍,可以將任何高級語言的代碼編譯為任何架構(gòu)的 CPU 都可以運(yùn)行的機(jī)器代碼。它將整個(gè)編譯過程分類了三個(gè)模塊:前端换途、公用優(yōu)化器懊渡、后端。(這里不要去思考任何關(guān)于 web 前端和 service 后端的概念军拟。)
- 前端:對目標(biāo)語言代碼進(jìn)行語法分析剃执,語義分析,生成中間代碼懈息。在這個(gè)過程中肾档,會進(jìn)行類型檢查,如果發(fā)現(xiàn)錯(cuò)誤或者警告會標(biāo)注出來在哪一行辫继。我們在開發(fā)的過程中怒见,其實(shí) Xcode 也會使用前端工具對你的代碼進(jìn)行分析,并實(shí)時(shí)的檢查出來某些錯(cuò)誤姑宽。前端是針對特定語言的遣耍,如果需要一個(gè)新的語言被編譯,只需要再寫一個(gè)針對新語言的前端模塊即可炮车。
- 公用優(yōu)化器:將生成的中間文件進(jìn)行優(yōu)化舵变,去除冗余代碼酣溃,進(jìn)行結(jié)構(gòu)優(yōu)化。
- 后端:后段將優(yōu)化后的中間代碼再次轉(zhuǎn)換纪隙,變成匯編語言救拉,并再次進(jìn)行優(yōu)化,最后將各個(gè)文件代碼轉(zhuǎn)換為機(jī)器代碼并鏈接瘫拣。鏈接是指將不同代碼文件編譯后的不同機(jī)器代碼文件合并成一個(gè)可執(zhí)行文件亿絮。
雖然目前 LLVM 并沒有達(dá)到其目標(biāo)(可以編譯任何代碼),但是這樣的思路是很優(yōu)秀的麸拄,在日常開發(fā)中派昧,這種思路也會為我們提供不少的幫助。
1.2 clang
clang 是 LLVM 的一個(gè)前端拢切,它的作用是針對 C 語言家族的語言進(jìn)行編譯蒂萎,像 c、c++淮椰、Objective-C五慈。而 Swift 則自己實(shí)現(xiàn)了一個(gè)前端來進(jìn)行 Swift 編譯,優(yōu)化器和后端依然是使用 LLVM 來完成主穗,后面會專門對 Swift 語言的 前端編譯流程進(jìn)行分析泻拦。
上面簡單的介紹了為什么需要編譯,以及 Objectie-C 和 Swift 代碼的編譯思路忽媒。這是基礎(chǔ)争拐,如果沒有這些基礎(chǔ),后面針對我們整個(gè)項(xiàng)目的編譯就無法理解晦雨,如果你理解了上面的知識點(diǎn)架曹,那么下面將要講述的整個(gè)項(xiàng)目的編譯過程就會顯得很簡單了。
2. iOS 項(xiàng)目編譯過程簡介
Xcode 在編譯 iOS 項(xiàng)目的時(shí)候闹瞧,使用的正是 LLVM绑雄,其實(shí)我們在編寫代碼以及調(diào)試的時(shí)候也在使用 LLVM 提供的功能。例如代碼高亮(clang)奥邮、實(shí)時(shí)代碼檢查(clang)万牺、代碼提示(clang)、debug 斷點(diǎn)調(diào)試(LLDB)漠烧。這些都是 LLVM 前端提供的功能杏愤,而對于后端來說,我們接觸到的就是關(guān)于 arm64已脓、armv7珊楼、armv7s 這些 CPU 架構(gòu)了,記得之前還有 32 位架構(gòu)處理器的時(shí)候度液,設(shè)定指定的編譯的目標(biāo) CPU 架構(gòu)就是一個(gè)比較痛苦的過程厕宗。
下面來簡單的講講整個(gè) iOS 項(xiàng)目的編譯過程画舌,其中可能會有一些疑問,先保留著已慢,后面會詳細(xì)解釋:
我們的項(xiàng)目是一個(gè) target曲聂,一個(gè)編譯目標(biāo),它擁有自己的文件和編譯規(guī)則佑惠,在我們的項(xiàng)目中可以存在多個(gè)子項(xiàng)目朋腋,這在編譯的時(shí)候就導(dǎo)致了使用了 Cocoapods 或者擁有多個(gè) target 的項(xiàng)目會先編譯依賴庫。這些庫都和我們的項(xiàng)目編譯流程一致膜楷。Cocoapods 的原理解釋將在文章后面一部分進(jìn)行解釋旭咽。
- 寫入輔助文件:將項(xiàng)目的文件結(jié)構(gòu)對應(yīng)表、將要執(zhí)行的腳本赌厅、項(xiàng)目依賴庫的文件結(jié)構(gòu)對應(yīng)表寫成文件穷绵,方便后面使用;并且創(chuàng)建一個(gè) .app 包特愿,后面編譯后的文件都會被放入包中;
- 運(yùn)行預(yù)設(shè)腳本:Cocoapods 會預(yù)設(shè)一些腳本揍障,當(dāng)然你也可以自己預(yù)設(shè)一些腳本來運(yùn)行。這些腳本都在 Build Phases 中可以看到亚兄;
- 編譯文件:針對每一個(gè)文件進(jìn)行編譯,生成可執(zhí)行文件 Mach-O审胚,這過程 LLVM 的完整流程,前端礼旅、優(yōu)化器膳叨、后端;
- 鏈接文件:將項(xiàng)目中的多個(gè)可執(zhí)行文件合并成一個(gè)文件痘系;
- 拷貝資源文件:將項(xiàng)目中的資源文件拷貝到目標(biāo)包菲嘴;
- 編譯 storyboard 文件:storyboard 文件也是會被編譯的;
- 鏈接 storyboard 文件:將編譯后的 storyboard 文件鏈接成一個(gè)文件汰翠;
- 編譯 Asset 文件:我們的圖片如果使用 Assets.xcassets 來管理圖片龄坪,那么這些圖片將會被編譯成機(jī)器碼,除了 icon 和 launchImage复唤;
- 運(yùn)行 Cocoapods 腳本:將在編譯項(xiàng)目之前已經(jīng)編譯好的依賴庫和相 關(guān)資源拷貝到包中健田。
- 生成 .app 包
- 將 Swift 標(biāo)準(zhǔn)庫拷貝到包中
12 .對包進(jìn)行簽名 - 完成打包
在上述流程中:2 - 9 步驟的數(shù)量和順序并不固定,這個(gè)過程可以在 Build Phases 中指定佛纫。Phases:階段亭畜、步驟寺鸥。這個(gè) Tab 的意思就是編譯步驟陕见。其實(shí)不僅我們的整個(gè)編譯步驟和順序可以被設(shè)定,包括編譯過程中的編譯規(guī)則(Build Rules)和具體步驟的參數(shù)(Build Settings)局雄,在對應(yīng)的 Tab 都可以看到。關(guān)于整個(gè)編譯流程的日志和設(shè)定存炮,可以查看這篇文章:Build 過程炬搭,跟著它的步驟來查看自己的項(xiàng)目將有助于你理解整個(gè)編譯流程。后面也會詳細(xì)講解這些內(nèi)容穆桂。
查看對應(yīng)位置的方法:在 Xcode 中選擇自己的項(xiàng)目尚蝌,在 targets 中選擇自己的項(xiàng)目,就可以看到對應(yīng)的 Tab 充尉。
3. 文件編譯過程
Objective-C 的文件中飘言,只有 .m 文件會被編譯 .h 文件只是一個(gè)暴露外部接口的頭文件,它的作用是為被編譯的文件中的代碼做簡單的共享驼侠。下面拿一個(gè)單獨(dú)的類文件進(jìn)行分析姿鸿。這些步驟中的每一步你都可以使用 clang 的命令來查看其進(jìn)度,記住 clang 是一個(gè)命令行工具倒源,它可以直接在終端中運(yùn)行笋熬。這里我們使用 c 語言作為例子類進(jìn)行分析胳螟,它的過程和 Objective-C 一樣糖耸,后面 3.7 會講到 Swift 文件是如何被編譯的嘉竟。
3.1 預(yù)處理
在我們的代碼中會有很多 #import 宏舍扰,預(yù)處理的第一步就是將 import 引入的文件代碼放入對應(yīng)文件边苹。
然后將自定義宏替換勾给,例如我們定義了如下宏并進(jìn)行了使用:
#define Button_Height 44
#define Button_Width 100
button.frame = CGRectMake(0, 0, Button_Width, Button_Height);
那么代碼將被替換為:
button.frame = CGRectMake(0, 0, 44, 100);
按照這樣的思路可以發(fā)現(xiàn),在自定義宏的時(shí)候要格外小心脓钾,尤其是一些攜帶參數(shù)和功能的宏可训,這些宏也只是簡單的直接替換代碼握截,不能真的代替方法或函數(shù)谨胞,中間會有很多問題胯努。
在將代碼完全拆開后叶沛,將會對代碼進(jìn)行符號化灰署,對于分析代碼的代碼 (clang)溉箕,我們寫的代碼就是一些字符串,為了后面給這些代碼進(jìn)行語法和語義分析偎痛,需要將我們的代碼進(jìn)行標(biāo)記并符號化,例如一段 helloworld 的 c 代碼:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
使用 clang 命令 clang -Xclang -dump-tokens helloworld.c 轉(zhuǎn)化后的代碼如下(去掉了 stdio.h 中的內(nèi)容):
int 'int' [StartOfLine] Loc=<helloworld.c:2:1>
identifier 'main' [LeadingSpace] Loc=<helloworld.c:2:5>
l_paren '(' Loc=<helloworld.c:2:9>
int 'int' Loc=<helloworld.c:2:10>
identifier 'argc' [LeadingSpace] Loc=<helloworld.c:2:14>
comma ',' Loc=<helloworld.c:2:18>
char 'char' [LeadingSpace] Loc=<helloworld.c:2:20>
star '*' [LeadingSpace] Loc=<helloworld.c:2:25>
identifier 'argv' Loc=<helloworld.c:2:26>
l_square '[' Loc=<helloworld.c:2:30>
r_square ']' Loc=<helloworld.c:2:31>
r_paren ')' Loc=<helloworld.c:2:32>
l_brace '{' [StartOfLine] Loc=<helloworld.c:3:1>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<helloworld.c:4:2>
l_paren '(' Loc=<helloworld.c:4:8>
string_literal '"Hello World!\n"' Loc=<helloworld.c:4:9>
r_paren ')' Loc=<helloworld.c:4:25>
semi ';' Loc=<helloworld.c:4:26>
return 'return' [StartOfLine] [LeadingSpace] Loc=<helloworld.c:5:2>
numeric_constant '0' [LeadingSpace] Loc=<helloworld.c:5:9>
semi ';' Loc=<helloworld.c:5:10>
r_brace '}' [StartOfLine] Loc=<helloworld.c:6:1>
eof '' Loc=<helloworld.c:6:2>
這里,每一個(gè)符號都會標(biāo)記出來其位置贪婉,這個(gè)位置是宏展開之前的位置疲迂,這樣后面如果發(fā)現(xiàn)報(bào)錯(cuò),就可以正確的提示錯(cuò)誤位置了郑气。針對 Objective-C 代碼尾组,我們只需要轉(zhuǎn)化對應(yīng)的 .m 文件就可以查看讳侨。
3.2 語意和語法分析
3.2.1 AST
對代碼進(jìn)行標(biāo)記之后跨跨,其實(shí)就可以對代碼進(jìn)行分析勇婴,但是這樣分析起來的過程會比較復(fù)雜铆帽。于是 clang 又進(jìn)行了一步轉(zhuǎn)換:將之前的標(biāo)記流轉(zhuǎn)換為一顆抽象語法樹(abstract syntax tree – AST)爹橱。
使用 clang 命令 clang -Xclang -ast-dump -fsyntax-only helloworld.c愧驱,轉(zhuǎn)化后的樹如下(去掉了 stdio.h 中的內(nèi)容):
`-FunctionDecl 0x7f8eaf834bb0 <helloworld.c:2:1, line:6:1> line:2:5 main 'int (int, char **)'
|-ParmVarDecl 0x7f8eaf8349b8 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7f8eaf834aa0 <col:20, col:31> col:26 argv 'char **':'char **'
`-CompoundStmt 0x7f8eaf834dd8 <line:3:1, line:6:1>
|-CallExpr 0x7f8eaf834d40 <line:4:2, col:25> 'int'
| |-ImplicitCastExpr 0x7f8eaf834d28 <col:2> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7f8eaf834c68 <col:2> 'int (const char *, ...)' Function 0x7f8eae836d78 'printf' 'int (const char *, ...)'
| `-ImplicitCastExpr 0x7f8eaf834d88 <col:9> 'const char *' <BitCast>
| `-ImplicitCastExpr 0x7f8eaf834d70 <col:9> 'char *' <ArrayToPointerDecay>
| `-StringLiteral 0x7f8eaf834cc8 <col:9> 'char [14]' lvalue "Hello World!\n"
`-ReturnStmt 0x7f8eaf834dc0 <line:5:2, col:9>
`-IntegerLiteral 0x7f8eaf834da0 <col:9> 'int' 0
這是一個(gè) main 方法的抽象語法樹吻商,可以看到樹頂是 FunctionDecl:方法聲明(Function Declaration)艾帐。
這里因?yàn)榻厝×瞬糠执a柒爸,其實(shí)并不是整個(gè)樹的樹頂捎稚。真正的樹頂描述應(yīng)該是:TranslationUnitDecl今野。
然后是兩個(gè) ParmVarDecl:參數(shù)聲明条霜。
接著下一層是 CompoundStmt:說明下面有一組復(fù)合的聲明語句蛔外,指的是我們的 main 方法里面所使用到的所有代碼夹厌。
再到里面就是每一行代碼的使用,方法的調(diào)用臂聋,傳遞的參數(shù)孩等,以及返回肄方。在實(shí)際應(yīng)用中還會有變量的聲明权她、操作符的使用等隅要。
關(guān)于 AST 的詳細(xì)解釋可以查看:Introduction to the Clang AST步清。
3.2.2 靜態(tài)分析
有了這樣的語法樹廓啊,對代碼的分析就會簡單許多崖瞭。對這棵樹進(jìn)行遍歷分析,包括類型檢查、實(shí)現(xiàn)檢查(某個(gè)類是否存在某個(gè)方法)雌续、變量使用驯杜,還會有一些復(fù)雜的檢查鸽心,例如在 Objective-C 中顽频,給某一個(gè)對象發(fā)送消息(調(diào)用某個(gè)方法)糯景,檢查這個(gè)對象的類是否聲明這個(gè)方法(但并不會去檢查這個(gè)方法是否實(shí)現(xiàn)蟀淮,這個(gè)錯(cuò)誤是在運(yùn)行時(shí)進(jìn)行檢查的)怠惶,如果有什么錯(cuò)誤就會進(jìn)行提示策治。因此可見逃延,Xcode 對 clang 做了非常深度的集成揽祥,在編寫代碼的過程中它就會使用 clang 來對你的代碼進(jìn)行分析拄丰,并及時(shí)的對你的代碼錯(cuò)誤進(jìn)行提示。
3.3 生成 LLVM 代碼
當(dāng)確認(rèn)代碼沒有問題后(靜態(tài)分析可分析出來的問題)奄侠,前端就將進(jìn)入最后一步:生成 LLVM 代碼垄潮,并將代碼遞交給優(yōu)化器。
使用命令 clang -S -emit-llvm helloworld.c -o helloworld.ll 將生成 LLVM IR旅急。
The most important aspect of its design is the LLVM Intermediate Representation (IR), which is the form it uses to represent code in the compiler. LLVM IR is designed to host mid-level analyses and transformations that you find in the optimizer section of a compiler. It was designed with many specific goals in mind, including supporting lightweight runtime optimizations, cross-function/interprocedural optimizations, whole program analysis, and aggressive restructuring transformations, etc. The most important aspect of it, though, is that it is itself defined as a first class language with well-defined semantics.
其設(shè)計(jì)的最重要的部分是 LLVM 中間表示(IR)藐吮,它是一種在編譯器中表示代碼的形式谣辞。LLVM IR 旨在承載在編譯器的優(yōu)化器中間的分析和轉(zhuǎn)換泥从。它的設(shè)計(jì)考慮了許多特定的目標(biāo)歉闰,包括支持輕量級運(yùn)行時(shí)優(yōu)化卓起,跨功能/進(jìn)程間優(yōu)化戏阅,整個(gè)程序分析和積極的重組轉(zhuǎn)換等等奕筐。但它最重要的方面是它本身被定義為具有明確定義的語義的第一類語言。
例如我們上面的代碼將會被生成為:
; ModuleID = 'helloworld.c'
source_filename = "helloworld.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.12.0"
@.str = private unnamed_addr constant [14 x i8] c"Hello World!\0A\00", align 1
; Function Attrs: nounwind ssp uwtable
define i32 @main(i32, i8**) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%6 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0))
ret i32 0
}
declare i32 @printf(i8*, ...) #1
attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "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-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "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" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"Apple LLVM version 8.1.0 (clang-802.0.42)"}
其實(shí)還是能實(shí)現(xiàn)我們功能的代碼,在這一步旬盯,所有 LLVM 前端支持的語言都將會被轉(zhuǎn)換成這樣的代碼胖翰,主要是為了后面的工作可以共用萨咳。下面就是 LVVM 中的優(yōu)化器的工作培他。
在這里簡單介紹一些 LLVM IR 的指令:
- %:局部變量
- @:全局變量
- alloca:分配內(nèi)存堆棧
- i32:32 位的整數(shù)
- i32**:一個(gè)指向 32 位 int 值的指針的指針
- align 4:向 4 個(gè)字節(jié)對齊,即便數(shù)據(jù)沒有占用 4 個(gè)字節(jié)怔毛,也要為其分配四個(gè)字節(jié)
- call:調(diào)用
3.4 優(yōu)化
上面的代碼是沒有進(jìn)行優(yōu)化過的,在語言轉(zhuǎn)換的過程中碎绎,有些代碼是可以被優(yōu)化以提升執(zhí)行效率的筋帖。使用命令 clang -O3 -S -emit-llvm helloworld.c -o helloworld.ll日麸,其實(shí)和上面的命令的區(qū)別只有 -O3 而已,注意墩划,這里是大寫字母 O 而不是數(shù)字 0乙帮。優(yōu)化后的代碼如下:
; ModuleID = 'helloworld.c'
source_filename = "helloworld.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.12.0"
@str = private unnamed_addr constant [13 x i8] c"Hello World!\00"
; Function Attrs: nounwind ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i32 @puts(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @str, i64 0, i64 0))
ret i32 0
}
; Function Attrs: nounwind
declare i32 @puts(i8* nocapture readonly) #1
attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "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-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind }
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"Apple LLVM version 8.1.0 (clang-802.0.42)"}
可以看到,即使是最簡單的 helloworld 代碼盼樟,也會被優(yōu)化晨缴。這一步驟的優(yōu)化是非常重要的,很多直接轉(zhuǎn)換來的代碼是不合適且消耗內(nèi)存的诀浪,因?yàn)槭侵苯愚D(zhuǎn)換雷猪,所以必然會有這樣的問題晰房,而優(yōu)化放在這一步的好處在于前端不需要考慮任何優(yōu)化過程,減少了前端的開發(fā)工作验夯。
如果想了解優(yōu)化過程中到底進(jìn)行了什么優(yōu)化摔刁,可以查看這篇文章:編譯器绑谣。
3.5 生成目標(biāo)文件
下面就是后端的工作了借宵,將優(yōu)化過的代碼根據(jù)不同架構(gòu)的 CPU 轉(zhuǎn)化生成匯編代碼矾削,再生成對應(yīng)的可執(zhí)行文件欲间,這樣對應(yīng)的 CPU 就可以執(zhí)行了挡逼。
使用命令 clang -S -o - helloworld.c | open -f 可以查看生成的匯編代碼:
“`
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 12
.globl _main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
subq 32,leaqL.str(movl32,leaqL.str(movl0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
movq %rax, %rdi
movb 0,callqprintfxorlmovlmovladdq0,callqprintfxorlmovlmovladdq32, %rsp
popq %rbp
retq
.cfi_endproc
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz “Hello World!\n”
.subsections_via_symbols
“`
注意代碼中的 .section
指令嘱能,它指定了接下來會執(zhí)行的代碼段惹骂。在這篇文章中对粪,詳細(xì)解釋了這些匯編指令或代碼到底是如何工作的:Mach-O 可執(zhí)行文件著拭。
3.6 可執(zhí)行文件
在最后牍帚,LLVM 將會把這些匯編代碼輸出成二進(jìn)制的可執(zhí)行文件鄙币,使用命令 clang helloworld.c -o helloworld.out 即可查看,-o helloworld.out 如果不指定十嘿,將會被默認(rèn)指定為 a.out绩衷。
可執(zhí)行文件會有多個(gè)部分版姑,對應(yīng)了匯編指令中的 .section迟郎,它的名字也叫做 section,每個(gè) section 都會被轉(zhuǎn)換進(jìn)某個(gè) segment 里控乾。這種方式用來區(qū)分不同功能的代碼蜕衡。將相同屬性的 section 集合在一起,就是一個(gè) segment慨仿。
使用 otool 工具可以查看生成的可執(zhí)行文件的 section 和 segment:
xcrun size -x -l -m helloworld.out
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x34 (addr 0x100000f50 offset 3920)
Section __stubs: 0x6 (addr 0x100000f84 offset 3972)
Section __stub_helper: 0x1a (addr 0x100000f8c offset 3980)
Section __cstring: 0xe (addr 0x100000fa6 offset 4006)
Section __unwind_info: 0x48 (addr 0x100000fb4 offset 4020)
total 0xaa
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000
上面的代碼中,每個(gè) segment 的意義也不一樣:
- __PAGEZERO segment 它的大小為 4GB万皿。這 4GB 并不是文件的真實(shí)大小牢硅,但是規(guī)定了進(jìn)程地址空間的前 4GB 被映射為 不可執(zhí)行减余、不可寫和不可讀绵脯。
- __TEXT segment 包含了被執(zhí)行的代碼佳励。它被以只讀和可執(zhí)行的方式映射休里。進(jìn)程被允許執(zhí)行這些代碼,但是不能修改赃承。
- __DATA segment 以可讀寫和不可執(zhí)行的方式映射妙黍。它包含了將會被更改的數(shù)據(jù)。
- __LINKEDIT segment 指出了 link edit 表(包含符號和字符串的動態(tài)鏈接器表)的地址瞧剖,里面包含了加載程序的元數(shù)據(jù)拭嫁,例如函數(shù)的名稱和地址。
電腦如何讀取代碼
深入剖析 iOS 編譯 Clang LLVM
本文轉(zhuǎn)自 iOS App 的編譯過程