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