一、以 Hello World開篇Hello World對(duì)程序員而言肯定是如雷貫耳。但是簡單的事物背后往往包含這個(gè)復(fù)雜的機(jī)制,如果深入思考Hello world就會(huì)發(fā)現(xiàn)很多問題。C語言中的Hello World往往是這樣寫的: #include 但是你是否想過以下問題: 1、程序?yàn)槭裁匆痪幾g之后才能運(yùn)行? 2、編譯器在把C語言程序轉(zhuǎn)換成可以執(zhí)行的機(jī)器碼的過程中做了什么? 3、最后編譯出來的可執(zhí)行文件里面是什么?除了機(jī)器碼還有什么?如何存放的? 4、#include 5、什么是編譯器,它以什么為分界線分為前端和后端?編譯器和解釋器有什么區(qū)別,為什么會(huì)有解釋型語言一說? 6、以及由此延伸出的一些相關(guān)問題:Swift 是靜態(tài)語言,為什么還有運(yùn)行時(shí)庫?OC中的Runtime和運(yùn)行時(shí)庫是什么關(guān)系? 7、什么是ABI ?ABI穩(wěn)定對(duì)一門語言的發(fā)展有何影響 ?為什么 Swift 打包的 App 會(huì)平白無故的多出幾Mb ? 8、........ 等等,還有很多問題,這些問題實(shí)際上和編譯都脫離不了干系。讀完本篇文章,你的這些疑惑都能得到解答。除此之外,你還將掌握一些主流語言的基本知識(shí)。另外,繼該篇文章之后,筆者打算后期再寫一篇文章動(dòng)手試試LLVM。歡迎關(guān)注。。。。。。 相信讀者對(duì)編譯的整個(gè)流程組成部分應(yīng)該相對(duì)比較熟悉。整個(gè)流程包括預(yù)處理(Prepressing)、編譯(Compilation)、匯編(Assembly)和鏈接(Linking)。 GCC編譯hello world程序過程分解 預(yù)編譯 首先是源代碼文件hello.c和相關(guān)頭文件,如 stdio.h 被編譯器 cpp預(yù)編譯到一個(gè) .i 文件。預(yù)編譯過程主要是處理那些源代碼文件中以 # 開始的預(yù)編譯指令。比如#include 、#include 等。經(jīng)預(yù)編譯后的 .i 文件不包含任何宏定義,因?yàn)樗械暮暌呀?jīng)被展開,并且包含的文件已經(jīng)被插入到 .i 文件中。 編譯 編譯過程就是把預(yù)處理的文件經(jīng)過一系列的詞法分析、語法分析、語義分析、生成中間代碼、生成目標(biāo)代碼 優(yōu)化后生產(chǎn)相應(yīng)的匯編文件代碼。 編譯器以中間代碼為界限,又可以分前端和后端。比如 clang 就是一個(gè)前端工具,而 LLVM 則負(fù)責(zé)后端處理。另一個(gè)知名工具 GCC(GNU Compile Collection)則是一個(gè)套裝,包攬了前后端的所有任務(wù)。 前端主要負(fù)責(zé)預(yù)處理、詞法分析、語法分析,最終生成語言無關(guān)的中間代碼。后端主要負(fù)責(zé)目標(biāo)代碼的生成和優(yōu)化。后面我會(huì)重點(diǎn)介紹編譯的整個(gè)過程的每一步。這里暫時(shí)簡單提一下。 匯編 匯編器將匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令,每一個(gè)匯編語句幾乎都對(duì)應(yīng)一條機(jī)器指令。所以匯編起的過程相對(duì)于編譯器而言是比較簡單的。因?yàn)闆]有復(fù)雜的語法,也沒有予以,所以就不需要做指令優(yōu)化,只是根據(jù)匯編指令和機(jī)器指令的對(duì)照便一一翻譯就可以了。到這一步,經(jīng)過預(yù)編譯、編譯和匯編就可直接輸出目標(biāo)文件。 鏈接 在一個(gè)目標(biāo)文件中,不可能所有變量和函數(shù)都定義在同一個(gè)文件內(nèi)部。不同文件之間要做相應(yīng)的鏈接處理。 最直白的來說,編譯器就是將高級(jí)語言翻譯成機(jī)器語言的一個(gè)工具。先來看一下編譯器的整個(gè)流程。從該流程圖我們可以看到編譯器被分為前端和后端,在前端和后端之間的過度是中間代碼。其中編譯器前端包含詞法分析、語法分析、語義分析、中間代碼生成(嚴(yán)格意義來說在此四個(gè)步驟之前還有預(yù)編譯操作);編譯器后端主要是代碼優(yōu)化、目標(biāo)代碼生成以及目標(biāo)代碼優(yōu)化,編譯器的大致自責(zé)就是如此。編譯器在整個(gè)編譯過程中輸入源是源代碼,輸出的是中間代碼。 3.1 詞法分析 首先是源代碼程序被輸入到掃描器,掃描器的任務(wù)很簡單,它只是簡單的進(jìn)行詞法分析。運(yùn)用有限狀態(tài)機(jī)的算法可以很輕松的將源代碼的字符序列分割成一系列的記號(hào)。記號(hào)一般分為如下幾類:關(guān)鍵字、標(biāo)識(shí)符、字面量(數(shù)字和字符串等)以及特殊符號(hào)(如+,= .....)。 lex程序可以實(shí)現(xiàn)詞法分析,它會(huì)按照用戶之前掃描害的詞法規(guī)則將輸入的字符串分割成一個(gè)個(gè)記號(hào)。因?yàn)檫@樣一個(gè)程序的存在,編譯器開發(fā)者就無需為每一個(gè)編譯器開發(fā)一個(gè)獨(dú)立的詞法掃描器,而是根據(jù)需要改變詞法規(guī)則就可以了。 3.2 語法分析 這一步驟語法分析器將由掃描器產(chǎn)生的記號(hào)進(jìn)行語法分析,從而產(chǎn)生語法樹。簡單的講,由語法分析器生成的語法樹就是以表達(dá)式為節(jié)點(diǎn)的樹。 以如上代碼為例,它的語法樹形式如下: 語法樹將字符串格式的源代碼轉(zhuǎn)化為樹狀的數(shù)據(jù)結(jié)構(gòu),更容易被計(jì)算機(jī)理解和處理。如前面詞法分析的 lex 一樣,語法分析同樣有現(xiàn)成的工具,其中有一個(gè)叫做Yet Another Compiler Compiler 簡稱 yacc 的工具。它可以根據(jù)用戶給定的語法規(guī)則對(duì)輸入的記號(hào)序列進(jìn)行解析,從而構(gòu)建出一棵語法樹。針對(duì)不同的語言,一般編譯器開發(fā)者只需要改變語法規(guī)則,根本不需要為每個(gè)編譯器重新寫一個(gè)語法分析器,所以它又被稱為編譯器的編譯器。3.3 語義分析 語義分析有語義分析器完成。語義分析之前的語法分析僅僅只是完成了對(duì)表達(dá)式成眠的語法層面分析,但是它并不能確定這個(gè)語句是否真正有意義。如OC中兩個(gè) Person 對(duì)象實(shí)例直接做加減乘除運(yùn)算,實(shí)際上是沒有意義的,但是在語法分析上確實(shí)合情合理。這里要說明一下編譯器分析的語義都是靜態(tài)語義,靜態(tài)語義是指在編譯器件可以確定的語義,與之對(duì)應(yīng)的動(dòng)態(tài)語義只能在運(yùn)行期間才能被確定。 靜態(tài)語義分析通常包括聲明、類型匹配、類型轉(zhuǎn)換等。如一個(gè)浮點(diǎn)類型賦值給整形變量,其中就隱含了浮點(diǎn)類型轉(zhuǎn)換為整型的語義;動(dòng)態(tài)語義分析是指運(yùn)行期間出現(xiàn)的相關(guān)語義問題。 經(jīng)過語義分析之后,在語法分析生成的語法樹的基礎(chǔ)上進(jìn)一步對(duì)表達(dá)式做一些標(biāo)識(shí)。如:有些某些類型需要做隱式轉(zhuǎn)化,語義分析器會(huì)在之前的語法樹中插入相應(yīng)的轉(zhuǎn)換節(jié)點(diǎn)。 3.4 生成中間代碼 3.4.1 生成中間代碼的意義 理論上來說,中間代碼是可以直接被省略的,因?yàn)槌橄笳Z法樹可以直接轉(zhuǎn)為目標(biāo)代碼(匯編代碼)。然而不同的 CPU 架構(gòu)采用的匯編語法并不一樣,如: Intel 架構(gòu)和 AT&T 架構(gòu)的匯編碼中,源操作數(shù)和目標(biāo)操作數(shù)位置恰好相反參考鏈接。中間代碼可以理解為抽象的代碼,一方面它和語言無關(guān),同時(shí)也和 CPU 無關(guān),它僅僅只是描述了代碼要做的事情,可以將其理解為是全世界通用的語言,任何語言都可以轉(zhuǎn)換為世界語言,而世界語言又能被任何人翻譯理解。要知道,中間代碼的存在使得編譯器被分為前端和后端。其中編譯器前端主要負(fù)責(zé)產(chǎn)生與機(jī)器無關(guān)的中間代碼,編譯器后端主要是將中間代碼轉(zhuǎn)換成目標(biāo)機(jī)器代碼。因?yàn)檫@意味著針對(duì)那些跨平臺(tái)的編譯器而言,可以針對(duì)不同的平臺(tái)使用同一個(gè)前端和針對(duì)不同機(jī)器平臺(tái)的多個(gè)后端。 3.4.2 生成中間代碼的過程 生成中間代碼主要包含以下步驟,以下是用 GCC 編譯器為實(shí)例說明。 3.5 目標(biāo)代碼生成與優(yōu)化 經(jīng)過上面生成中間代碼步驟之后,這一步驟屬于編譯器后端。該步驟主要的任務(wù)是生成并優(yōu)化目標(biāo)代碼,目標(biāo)代碼亦稱為匯編代碼(其實(shí)和匯編代碼非常接近)。編譯器后端主要包括目標(biāo)代碼生成器和目標(biāo)代碼優(yōu)化去。 代碼生成器將中間代碼轉(zhuǎn)換成目標(biāo)機(jī)器代碼,此過程依賴目標(biāo)機(jī)器,應(yīng)為不同的機(jī)器有不同的寄存器、整數(shù)數(shù)據(jù)類型和浮點(diǎn)數(shù)據(jù)類型等。 目標(biāo)代碼優(yōu)化器主要是對(duì)目標(biāo)代碼進(jìn)行優(yōu)化,如:選擇合適的尋址方式、使用位移代替乘法運(yùn)算、刪除多余的指令等。 3.6 編譯過程小結(jié) 編譯器的結(jié)構(gòu)實(shí)際上是異常復(fù)雜的,主要在于三個(gè)因素。 匯編過程中輸入源是匯編代碼,輸出是二進(jìn)制機(jī)器碼(后綴為 .o 的目標(biāo)文件)。輸出的二進(jìn)制機(jī)器碼可以直接被 CPU 識(shí)別并執(zhí)行。匯編過程相對(duì)于編譯器過程而言相對(duì)簡單些,因?yàn)闆]有復(fù)雜的語法、沒有語義、不需要做指令優(yōu)化,根據(jù)匯編指令和機(jī)器指令的對(duì)照表一一翻譯即可。 由于匯編更接近機(jī)器語言,能夠直接對(duì)硬件進(jìn)行操作,生成的程序與其他的語言相比具有更高的運(yùn)行速度,占用更小的內(nèi)存,因此在一些對(duì)于時(shí)效性要求很高的程序、許多大型程序的核心模塊以及工業(yè)控制方面大量應(yīng)用。 5.1 鏈接的簡單介紹 大型軟件往往有成千上萬的模塊,模塊之前相互依賴但又獨(dú)立。一個(gè)程序被分割成多個(gè)模塊之后,這些模塊又是通過何種形式組合成一個(gè)完整的程序?模塊之間如何組合的問題實(shí)際上就是模塊之間的通信問題。 鏈接過程主要包括了: 鏈接的過程 讓我們來看看什么是重定位。假設(shè)有個(gè)全局變量叫做 var ,它在目標(biāo)文件A里面。我們在目標(biāo)文件B里面要訪問這個(gè)全局變量。由于在編譯目標(biāo)文件B的時(shí)候,編譯器并不知道變量var的目標(biāo)地址,所以編譯器在沒法確定的情況下,將目標(biāo)地址設(shè)置為0,等待鏈接器在目標(biāo)文件A和B連接起來的時(shí)候?qū)⑵湫拚?。這個(gè)地址修正的過程被叫做重定位,每個(gè)被修正的地方叫一個(gè)重定位入口。 鏈接器就是靠著重定位表來知道哪些地方需要被重定位的。每個(gè)可能存在重定位的段都會(huì)有對(duì)應(yīng)的重定位表。在鏈接階段,鏈接器會(huì)根據(jù)重定位表中,需要重定位的內(nèi)容,去別的目標(biāo)文件中找到地址并進(jìn)行重定位。 5.2靜態(tài)鏈接的缺點(diǎn) 基于上述兩個(gè)問題,就引出了一個(gè)名詞,動(dòng)態(tài)鏈接。 5.3 動(dòng)態(tài)鏈接 要解決上述兩個(gè)問題,就是不對(duì)哪些組成程序的目標(biāo)文件進(jìn)行鏈接,等到程序要運(yùn)行時(shí)才進(jìn)行鏈接。也就是說,把鏈接這個(gè)過程推遲到了運(yùn)行時(shí)再進(jìn)行,這就是動(dòng)態(tài)鏈接(Dynamic Linking)的基本思想。所謂的動(dòng)態(tài)鏈接表示重定位發(fā)生在運(yùn)行時(shí)而非編譯后。 雖然動(dòng)態(tài)鏈接可以解決上述的兩個(gè)問題,但是在性能上要略微比靜態(tài)鏈接差一些。筆者之前也寫過一篇Swift性能分析的文章,其中也涉及到一些關(guān)于OC和Swift語言動(dòng)態(tài)鏈接相關(guān)的點(diǎn)。 6.1 解釋型語言 編譯器和解釋器 解釋器是一條一條的解釋執(zhí)行源語言,不需要編譯直接由解釋器執(zhí)行,對(duì)應(yīng)的語言稱為解釋型語言也稱作腳本語言。比如 Php,Ruby,JavaScript、Python 等就是典型的解釋性語言。 解釋型語言同編譯型語言相比,編譯器是把源代碼整個(gè)編譯成目標(biāo)代碼,執(zhí)行時(shí)不在需要再去編譯器,直接在支持目標(biāo)代碼的平臺(tái)上運(yùn)行,所以執(zhí)行效率比解釋執(zhí)行快很多。比如C語言代碼被編譯成二進(jìn)制代碼(exe程序),在windows平臺(tái)上執(zhí)行。 6.2 解釋型語言和編譯型語言的共同點(diǎn) 兩者的共同點(diǎn)很簡單,一句話總結(jié):都需要轉(zhuǎn)換成二進(jìn)制才能執(zhí)行。 6.3 解釋型語言和編譯型語言的不同點(diǎn) 7.1 關(guān)于運(yùn)行時(shí) 7.1.1 Runtime 如果你是一個(gè) iOS 開發(fā)者,想必都聽過并用過 runtime。但其實(shí) runtime 并非是 Objective-C 的專利,絕大多數(shù)語言都有這個(gè)概念。所以說 runtime 讓 Objective-C 具有動(dòng)態(tài)性這句話是錯(cuò)誤的。如果要認(rèn)清楚這一點(diǎn),感覺有必要先認(rèn)清楚運(yùn)行時(shí)庫,要知道 runtime 就是運(yùn)行時(shí)庫的一部分。 7.1.2 運(yùn)行時(shí)庫概念 以 C 語言為例說明運(yùn)行時(shí)庫的概念。在 C 語言中 glibc 這個(gè)動(dòng)態(tài)鏈接庫通常會(huì)被很多操作依賴,包括字符串處理(strlen、strcpy)、信號(hào)處理、socket、線程、IO、動(dòng)態(tài)內(nèi)存分配等等。由于每個(gè)程序都依賴于運(yùn)行時(shí)庫,這些庫一般都是動(dòng)態(tài)鏈接的。這樣一來,運(yùn)行時(shí)庫可以存儲(chǔ)在操作系統(tǒng)中,很多程序共享一個(gè)動(dòng)態(tài)庫,這樣就可以節(jié)省內(nèi)存占用空間和應(yīng)用程序大小。 7.1.3 Swift運(yùn)行時(shí)庫 參照上述 C 語言的運(yùn)行石庫,就很容易理解 Swift運(yùn)行時(shí)庫的概念了。一方面,swift 是絕對(duì)的靜態(tài)語言,另一方面,swift 毫無疑問的帶有自己的運(yùn)行時(shí)庫。按照常理來說類似字符串、數(shù)組、print 函數(shù)都應(yīng)該是運(yùn)行時(shí)庫中的一部分。然而,Swift 依然沒有穩(wěn)定自己的 ABI ,導(dǎo)致每個(gè)程序都必須自帶運(yùn)行時(shí)庫,這也就是為什么目前 swift 開發(fā)的 app 普遍會(huì)增加幾 Mb 包大小的原因。 7.2 ABI 簡單概念 7.2.1 什么是ABI? ABI 是 Application Binary Interface的縮寫,它是一個(gè)規(guī)范。簡單的說它就是編譯后的 API (API 描述了在應(yīng)用程序級(jí)別,模塊之間的調(diào)用約定)。 通過這個(gè)規(guī)范,所有被獨(dú)立編譯的二進(jìn)制實(shí)體才能被鏈接在一起并執(zhí)行。這些二進(jìn)制實(shí)體必須在一些很低層的細(xì)節(jié)上達(dá)成一致,例如:如何調(diào)用函數(shù),如何在內(nèi)存中表示數(shù)據(jù),甚至是如何存儲(chǔ)以及訪問數(shù)據(jù)。要重點(diǎn)知道,ABI 是平臺(tái)相關(guān)的,因?yàn)樗P(guān)注的這些底層細(xì)節(jié)會(huì)受到不同的硬件架構(gòu)以及操作系統(tǒng)的的影響。 為了更好的理解什么是ABI,如下舉個(gè)詳細(xì)的列子說明。 比如模塊 A 有兩個(gè)整數(shù) a 和 b,它們的內(nèi)存布局如下: 其他模塊調(diào)用 A 模塊的 b 變量,可以通過初始地址加偏移量的方式獲取b變量。如果后來模塊 A 新增了一個(gè)整數(shù) c (該過程可以看做是手機(jī)系統(tǒng)更新(伴隨著運(yùn)行時(shí)庫更新)),它的內(nèi)存布局可能又會(huì)變成如下這種形式。如果還是通過初始地址加偏移量的方式獲取變量,那么此時(shí)獲取的是 a 變量,而不再是之前的 b 變量。如果把模塊 A 看做是 Swift 運(yùn)行時(shí)庫,假設(shè)現(xiàn)在該運(yùn)行時(shí)庫已經(jīng)內(nèi)置于操作系統(tǒng)中并與手機(jī)上不同的應(yīng)用程序動(dòng)態(tài)鏈接在一些。如果每次更新系統(tǒng),就會(huì)出現(xiàn)某些 App 崩潰的情況。如何定義好 A 模塊獲取變量的規(guī)則,其中的規(guī)則就是所謂的 ABI 。 7.2.2 什么是ABI穩(wěn)定? ABI 穩(wěn)定就是將 ABI 鎖定在某種形式下,使之后的相關(guān)編譯器可以遵守這種二進(jìn)制實(shí)體,這種二進(jìn)制實(shí)體可以是庫也可以是程序。一旦穩(wěn)定了 ABI ,基本便是它會(huì)伴隨著這個(gè)平臺(tái)一生一世,甚至是走到滅忙。 對(duì) ABI 做出的每一個(gè)決定都會(huì)對(duì)一門編程語言產(chǎn)生長遠(yuǎn)的影響,甚至可能會(huì)約束一門語言后期的發(fā)展和進(jìn)化。如:Swift 語言一直尚未申明ABI的穩(wěn)定,但只要申明了某個(gè)平臺(tái)的 ABI 已經(jīng)穩(wěn)定,那么任何有缺陷的設(shè)計(jì)將永遠(yuǎn)伴隨著這個(gè)平臺(tái)。 7.2.3 ABI穩(wěn)定了會(huì)怎樣? ABI 穩(wěn)定之后,OS 發(fā)行商就可以把 Swift 標(biāo)準(zhǔn)庫和運(yùn)行時(shí)作為操作系統(tǒng)的一部分嵌入,由于這些標(biāo)準(zhǔn)庫和運(yùn)行時(shí)可以支持用更老或更新版本 Swift 構(gòu)建的應(yīng)用程序,這樣,開發(fā)者就無需在分發(fā)應(yīng)用的同時(shí),還要帶上一份自己構(gòu)建應(yīng)用時(shí)使用的標(biāo)準(zhǔn)庫和運(yùn)行時(shí)拷貝。這使得工具和操作系統(tǒng)可以更好的進(jìn)行集成。 然而目前 Swift 還是一門年輕的語言,ABI 尚未穩(wěn)定,暫時(shí)還未和 iOS 系統(tǒng)硬件綁定,所以在開發(fā)移動(dòng)端應(yīng)用的時(shí)候會(huì)發(fā)現(xiàn) app 普遍會(huì)增加幾 Mb 包大小。 簡單的做個(gè)小結(jié),本文顯示總的介紹的整個(gè)編譯過程,之后針對(duì)編輯中的每個(gè)步驟做了進(jìn)一步的說明。最后相繼介紹了編譯型和解釋型語言的區(qū)別、runtime、運(yùn)行時(shí)庫、什么是 ABI 以及 ABI 穩(wěn)定的意義。 |
|