這幾天看了一篇名為《大前端開發(fā)者需要了解的基礎(chǔ)編譯原理和語言知識》摧冀,讀完之后深感作者知識面之廣,內(nèi)容之豐富梯捕,感謝作者分享如此高質(zhì)量的文章。
同時窝撵,鑒于文章內(nèi)容較多傀顾,自己寫下這篇文章,作為心得和筆記碌奉。
代碼的編譯過程往粗了說分為四個階段:1.預(yù)處理(preprocessing)2.編譯(compliation)3.匯編(assembly)4.鏈接(linking)短曾。
往細(xì)了說分為七個階段:1.預(yù)處理、2.詞法分析赐劣、3.語法分析嫉拐、4.生成中間代碼、5.生成目標(biāo)代碼魁兼、6.匯編婉徘、7.鏈接。
這里面的主要區(qū)別就是編譯包括了詞法分析咐汞、語法分析盖呼、生成中間代碼和生成目標(biāo)代碼四個部分。
編譯器負(fù)責(zé)預(yù)處理化撕、詞法分析几晤、語法分析、生成中間代碼和生成目標(biāo)代碼五個步驟植阴,編譯器的輸入是源代碼锌仅,輸出是中間代碼章钾。編譯器以中間代碼為分界又分為編譯器前端和編譯器后端墙贱。編譯器前端負(fù)責(zé)語法分析生成抽象語法樹(AST热芹,Abstract Syntax Tree),后端負(fù)責(zé)將抽象語法樹轉(zhuǎn)換為中間代碼惨撇。
ps:中間代碼已經(jīng)非常接近于實(shí)際的匯編代碼伊脓,它幾乎可以直接被轉(zhuǎn)化。主要的工作量在于兼容各種 CPU 以及填寫模板魁衙。此工作由編譯器或匯編器完成报腔。
主流的C/C++的編譯器有GCC,這是GNU發(fā)布的一款極具影響力的編譯器剖淀,已經(jīng)成為Linux和Unix系統(tǒng)的默認(rèn)編譯器纯蛾。從事iOS開發(fā)使用的Xcode目前用的編譯器是clang+LLVM。Xcode4之前纵隔,用的是GCC編譯器翻诉,由于GCC對Objective-C的支持不是很好,于是蘋果采用了“自家”發(fā)起的clang+LLVM作為默認(rèn)編譯器捌刮。
這兩個編譯器的主要區(qū)別是GCC直接負(fù)責(zé)整個編譯過程的五個步驟碰煌,而clang+LLVM將編譯的步驟拆分,clang作為編譯器前端(還記得編譯器前端的職責(zé)么绅作?)芦圾,LLVM作為編譯器的后端兩者配合使用。當(dāng)然歷時上也曾經(jīng)出現(xiàn)過俄认,GCC作為編譯器前端个少,LLVM作為編譯器后端的搭配組合。
接下來就以細(xì)分的模塊為例眯杏,總結(jié)一下每個模塊由什么負(fù)責(zé)夜焦,分別都做了哪些工作。
1.預(yù)處理(preprocessing) —— 處理宏定義
預(yù)處理主要是處理一些宏定義役拴,比如:#define糊探、#include、**#if **等河闰。預(yù)處理的實(shí)現(xiàn)有很多種科平,有的編譯器會在詞法分析前先進(jìn)行預(yù)處理,替換掉所有 # 開頭的宏姜性,而有的編譯器則是在詞法分析的過程中進(jìn)行預(yù)處理瞪慧。當(dāng)分析到 # 開頭的單詞時才進(jìn)行替換。雖然先預(yù)處理再詞法分析比較符合直覺部念,但在實(shí)際使用中弃酌,GCC 使用的卻是一邊詞法分析氨菇,一邊預(yù)處理的方案。(以上這些內(nèi)容是我從《大前端開發(fā)者需要了解的基礎(chǔ)編譯原理和語言知識》直接粘貼過來的)
2.詞法分析 —— 輸出符號狀態(tài)
詞法分析的主要實(shí)現(xiàn)原理是狀態(tài)機(jī)妓湘,它逐個讀取字符查蓉,然后根據(jù)讀到的字符的特點(diǎn)轉(zhuǎn)換狀態(tài)。計算機(jī)不像人類可以直接識別源代碼的內(nèi)容榜贴,它只能一個一個的識別每個單詞豌研,詞法分析要做的就是把源代碼分割開,形成若干個單詞唬党,并標(biāo)記狀態(tài)為語法分析做準(zhǔn)備鹃共。比如,a=1” 和 “a==1”驶拱,這兩個語句霜浴,計算機(jī)在從左向右識別時,識別到第一個 “=” 時并不能判定該語句是賦值符號還是條件判斷符號蓝纲,必須結(jié)合 “=” 之后的字符才能做出正確的判斷阴孟,若是 “1” 則是賦值符號,若是 “=” 則是條件判斷符號驻龟,根據(jù)識別的不同結(jié)果温眉,進(jìn)行狀態(tài)標(biāo)記。具體的案例在原文中講的很詳細(xì)翁狐,各位讀者可以參考类溢。
3.語法分析 —— 輸出抽象語法樹(AST, Abstract Syntax Tree)
詞法分析以后,編譯器已經(jīng)知道了每個單詞的意思露懒,但這些單詞組合起來表示的語法還不清楚闯冷,這時需要編譯器前端(如:GCC或者clang)進(jìn)行語法分析。
實(shí)現(xiàn)語法分析的一個簡單的思路是模板匹配懈词,即將編程語言的基本語法規(guī)則抽象成模板進(jìn)行匹配蛇耀,符合相應(yīng)的模板,即對應(yīng)相應(yīng)的意思坎弯,同時生成抽象語法樹(AST, Abstract Syntax Tree)纺涤。
比如有 “int a = 10;” 語句,其實(shí)表示了這么一種通用的語法格式:
“類型 變量名 = 常量;”抠忘。
而在成功解析語法以后撩炊,我們會得到抽象語法樹(AST: Abstract Syntax Tree)。
以這段代碼為例:
int fun(int a, int b) {
int c = 0;
c = a + b;
return c;
}
它的語法樹如下:
語法樹將字符串格式的源代碼轉(zhuǎn)化為樹狀的數(shù)據(jù)結(jié)構(gòu)崎脉,更容易被計算機(jī)理解和處理拧咳。
4.生成中間代碼IR(Intermeidate Representation)
其實(shí)抽象語法樹可以直接轉(zhuǎn)化為目標(biāo)代碼(匯編代碼)。然而囚灼,不同的 CPU 的匯編語法并不一致骆膝,比如《AT&T與Intel匯編風(fēng)格比較》這篇文章所提到的砰嘁,Intel 架構(gòu)和 AT&T 架構(gòu)的匯編碼中序臂,源操作數(shù)和目標(biāo)操作數(shù)位置恰好相反诊县。Intel 架構(gòu)下操作數(shù)和立即數(shù)沒有前綴但** AT&T 架構(gòu)有密似。因此一種比較高效的做法是先生成語言無關(guān)秀撇,CPU 也無關(guān)的中間代碼(IR)**燕酷,然后再生成對應(yīng)各個 **CPU **的匯編代碼粮揉。
生成中間代碼(IR,Intermediate Representation)是非常重要的一步膊畴,一方面它和語言無關(guān),也和 CPU 與具體實(shí)現(xiàn)無關(guān)锥涕。可以理解為中間代碼是一種非常抽象狭吼,又非常普適的代碼层坠。它客觀中立的描述了代碼要做的事情,如果用中文刁笙、英文來分別表示** C 和 Java 的話破花,中間碼某種意義上可以被理解為世界語**。
另一方面疲吸,中間碼是編譯器前端和后端的分界線座每。編譯器前端負(fù)責(zé)把源碼轉(zhuǎn)換成中間代碼(IR),編譯器后端負(fù)責(zé)把中間碼轉(zhuǎn)換成目標(biāo)代碼(匯編代碼)摘悴。
抽象語法樹生成中間碼(IR)的過程大體上分為兩步峭梳,第一步是生成中間碼(IR),第二步是將中間碼(IR)最佳化蹂喻。
以** GCC** 為例葱椭,生成中間代碼可以分為三個步驟:(
1.語法樹轉(zhuǎn)高端 gimple
2.高端 gimple 轉(zhuǎn)低端 gimple
3.低端 gimple 經(jīng)過 cfa 轉(zhuǎn) ssa 再轉(zhuǎn)中間代碼
注:使用Xcode 進(jìn)行iOS 客戶端開發(fā)時,LLVM 負(fù)責(zé)將抽象語法樹轉(zhuǎn)為中間碼(IR)口四。
** Gimple** 是GCC編譯器產(chǎn)生的一種包含最多三個操作數(shù)的中間指令孵运,也就是編譯原理里講的四元碼(三個操作數(shù),一個操作符)蔓彩,基本上也就是 dst = src1 @ src2 的這種形式治笨。由于** Gimple** 最多只能對兩個操作數(shù)進(jìn)行計算,因此一個復(fù)雜的表達(dá)式會展開為一系列的** Gimple** 指令赤嚼,這一過程就是** Gimple** 化旷赖。
4.1 語法樹轉(zhuǎn)高端 Gimple
語法樹轉(zhuǎn)高端 Gimple,主要是處理寄存器和棧探膊,比如:“ c = a + b” 并沒有直接的匯編代碼與之對應(yīng)杠愧,一般來說需要把 a + b 的結(jié)果保存到寄存器中,然后再把寄存器賦值給 c逞壁。另外流济,調(diào)用一個新的函數(shù)時會進(jìn)入到函數(shù)自己的棧锐锣,建棧的操作也需要在 Gimple 中聲明。
4.2 高端 Gimple轉(zhuǎn)低端Gimple
高端 Gimple轉(zhuǎn)低端 Gimple绳瘟,主要是把變量定義雕憔,語句執(zhí)行和返回語句區(qū)分存儲,分配椞巧空間斤彼。
比如:
int a = 1;
a++;
int b = 1;
會被處理成:
int a = 1;
int b = 1;
a++;
這樣做的好處是很容易計算一個函數(shù)到底需要多少棧空間蘸泻。
此外琉苇,return 語句會被統(tǒng)一處理,放在函數(shù)的末尾悦施,比如:
if (1 > 0) {
return 1;
}
else {
return 0;
}
會被處理成:
if (1 > 0) {
goto a;
}
else {
goto b;
}
a:
return 1;
b:
return 0;
4.3 低端Gimple轉(zhuǎn)中間代碼
這一步主要是進(jìn)行各種優(yōu)化并扇,添加版本號等。
5.生成目標(biāo)代碼(匯編代碼)
目標(biāo)代碼也可以叫做匯編代碼抡诞。由于中間碼已經(jīng)非常接近于實(shí)際的匯編代碼穷蛹,它幾乎可以直接被轉(zhuǎn)化。主要的工作量在于針對不同的CPU生成不同的匯編代碼昼汗,兼容各種 CPU 以及填寫模板肴熏。在最終生成的匯編代碼中,不僅有匯編命令顷窒,也有一些對文件的說明蛙吏。
6.匯編(assembly)——匯編器生成二進(jìn)制機(jī)器碼
匯編器會接收匯編代碼,將它轉(zhuǎn)換成二進(jìn)制的機(jī)器碼蹋肮,生成目標(biāo)文件(后綴是 .o)出刷,機(jī)器碼可以直接被 CPU 識別并執(zhí)行。由于目標(biāo)代碼是分段的坯辩,最終的目標(biāo)文件(機(jī)器碼)也是分段的馁龟。這是因為:
- 數(shù)據(jù)和代碼區(qū)分開。其中代碼只讀漆魔,數(shù)據(jù)可寫坷檩,方便權(quán)限管理,避免指令被改寫改抡,提高安全性矢炼。
- 現(xiàn)代 CPU 一般有自己的數(shù)據(jù)緩存和指令緩存,區(qū)分存儲有助于提高緩存命中率阿纤。
- 當(dāng)多個進(jìn)程同時運(yùn)行時句灌,他們的指令可以被共享,這樣能節(jié)省內(nèi)存。
7.鏈接(linking)
鏈接就是將目標(biāo)文件(.o文件)與其中調(diào)用的外部函數(shù)所在的目標(biāo)文件通過重定位關(guān)聯(lián)起來胰锌。
在一個目標(biāo)文件中骗绕,不可能所有變量和函數(shù)都定義在文件內(nèi)部。比如** strlen 函數(shù)就是一個被調(diào)用的外部函數(shù)资昧,此時就需要把 main.o 這個目標(biāo)文件和包含了 strlen 函數(shù)實(shí)現(xiàn)的目標(biāo)文件鏈接起來酬土。我們知道函數(shù)調(diào)用對應(yīng)到匯編其實(shí)是 jump 指令,后面寫上被調(diào)用函數(shù)的地址格带,但在生成 main.o 的過程中撤缴, strlen() 函數(shù)的地址并不知道,所以只能先用 0 來代替叽唱,直到最后鏈接**時屈呕,才會修改成真實(shí)的地址。
鏈接器就是靠著重定位表來知道哪些地方需要被重定位的尔觉。每個可能存在重定位的段都會有對應(yīng)的重定位表凉袱。在鏈接階段,鏈接器會根據(jù)重定位表中侦铜,需要重定位的內(nèi)容,去別的目標(biāo)文件中找到地址并進(jìn)行重定位钟鸵。
有時候我們還會聽到動態(tài)鏈接這個名詞钉稍,它表示重定位發(fā)生在運(yùn)行時而非編譯后。動態(tài)鏈接可以節(jié)省內(nèi)存棺耍,但也會帶來加載的性能問題贡未,這里不詳細(xì)解釋,感興趣的讀者可以閱讀《程序員的自我修養(yǎng)》這本書蒙袍。(以上是從《大前端開發(fā)者需要了解的基礎(chǔ)編譯原理和語言知識》直接粘貼過來的)
寫在最后
最后十分感謝《大前端開發(fā)者需要了解的基礎(chǔ)編譯原理和語言知識》的作者能夠貢獻(xiàn)如此高質(zhì)量的一篇文章俊卤,講清楚了整個編譯過程中的細(xì)節(jié)。本文有大量內(nèi)容是直接從該文章中復(fù)制過來害幅,最終的版權(quán)歸原作者所有消恍,本人只是作為一個讀后筆記寫下本文,若有侵權(quán)的地方以现,煩請告知狠怨。
參考文獻(xiàn)