目錄
一、編譯系統(tǒng)的形成與發(fā)展
? ? 1.1 手工硬件編程
? ? 1.2 面向硬件編程
? ? 1.3 高級語言編程
? ? 1.4 編譯系統(tǒng)的組成
二竣稽、編譯系統(tǒng)的邏輯結(jié)構(gòu)
? ? 2.1 狹義編譯
? ? 2.2 最狹義編譯
? ? 2.3 鏈接過程
? ? 2.4 組建系統(tǒng)
三、編譯原理簡介
? ? 3.1 詞法分析
? ? 3.2 語法分析
? ? 3.3 語義分析
? ? 3.4 中間碼生成
? ? 3.5 中間碼優(yōu)化
? ? 3.6 機器碼生成
? ? 3.7 機器碼優(yōu)化
? ? 3.8 小型編譯器推薦
四歧斟、靜態(tài)鏈接與動態(tài)鏈接
? ? 4.1 靜態(tài)鏈接
? ? 4.2 動態(tài)鏈接
? ? 4.3 實例解析
五穷遂、總結(jié)回顧
一、編譯系統(tǒng)的形成與發(fā)展
什么是編譯器疹味?
為什么要有編譯器?
編譯器的作用是什么帜篇?
編譯系統(tǒng)的組成部分有哪些糙捺,它們之間的關(guān)系是什么?
有一句名言說的非常好:了解一件事情最好從它的歷史開始笙隙。要想對整個編譯系統(tǒng)有個全面透徹地理解洪灯,我們就必須要先去認真研究它的發(fā)展歷史。下面我們就來看一下編譯系統(tǒng)的發(fā)展歷史竟痰。
1.1 手工硬件編程
最早開始的時候签钩,我們是手工硬件編程,注意是手工硬件編程坏快,不是面向硬件編程铅檩。手工硬件編程是程序員直接用手去改變計算機中的跳線連接方式來編程。把所有的跳線連接都改完之后莽鸿,插上電源昧旨,按執(zhí)行按鈕,計算機就可以執(zhí)行程序了祥得。很顯然這種編程方式很原始兔沃、很low、很麻煩级及。如果我們有兩個程序A和B乒疏,我們執(zhí)行完A之后想要執(zhí)行B,就要先把A毀掉之后再把B給手工編到硬件上去才能執(zhí)行B饮焦。下次再需要執(zhí)行A的時候還需要重新再把A編到硬件上缰雇。這是多么的麻煩入偷,于是大神馮諾依曼就提出了一個想法叫做存儲程序設(shè)計追驴,就是我們把程序存儲到存儲器上械哟,計算機每次運行的時候都去存儲器讀指令來執(zhí)行就可以了。存儲程序設(shè)計是一個非常偉大的想法殿雪,是計算機歷史上發(fā)展的一大步暇咆。以前當我看到書上對存儲程序設(shè)計大加贊賞時,我就非常疑惑不解丙曙,存儲程序爸业,還用你說,程序不是本來就存在硬盤上嗎亏镰,這不是理所應(yīng)當?shù)膯岢犊酰@有什么好夸獎的。當我們對一個東西習以為常索抓、司空見慣的時候钧忽,我們就會覺得這個東西沒什么,但是實際上這個東西的提出或者發(fā)明是非常偉大的逼肯。就好比我們都站著走路覺得這個沒什么耸黑,這不是應(yīng)該的嗎,本來就是這樣篮幢。但是實際上人類直立行走是人從動物進化到人的過程中非常重要的一步大刊,如果沒有直立行走,人類現(xiàn)在說不定還是生活在原始森林里的動物三椿。好缺菌,我們接著往下說,有了存儲程序設(shè)計搜锰,我們就從手工硬件編程進化到了面向硬件編程伴郁。
1.2 面向硬件編程
面向硬件編程又分為1.0 二進制編程、2.0 手工匯編編程纽乱、3.0 匯編編程三個版本蛾绎。由于計算機硬件采取的是二進制實現(xiàn)方式,所以1.0版本的面向硬件編程就是二進制編程鸦列,就是直接用一堆0110101來編程租冠。計算機為什么要采用二進制的方式來實現(xiàn)而不是采用人類最熟悉的十進制方式或者其他進制方式來實現(xiàn)呢?這里面有多個原因薯嗤,首先是因為二進制是最簡單的進制顽爹,不可能有比二進制更簡單的進制了。二是因為硬件的很多特征正好可以簡單對應(yīng)到二進制的0和1上骆姐,比如高電壓是1镜粤,低電壓是0捏题,通路是1,斷路是0肉渴,非常方便公荧,便于實現(xiàn)。三是因為邏輯運算布爾代數(shù)正好可以對應(yīng)到二進制0和1上同规。如果采取3進制循狰、5進制、8進制券勺、10進制绪钥,將會面臨著很多的麻煩。采用二進制也存在一個問題关炼,就是人類熟悉10進制程腹,不熟悉2進制,這個問題可以通過在顯示的時候把2進制轉(zhuǎn)換為10進制來解決儒拂,人們用電腦時看到的都會是10進制數(shù)寸潦,沒有影響。
有了二進制編程侣灶,我們就可以直接書寫處理器能直接識別和執(zhí)行的二進制指令來編程了甸祭,比手工硬件編程方便了很多。但是顯然直接書寫一堆0110101的代碼褥影,很違背直覺池户,很難寫,也很容易出錯凡怎。如果不小心把某個0寫成了1校焦,想把這個錯誤找出來都非常困難。于是人們想出了一個方法统倒,我先在草稿紙上用偽代碼編程寨典,比如,加法的指令是10101010房匆,我先用 ADD 來代替耸成。等把程序?qū)懞昧耍以僖粋€一個地把 ADD SUB JMP 等這些指令助記符手工轉(zhuǎn)換成0110浴鸿,再輸入到電腦上去執(zhí)行井氢。我們把這種編程方式叫做手工匯編編程,這種編程方式和手工硬件編程岳链、二進制編程相比都有很大的進步花竞,但是依然很麻煩。于是我們就想掸哑,把助記符轉(zhuǎn)換成二進制指令的這個過程约急,我能不能用程序來實現(xiàn)呢零远,何必非要自己手工去做呢,于是就有了匯編器程序厌蔽。我們把二進制指令叫做機器語言牵辣,把助記符指令叫做匯編語言,你用匯編語言寫的程序叫做匯編程序,匯編器程序把你寫的匯編程序轉(zhuǎn)換成二進制程序,然后就可以運行了彬犯。匯編器程序也是程序啊婿着,該用什么語言來寫呢,顯然此時只能用二進制編程或者手工匯編編程來寫匯編器程序近她,我們把這個匯編器叫做盤古匯編器叉瘩。盤古匯編器的特點是它是手工產(chǎn)生的,不需要匯編或者編譯來生成粘捎。盤古匯編器有了之后薇缅,就可以對所有的匯編程序進行匯編了,此時我們就進入了匯編編程時代攒磨。既然所有的程序都能用匯編語言編寫了泳桦,那么我們能不能用匯編語言再寫一個匯編器呢,答案是能娩缰,我們把這個匯編器叫做女媧匯編器灸撰。我們用盤古匯編器匯編女媧匯編器的源碼,得到二進制的女媧匯編器拼坎,然后就可以用二進制的女媧匯編器匯編女媧匯編器的源碼浮毯,又得到了新的二進制女媧匯編器。從此我們就可以拋棄盤古匯編器了泰鸡,實現(xiàn)二進制女媧匯編器和源碼女媧匯編器的雞生蛋债蓝、蛋生雞的循環(huán)了。我們就可以不斷地改善女媧匯編器的源碼盛龄,實現(xiàn)女媧匯編器的進化饰迹,并且還可以編譯其他匯編程序。
第一個編譯器是怎么來的余舶?
在此我們提前回答一個問題啊鸭,編譯器也是程序,也需要被編譯欧芽,那么第一個編譯器是怎么來的莉掂,是誰編譯的呢。第一個C語言編譯器是用B語言寫的千扔,用B語言編譯器編譯的憎妙,有個這個盤古C語言編譯器库正,我們就可以再用C語言來重新寫一個C語言編譯器了,叫做女媧C語言編譯器厘唾。用二進制的C語言盤古編譯器編譯源碼形式的女媧C語言編譯器褥符,就得到了二進制的女媧C語言編譯器,然后二進制的女媧C語言編譯器再去編譯源碼形式的女媧C語言編譯器抚垃,就可以實現(xiàn)類似女媧匯編器的循環(huán)了喷楣。同理你會問那么第一個B語言編譯器是怎么來的呢,第一個B語言編譯器是用BCPL語言書寫的鹤树,是用BCPL語言編譯器編譯的铣焊。一直這么追問下去,第一個編譯器是怎么實現(xiàn)的呢罕伯?第一個編譯器是用匯編語言寫的曲伊,匯編器匯編出來的。再往前推追他,第一個匯編器是手工書寫的二進制代碼坟募。所以編譯器的源頭是我們?nèi)祟愔苯佑脵C器語言書寫的盤古匯編器,它不需要編譯也不需要匯編邑狸,能直接在機器上運行懈糯。
1.3 高級語言編程
面向硬件編程3.0版本--匯編編程,雖然已經(jīng)很先進了单雾,但它仍然是面向硬件編程啊赚哗,程序員仍然需要了解很多的硬件細節(jié),不能專注于程序自身的邏輯铁坎。實際上絕大部分程序蜂奸,它的功能實現(xiàn)都和硬件無關(guān),而且用匯編語言寫的程序不具有跨平臺性硬萍,程序員要為每個硬件平臺都寫一份程序或者要移植一下扩所,這非常麻煩。能不能發(fā)明一種語言朴乖,屏蔽各種硬件細節(jié)祖屏,只注重于實現(xiàn)程序自身的邏輯,實現(xiàn)源碼級的跨平臺性呢买羞,于是高級語言誕生了袁勺。第一個高級語言大概是Algol吧,然后從Algol一路發(fā)展到BCPL畜普、B期丰、C、C++、JAVA钝荡、C# 等街立。在高級語言里面沒有各種硬件指令,沒有寄存器埠通,沒有復(fù)雜的尋址模式赎离,取而代之的是各種數(shù)據(jù)類型的數(shù)據(jù)定義,和對數(shù)據(jù)的直觀操作端辱,以及if梁剔、for、while舞蔽、switch等控制語句荣病,還有一些更高級的語法構(gòu)造如類、模板等喷鸽,都是用接近于人類自然語言的方式來表達的众雷,可讀性非常強。如果想要了解學習一些高級語言做祝,如C和C++,可以參看《深入理解C與C++》鸡岗。
編譯器的定義:
好混槐,說到這里,大家對什么是編譯器心里應(yīng)該已經(jīng)很明白了轩性。我們來總結(jié)一下声登,什么是編譯器,編譯器是人類和計算機之間的一個矛盾的產(chǎn)物揣苏。這個矛盾就是計算機能夠理解和執(zhí)行二進制格式的程序卻不能理解和執(zhí)行文本格式的程序悯嗓。而人類正好相反,人類能理解和書寫文本格式的程序卻難以理解和書寫二進制格式的程序卸察。于是編譯器出現(xiàn)了脯厨,用來幫助人類解決這個矛盾。人類書寫文本格式的程序坑质,編譯器給翻譯成二進制格式的程序合武,計算機再來執(zhí)行。
1.4 編譯系統(tǒng)的組成
上面我們把什么是編譯器給解釋清楚了涡扼,我們再繼續(xù)順著這個思路來解釋一下什么是鏈接稼跳、加載和組建。剛開始的時候吃沪,程序都很簡單汤善,一個C文件就搞定了,我們可以把一個C文件直接編譯成可執(zhí)行程序。但是后來隨著程序越來越龐大红淡,再把程序全寫到一個C文件里就不合適了不狮,一個C文件寫幾千行還算合理,但是寫幾萬行锉屈,幾十萬行甚至幾百萬行就不合理了荤傲。于是我們把程序?qū)懙蕉鄠€文件里,每個文件單獨編譯颈渊,生成中間目標文件遂黍,再把這些中間目標文件連接合并成一個可執(zhí)行程序,這個連接合并過程就叫做鏈接俊嗽,執(zhí)行鏈接的程序叫做鏈接器雾家。后來隨著程序的更加復(fù)雜龐大,這種鏈接方式也有問題绍豁,因為有很多公共程序芯咧,你把它們都鏈接到每個程序中,這樣每個程序都自己包含一份庫程序竹揍,這會導致浪費大量的磁盤空間敬飒,而且運行時也很浪費物理內(nèi)存,如果一個庫程序更新了芬位,每個可執(zhí)行程序都要重新鏈接一遍无拗,也很麻煩。為了解決這幾個問題昧碉,于是誕生了動態(tài)鏈接英染,于是就把之前的鏈接方式叫做靜態(tài)鏈接。我們把一些公共庫程序做成動態(tài)鏈接庫被饿,每個可執(zhí)行程序鏈接到動態(tài)庫時只是在程序內(nèi)部做了一個鏈接記錄四康,并不會把動態(tài)庫復(fù)制到可執(zhí)行程序本身,這樣整個磁盤里面就只有一份這個動態(tài)庫狭握。運行時闪金,這個動態(tài)庫也只需要加載進物理內(nèi)存一次,然后分別映射到不同進程的虛擬內(nèi)存空間中就可以了哥牍,這個動作叫做加載毕泌。至此我們明白了編譯、鏈接嗅辣、靜態(tài)鏈接撼泛、動態(tài)鏈接、加載這幾個概念澡谭,做編譯的是編譯器愿题,做鏈接的是鏈接器损俭,做加載的是加載器。
下面我們說一說什么是組建潘酗,假設(shè)我們有個軟件由十幾個文件夾幾百個C文件組成杆兵,我們每次想編譯時該怎么做,一個一個地編譯每個C文件仔夺,然后再把所有的中間目標文件靜態(tài)鏈接琐脏、動態(tài)鏈接起來生成最終的可執(zhí)行程序,每次都手工輸入那么多命令是多么麻煩啊缸兔。我們該怎么辦日裙,一個最簡單直接的方式是把這些命令都寫到腳本里,后面每次都執(zhí)行這個腳本就可以了惰蜜。這個想法很好昂拂,也解決了問題,但是還存在問題抛猖,就是寫這個腳本比較麻煩格侯,修改這個腳本也比較麻煩。于是就出現(xiàn)了一些可以自動生成這個腳本的方法财著,你寫一些簡單的配置和規(guī)則联四,然后用一個程序處理這個規(guī)則,就可以自動生成這個腳本撑教,再執(zhí)行這個腳本就可以編譯整個程序了碎连。后來你發(fā)現(xiàn),你的解析程序解析你的規(guī)則文件后驮履,直接在內(nèi)部生成這個腳本執(zhí)行這個腳本就可以了,沒必要非要把這個腳本顯式地寫出來再去執(zhí)行廉嚼,這一套東西就叫做組建系統(tǒng)玫镐。組建系統(tǒng)由兩部分組成,解析程序和規(guī)則文件怠噪。最著名的組建系統(tǒng)就是make了恐似,make由兩部分組成,make程序和Makefile文件傍念。你按照Makefile的要求去書寫Makefile文件矫夷,然后執(zhí)行make命令就可以編譯整個程序了。后來隨著程序的更加龐大和復(fù)雜化憋槐,Makefile也變得越來越龐大越難以書寫了双藕,于是又產(chǎn)生了元組建系統(tǒng),來幫你生成Makefile文件阳仔。元組建系統(tǒng)也由兩部分組成忧陪,解析程序和規(guī)則文件,你按照它的要求書寫規(guī)則文件,然后執(zhí)行解析程序生成Makefile嘶摊,然后再執(zhí)行make命令編譯整個程序延蟹。針對make的比較著名的元組建系統(tǒng)有CMake、autoconf/automake等叶堆。由于make運行時需要把每個文件夾下Makefile都include進來阱飘,這對于小規(guī)模的程序來說還能忍受,但是當程序非常龐大虱颗、源碼下的文件夾比較多的時候沥匈,這個耗時就難以忍受了,于是人們開發(fā)出了ninja組建系統(tǒng)上枕,它運行時只解析一個build文件咐熙,運行效率就比較高。不過要手工寫ninja的build文件是很麻煩的辨萍,對大型程序來說幾乎是不現(xiàn)實的棋恼,所以對ninja組建系統(tǒng)幾乎是必配元組建系統(tǒng),當只修改源碼內(nèi)容時可以直接運行ninja命令锈玉,但是當程序結(jié)構(gòu)發(fā)生變化時必須要先運行一下元組建系統(tǒng)重新生成一下ninja的build文件爪飘。
現(xiàn)在我們看到整個編譯系統(tǒng)由3部分組成,編譯拉背、鏈接师崎、組建,執(zhí)行它們的分別是編譯器椅棺、鏈接器犁罩、組建系統(tǒng)。我們在命令行編譯一個程序的時候两疚,只需要調(diào)用一個組建命令床估,組建系統(tǒng)就會幫我們自己編譯、鏈接整個程序诱渤,還會做一些其他輔助工作如打包丐巫、簽名等。有些同學可能會說我從來沒用過什么編譯器勺美、鏈接器递胧、組建系統(tǒng),我每次編程都是直接寫程序赡茸,然后直接按一個按鈕就行了缎脾,這個東西就叫做集成開發(fā)環(huán)境,它把文本編輯器坛掠、編譯器赊锚、鏈接器治筒、組建系統(tǒng)、調(diào)試器都集成在一起了舷蒲,可以方便大家的開發(fā)工作耸袜,但也使得很多人都不了解它背后的工作原理。所以說命令行雖然不太好用牲平,但是它的不太好用是指學會用它比較麻煩堤框,但是一旦學會用它,你會發(fā)現(xiàn)它非常強大纵柿,而且同時你對它背后的原理也理解地比較深刻蜈抓。
很多時候我們在工作的時候會經(jīng)常說到編譯這個詞,但是在不同的場景下它的含義是不一樣的昂儒。下面我們來進行一下名詞解析沟使,說一下編譯的四種不同含義
最廣義的編譯:就是組建過程,包括編譯渊跋、鏈接腊嗡、打包、簽名等
廣義的編譯:包括狹義的編譯和鏈接
狹義的編譯:就是單指編譯拾酝,把源文件編譯成目標文件的過程
最狹義的編譯:就是編譯原理中的編譯燕少,不包括開頭的預(yù)處理和最后的匯編過程(如果有的話)。
編譯原理里面講的編譯就是指這個最狹義的編譯蒿囤,一般是沒有這個匯編過程的客们,就是直接生成目標文件,如果有匯編過程的則不包括匯編過程材诽,很多編譯器采取的方式是有匯編的過程的底挫。
為什么要在這里做個名詞解析呢,因為在工作和一些討論中會因為大家說的編譯這個詞的含義不同而產(chǎn)生一些說不清的爭論脸侥。比如說我們在編譯整個程序的時候凄敢,我們只會說編譯這個程序,而不會去說組建這個程序湿痢,也不會去說編譯加鏈接加打包這個程序,這樣說會顯得很別扭扑庞,所以很多時候會因為編譯的含義不同而產(chǎn)生一些不必要的爭論譬重。例如有一次服務(wù)器編譯出錯了,我說編譯出錯了罐氨,有一個同事說編譯沒有錯臀规,是打包出錯了。還有一次有個同事編譯出錯了栅隐,讓我?guī)兔纯词窃趺椿厥滤遥铱戳酥笳f編譯過程沒問題玩徊,鏈接階段出錯了,找不到符號谨究,他一臉疑惑的說恩袱,這編譯不是出錯了嗎。還有些同學不明白編譯原理中的編譯和工作中的編譯的不同胶哲,會問一些非常有意思的問題畔塔,比如有人問過我,預(yù)處理的時候為什么不能提前發(fā)現(xiàn)語法錯誤呢鸯屿,我說預(yù)處理的時候還沒到編譯階段呢澈吨,他說預(yù)處理不是編譯嗎。如果我們知道了編譯的四個不同廣度的含義就能更好地溝通清楚我們要表達的意思寄摆。
二谅辣、編譯系統(tǒng)的邏輯結(jié)構(gòu)
前面一章我們對編譯的概念和編譯系統(tǒng)的組成已經(jīng)有了基本的了解,這一章我們來具體講講它們之間的結(jié)構(gòu)關(guān)系和每個組成部分的功能作用婶恼。
2.1 狹義編譯
我們首先來看一下狹義編譯:
狹義編譯是對一個源文件和n個頭文件進行的編譯桑阶,它包含預(yù)處理、最狹義編譯和匯編三個過程熙尉。預(yù)處理是對源文件中的預(yù)處理指令進行處理的過程联逻,預(yù)處理指令包括頭文件包含指令(#include)、宏定義指令(#define)检痰、取消宏定義指令(#undef)包归、條件編譯指令(#if #ifdef #ifndef #else #elif #endif)和其他一些編譯指令。預(yù)處理過程會把所有被包含的頭文件插入到源文件中铅歼,并對其它預(yù)處理指令進行處理公壤,最終生成一個編譯單元 .i文件。然后對編譯單元進行最狹義的編譯椎椰,也就是編譯原理里面所說的編譯厦幅。編譯原理里面說最狹義編譯最后可以生成匯編文件也可以直接生成目標文件沒有匯編過程,但是書里一般都是按照直接生成目標文件來講的慨飘,而現(xiàn)實中的編譯器一般都是生成匯編文件确憨,最后再經(jīng)歷一個匯編過程。匯編是把匯編文件生成目標文件的過程瓤的,目標文件里面包含可直接執(zhí)行的機器指令休弃,但是整個文件還不是可執(zhí)行格式。
2.2 最狹義編譯
最狹義編譯是編譯原理中的編譯圈膏。編譯原理是一門非常艱深課程塔猾,是計算機科學中最有技術(shù)含量的領(lǐng)域之一,是理論和實現(xiàn)都極其難以理解的一門科學稽坤。我們這里不準備詳細講解編譯原理的知識丈甸,只是對其框架進行大概的介紹糯俗。我們先看一張圖:
我們按照直接生成目標文件的方式來講,因為這樣整個編譯器結(jié)構(gòu)比較完整睦擂,如果是生成匯編文件的話得湘,那么機器碼生成這塊就不存在了。編譯器一般都分成前端和后端兩個部分祈匙,前端負責對語言本身進行解析忽刽,后端負責機器碼生成。為什么要分成前端和后端兩個部分呢夺欲,因為前端和后端并不是必然關(guān)聯(lián)的跪帝,分開之后可以有更大的靈活性。靈活性主要有兩個方面些阅,對于同一種語言來說伞剑,語法解析我們只需要實現(xiàn)一次就行了,當語言需要移植到另一個CPU架構(gòu)時市埋,只需要實現(xiàn)后端就可以了黎泣。對于不同的語言來說,對于同一個CPU架構(gòu)我們沒必要實現(xiàn)多個后端缤谎,所有的語言都可以共用同一個后端抒倚,當編譯器需要支持新的語言的時候,只需要實現(xiàn)前端就行了坷澡。前端包括詞法分析托呕、語法分析(句法分析)、語義分析频敛,后端包括中間碼生成项郊、中間碼優(yōu)化、機器碼生成斟赚、機器碼優(yōu)化着降,下一章將會對其進行稍微詳細的介紹。
2.3 鏈接過程
經(jīng)過狹義編譯之后我們得到了目標文件拗军,但是目標文件并不是最終的可執(zhí)行程序任洞,我們需要把目標文件鏈接起來才能生成可執(zhí)行程序。程序執(zhí)行時生成進程发侵,進程是由一個exe主程序和n個so庫程序組成,主程序和庫程序都是由目標文件和靜態(tài)庫文件通過靜態(tài)鏈接生成的器紧。靜態(tài)鏈接分為隱式靜態(tài)鏈接和顯式靜態(tài)鏈接,隱式靜態(tài)鏈接是目標文件和目標文件直接合并在一起楼眷,顯式靜態(tài)鏈接是目標文件和靜態(tài)庫進行鏈接铲汪,本質(zhì)上還是和靜態(tài)庫里面的目標文件進行鏈接熊尉。隱式靜態(tài)鏈接和顯式靜態(tài)鏈接區(qū)別不大,本質(zhì)上是沒有區(qū)別的掌腰,只是在編譯命令上有形式上的區(qū)別狰住。Exe對so,so對so齿梁,是動態(tài)鏈接的催植,動態(tài)鏈接分為半動態(tài)鏈接和完全動態(tài)鏈接,兩者的本質(zhì)是相同的勺择,都是動態(tài)鏈接的创南,也就是說沒有復(fù)制文件的過程,但是兩者的實現(xiàn)形式還是有很大差異的省核。半動態(tài)鏈接就是我們平常所說的動態(tài)鏈接稿辙,它在編程時需要include so的頭文件,在編譯時需要指定so所在的路徑气忠,鏈接后生產(chǎn)的文件中會記錄這個so的相關(guān)信息邻储,在進程啟動時加載器會加載這個so,在程序運行時調(diào)用了這個so的函數(shù)的時候會去動態(tài)解析符號旧噪。完全動態(tài)鏈接是指在代碼中通過函數(shù)dlopen加載相應(yīng)的so吨娜,通過函數(shù)dlsym查找要調(diào)用的函數(shù)的符號,然后去調(diào)用這個函數(shù)淘钟,使用完了之后可以通過dlclose卸載這個so宦赠。完全動態(tài)鏈接不需要include相應(yīng)的頭文件,編譯時也不需要指定so的路徑日月,運行時如果找不到so袱瓮,dlopen會返回NULL,程序不會崩潰爱咬,用完了之后還可以把so卸載了尺借。相反,對于半動態(tài)鏈接精拟,如果編譯時找不到so就會編譯不過燎斩,運行時找不到so程序就會啟動失敗,程序運行的過程中也不可能把so給卸載移出進程的內(nèi)存空間蜂绎。
下面看一下編譯時鏈接的過程:
編譯時鏈接栅表,不僅會進行靜態(tài)鏈接,也會進行半動態(tài)鏈接的第一步师枣。下面再看一下半動態(tài)鏈接的啟動加載和完全動態(tài)鏈接:
進程啟動時首先是fork創(chuàng)建進程的殼怪瓶,然后exec加載進程的主程序hello.exe和加載器ld.so。然后進程返回用戶空間践美,執(zhí)行權(quán)交給ld洗贰,ld會根據(jù)主程序文件中的動態(tài)段的信息去加載所有的so文件找岖,并完成重定位工作,最后把執(zhí)行權(quán)交給主程序敛滋。主程序首先執(zhí)行的并不是main函數(shù)许布,而是_start函數(shù),_start函數(shù)會調(diào)用 __libc_start_main函數(shù),此函數(shù)會完成進程環(huán)境的初始化绎晃,最后調(diào)用main函數(shù)蜜唾。進入了main函數(shù)就是程序執(zhí)行的主體了,程序如果執(zhí)行到了dlopen函數(shù)庶艾,就會去加載相應(yīng)的so文件坛怪。
2.4 組建系統(tǒng)
組建系統(tǒng)英語是build system宰闰,也有翻譯成構(gòu)建系統(tǒng)的。什么是組建系統(tǒng)呢,如果你每次編譯程序都要執(zhí)行一大堆gcc ld 命令楞抡,那是多么的麻煩控汉,有了組建系統(tǒng)你每次只需要執(zhí)行一個簡單的命令心铃,就能編譯整個程序了译断。下面我們以使用最普遍的組建系統(tǒng)make為例簡單地講一講。make命令會在同目錄下尋找文件Makefile积暖,然后解析并執(zhí)行它藤为。Makefile的內(nèi)容包含5個部分,1.變量定義夺刑,2.顯示規(guī)則缅疟,3.隱式規(guī)則,4.文件指示遍愿,5注釋存淫。變量定義相當于C語言中的宏,使用時會被替換到相應(yīng)的位置沼填。顯示規(guī)則說明了如何生成一個文件桅咆,顯示規(guī)則包含3部分,目標坞笙、依賴和命令岩饼。由于很多規(guī)則模式是很常用的,所以make內(nèi)置了一些規(guī)則薛夜,我們沒必要把規(guī)則寫全籍茧,make會自動推導。文件指示類似于C語言中的預(yù)處理命令梯澜,如#include可以包含下層makefile寞冯。注釋是以#開頭的行。
顯示規(guī)則的格式如下,隱式規(guī)則沒有命令部分吮龄。
三檬某、編譯原理簡介
前面我們講了一下最狹義編譯,也就是編譯原理中的編譯螟蝙。這里我們把編譯原理給大家簡單介紹一下,就不展開詳細討論了民傻,因為編譯原理實在是太深了胰默,想要深入研究的同學可以去看參考文獻里推薦的書籍。我們先來看一下編譯的總體過程:
3.1 詞法分析
什么是詞法分析漓踢,為什么要進行詞法分析呢牵署?詞法分析是把一個一個的字符變成一個一個的單詞(Token)。單詞喧半,英文是Token奴迅,漢語有翻譯成記號的,也有翻譯成符號的挺据,它是源代碼的最小邏輯單位取具。詞法分析的作用就相當于我們小時候的學的如何斷句,把一句話分成一個一個的詞扁耐,詞有名詞代詞動詞形容詞副詞介詞連詞等暇检,它們再組成了一句話的主謂賓定狀補。類似的婉称,詞法分析中從源代碼字符流中分出一個一個的單詞块仆,并對單詞進行屬性分類,方便后面進一步地分析:句法分析(語法分析)王暗,把單詞組成句子悔据。詞法分析的基本原則也比較簡單,主要就是按照空格劃分俗壹,按照換行劃分科汗,但是又不完全如此,又有一些特殊情況策肝。比如 a=b+c肛捍,雖然沒有一個空格,但是并不能整體上看成一個單詞之众,而是要把它看成是 a拙毫、=、b棺禾、+缀蹄、c,五個單詞。又比如 s = “hello world”,雖然“hello world”中間有一個空格缺前,但是并不能把它看成是兩個單詞蛀醉,而是把它看成是一個單詞。
詞法分析器分析出來的單詞都有哪些類別呢衅码,一共有5種類別拯刁,分別是關(guān)鍵字、標識符逝段、常量垛玻、操作符、分界符奶躯。關(guān)鍵字其實也算是標識符帚桩,只不過是特殊的標識符,它是計算機語言標準規(guī)定的具有特殊含義的標識符嘹黔,主要有兩類账嚎,一類是表示數(shù)據(jù)類型的,比如int儡蔓、long郭蕉、float等,另一類是表達流程的浙值,比如if恳不、while、for开呐。標識符是普通的標識符烟勋,是程序員為程序中的實體起的名字,主要分為兩類筐付,分別是變量名和函數(shù)名卵惦。常量就是程序中的字面常量,包括整數(shù)型常量瓦戚、浮點型常量沮尿、字符常量、字符串常量较解、布爾常量等畜疾。操作符是用來進行一些運算的符號,如進行算術(shù)運算的 + - x / % 等運算符印衔,進行比較操作的 > >= < <= == != 等啡捶,進行邏輯運算的 && || !等運算符奸焙,還有進行移位運算瞎暑、賦值操作彤敛、條件操作等操作符。分界符是沒有啥具體含義用來分割一些邏輯單元的邊界的符號了赌,如分號墨榄;表達一個語句的結(jié)束,大括號{}表達一個代碼塊或者函數(shù)的開始和結(jié)束勿她,小括號用來表示表達式袄秩、函數(shù)參數(shù)等的分界。
那么我們該用什么方法進行詞法分析呢逢并,比如我們可以使用正則表達式播揪,使用有限狀態(tài)機,包括不確定性有限狀態(tài)機(NFA)和確定性有限狀態(tài)機(DFA),這方面的具體細節(jié)可以參看參考文獻里的書籍筒狠。
我們該怎么得到一個詞法分析器呢?有兩種方法箱沦,一種是手工編寫代碼辩恼,一種是用工具幫你生成。手工編寫的好處是自由靈活高效谓形,缺點是麻煩費腦子灶伊,工具生成的優(yōu)點是簡單快捷,缺點是不夠靈活寒跳,沒法自定義聘萨。目前大部分主流編譯器采取的都是手工編寫詞法分析器。由于詞法分析的過程像是掃描童太,所以詞法分析又叫做掃描米辐,詞法分析器又叫做掃描器。一個比較著名的詞法分析器生成工具是flex书释。
詞法分析把源代碼的字節(jié)流變成了單詞流翘贮,并標記了單詞的類別,并把標識符的相關(guān)信息等放入了符號表爆惧,這些都會作為下一步句法分析的輸入狸页。
3.2 語法分析
我們有了單詞,單詞可以組成句子扯再,句子可以構(gòu)成段落芍耘,段落可以組成文章,一篇美妙的文章就這么誕生了熄阻。對于簡單的文學作品是這樣的斋竞,對于復(fù)雜的文學作品如《紅樓夢》《三國演義》,有更高一層的結(jié)構(gòu)饺律,一篇文章只是其中的一回窃页。同樣的跺株,一個程序也是類似的結(jié)構(gòu),簡單的程序只有一個c文件脖卖,復(fù)雜的程序有很多的c文件乒省,一個c文件就是一篇文章,是編譯的基本單元畦木。具體來說袖扛,預(yù)處理之后的c文件才是一個編譯單元。從前面的描述我們可以看出來十籍,一個編譯單元蛆封,它的結(jié)構(gòu)是樹狀的,它的葉子節(jié)點的底層節(jié)點是單詞勾栗。語法分析就是把這些底層節(jié)點(單詞)一步一步地生成一棵樹的過程惨篱,這棵樹就是抽象語法樹(AST)。這顆樹的根節(jié)點就是編譯單元围俘,編譯單元的子節(jié)點有兩類砸讳,分別是聲明和定義,聲明包括變量聲明和函數(shù)聲明界牡,定義包括變量定義和函數(shù)定義簿寂。變量聲明、函數(shù)聲明和變量定義宿亡,都比較簡單常遂,都是一句,它們的子節(jié)點很快就到了葉子節(jié)點(單詞)挽荠。函數(shù)定義比較復(fù)雜克胳,首先包括函數(shù)簽名和函數(shù)體,函數(shù)體又是由很多語句組成的圈匆,語句又有很多的類型毯欣。下面我們來畫一下編譯單元的抽象語法樹。
下面我們再來看一個具體的賦值語句 a=b+c; 的抽象語法樹臭脓。
生成一個編譯單元的抽象語法樹是一個非常復(fù)雜的事情酗钞,那么該如何生成這個抽象語法樹呢?有兩類方法来累,一類是自頂向下方法砚作,包括遞歸下降算法和LL算法,一個是自底向上方法嘹锁,包括SR算法和LR算法葫录。具體細節(jié)請參看參考文獻中的書籍。
3.3 語義分析
經(jīng)過詞法分析和語法分析领猾,我們得到了抽象語法樹米同,但是此時得到的結(jié)果只能保證程序是形式上合法的骇扇,并不能保證程序在語義上是合理的有意義的。比如給指針賦值浮點數(shù)面粮,形式上是個賦值操作少孝,實際上是沒有意義的。因此此時要進行語義分析熬苍,保證程序在邏輯上是有意義的稍走,沒法發(fā)現(xiàn)語義不合法的就報錯,停止編譯柴底。
3.4 中間碼生成
現(xiàn)在的編譯器都不是直接生成機器代碼婿脸,而是先生成一種中間語言的代碼。這么做是為了柄驻,1.使得所有的語言都可以用同一個后端狐树,同一個語言可以輕松添加對新CPU架構(gòu)的支持,2.所有語言都可以使用同樣的機器無關(guān)的代碼優(yōu)化方法鸿脓,避免重復(fù)實現(xiàn)優(yōu)化算法褪迟。中間語言一般采取三地址碼的形式,每個三地址碼都是一個四元組(運算符答憔,操作數(shù)1,操作數(shù)2掀抹,結(jié)果),由于每個四元組都包含了三個變量虐拓,即每條指令最多有三個操作數(shù),所以被稱為三地址碼傲武。中間碼生成是把抽象語法樹轉(zhuǎn)化為中間語言的過程蓉驹。
3.5 中間碼優(yōu)化
對中間碼進行優(yōu)化,優(yōu)化的原則是不改成程序本身的執(zhí)行結(jié)果的情況下揪利,減少程序執(zhí)行的代碼量态兴,能提高程序執(zhí)行的效率。優(yōu)化的方法有疟位,常量表達式優(yōu)化瞻润,直接把常量表達式的結(jié)果計算出來,避免其在運行時再去計算甜刻。公共子表達式消除绍撞,若兩個表達式有相同的子表達式,則只對它們計算一遍得院,復(fù)用計算的結(jié)果傻铣。復(fù)制傳播,某些變量的值并未被改變過便賦給其他變量祥绞,則可直接引用原值本身非洲。死代碼消除鸭限,有些代碼不會被執(zhí)行到,把它們刪除两踏。無用代碼消除败京,有些代碼的執(zhí)行沒有意義,對程序沒有作用缆瓣,把它們刪除喧枷。代碼外提,有些在循環(huán)中的代碼弓坞,在每次循環(huán)中的計算結(jié)果都是一樣的隧甚,把它放在循環(huán)外只計算一遍就行了。還有很多其他優(yōu)化方法就不一一列舉了渡冻,具體情況請參看參考文獻中的書籍戚扳。
3.6 機器碼生成
編譯程序的目的是為了把源碼翻譯成最終能在機器上執(zhí)行運行的程序,所以編譯器最后要把中間碼轉(zhuǎn)換為機器碼族吻。
3.7 機器碼優(yōu)化
生成目標代碼時要考慮優(yōu)化帽借,生成最高效的代碼,有三個因素需要考慮超歌,1.同一個效果可能有多個不同的指令都能實現(xiàn)砍艾,選擇最優(yōu)的指令,2.充分利用寄存器巍举,減少訪問內(nèi)存的次數(shù)脆荷,3.在不影響程序正確性的情況下對指令進行重排序,提高代碼的運行效率懊悯。
3.8 小型編譯器推薦
當我們學習了編譯原理之后蜓谋,非常想通過研究實際的編譯器代碼來深入理解編譯原理。但是目前最流行的兩個編譯器GCC炭分、LLVM代碼都非常龐大桃焕,不適合拿來入手。為此我推薦幾個市面上比較流行的小型編譯器捧毛,代碼都比較短小簡單观堂,大家可以用來研究編譯原理。
1.c4
一個非常簡單的解釋型C編譯器呀忧,整個代碼只有一個文件500多行型将,包含4個函數(shù)〖雠埃可以解釋執(zhí)行一些簡單的C程序七兜,更神奇的是可以解釋執(zhí)行自己。
源碼地址:https://github.com/rswier/c4
2.ucc
ucc是曾任小米公司MIUI首席架構(gòu)師的汪文俊同學在研究生期間花了一年多的時間寫的福扬。Ucc 代碼簡潔易懂腕铸,結(jié)構(gòu)清晰惜犀,易于學習和掌握。鄒昌偉編寫的書籍《C編譯器剖析》就是以ucc為源代碼來解析編譯器實現(xiàn)的狠裹。
源碼地址:https://github.com/sheisc/ucc162.3
3.chibicc
一個簡單而又優(yōu)雅的小型編譯器虽界,實現(xiàn)了大多數(shù) C11 特性,而且能成功編譯git涛菠、sqlite等源碼莉御。
源碼地址:https://github.com/rui314/chibicc
4.tcc
一個小型但是又非常完整的編譯器,不僅實現(xiàn)了編譯器前端俗冻,還實現(xiàn)了匯編器和鏈接器礁叔。
源碼地址:https://github.com/TinyCC/tinycc
四、靜態(tài)鏈接與動態(tài)鏈接
當程序變得越來越龐大之后迄薄,我們就會對程序進行模塊劃分琅关。最先使用的模塊方式是文件,把程序放到不同的源代碼中讥蔽,最后編譯鏈接成一個可執(zhí)行文件涣易。當程序變得更龐大了之后,我們會把程序劃分為好幾個執(zhí)行體冶伞,每個執(zhí)行體實現(xiàn)一個單獨的功能邏輯新症。執(zhí)行體分為主執(zhí)行體,也就是exe可執(zhí)行程序响禽,和庫執(zhí)行體徒爹,也就是so動態(tài)共享庫。主執(zhí)行體有且只有一個金抡,庫執(zhí)行體有n個,n 大于等于 0腌且。執(zhí)行體一般對應(yīng)一個源碼文件夾梗肝,源碼文件夾還可以有多個層級。我們對每個源碼文件進行編譯铺董,得到一個目標文件巫击,執(zhí)行體都是由若干個目標文件靜態(tài)鏈接而生成的。
目標文件精续、主執(zhí)行體坝锰、庫執(zhí)行體,它們都要有一定的格式重付。不同的操作系統(tǒng)可以采用不同的格式顷级,Windows用的是PE格式,UNIX(包括Linux)用的是ELF格式确垫。ELF是一個總體格式弓颈,對上帽芽,目標文件、主執(zhí)行體翔冀、庫執(zhí)行體又有不同的子類型格式导街,對下,在不同的硬件平臺上它們又有具體的格式纤子。
大家經(jīng)常把靜態(tài)庫和動態(tài)庫放在一起來對比搬瑰,導致人們習慣性認為它倆是非常對稱的。實際上它倆只是稍微有一些對稱性控硼,它們的不對稱性還是很大的泽论。靜態(tài)庫和動態(tài)庫最大的區(qū)別就是,靜態(tài)庫會被復(fù)制進被鏈接的程序中象颖,而動態(tài)庫不會佩厚。也就是這一點區(qū)別導致了它們本質(zhì)性的區(qū)別,動態(tài)庫是執(zhí)行體说订,而靜態(tài)庫不是抄瓦,靜態(tài)庫是組成執(zhí)行體的原料。
頭文件的目的和意義:
當整個程序只有一個源文件的時候陶冷,是沒有頭文件的钙姊。當程序被放到多個源文件的時候,由于每個源文件都是單獨編譯的埂伦,編譯的時候并不知道其他源文件的信息煞额,所以需要頭文件來提供其他源文件的信息。頭文件里面主要放的是變量聲明沾谜、函數(shù)聲明膊毁、數(shù)據(jù)類型聲明。頭文件里面盡量只放聲明性的東西基跑,不要放定義性東西婚温。聲明和定義的區(qū)別是,聲明性的東西不分配內(nèi)存空間媳否,定義性的東西分配內(nèi)存空間栅螟。如果在頭文件里面放定義性的東西的話,由于頭文件會被包含到很多源文件中篱竭,所以會導致這個定義會有很多份力图,這在大多數(shù)情況下都不是想要的情況。有一種情況可以在頭文件中放定義掺逼,那就是有些比較短小的又想要內(nèi)聯(lián)的函數(shù)可以放在頭文件中吃媒。
4.1 靜態(tài)鏈接
我們經(jīng)常把鏈接到靜態(tài)庫叫靜態(tài)鏈接,其實目標文件的合并也是靜態(tài)鏈接。它倆形式上雖然不太相同晓折,但是本質(zhì)上是一樣的惑朦。靜態(tài)庫僅僅是對目標文件的打包而已,鏈接到靜態(tài)庫實際上還是目標文件的合并漓概。為了區(qū)分兩者漾月,我們把前者叫做顯式靜態(tài)鏈接,后者叫做隱式靜態(tài)鏈接胃珍。如果我們的程序有兩個源文件組成梁肿,那么最終生成程序的時候就是兩個目標文件的合并,也就是隱式靜態(tài)鏈接觅彰。而我們鏈接到靜態(tài)庫的顯示靜態(tài)鏈接吩蔑,和這個隱式靜態(tài)鏈接沒有區(qū)別。明白了這件事填抬,我們就容易想明白一個問題烛芬,那就是我們的程序如果依賴于一個靜態(tài)庫,那么這個靜態(tài)庫的依賴飒责,我們需要在本程序里要再聲明一次赘娄。因為靜態(tài)庫在這個意義上和你自己的某個C文件沒有區(qū)別,相當于是你自己對外部的依賴宏蛉。這點和動態(tài)庫就有很大的不同遣臼,你依賴于動態(tài)庫,并不需要知道動態(tài)庫依賴于誰拾并。
顯式靜態(tài)鏈接的編譯命令和動態(tài)鏈接的命令是一樣的揍堰,和隱式靜態(tài)鏈接不一樣,但是在處理邏輯上正好反了過來嗅义。
4.2 動態(tài)鏈接
動態(tài)鏈接也分為兩種屏歹,我們經(jīng)常說的動態(tài)鏈接叫半動態(tài)鏈接,通過dlopen進行的鏈接叫做完全動態(tài)鏈接之碗。因為dlopen確實是完全動態(tài)的蝙眶,只有dlopen代碼執(zhí)行到了才會進行鏈接,執(zhí)行不到根本就不會鏈接继控。而半動態(tài)鏈接械馆,你在編譯時就要在命令參數(shù)中指明要鏈接的動態(tài)庫胖眷,鏈接后動態(tài)庫的信息會被放到被鏈接的執(zhí)行體的動態(tài)段里面武通。程序啟動的時候加載器還會把一個程序所依賴的所有動態(tài)庫都加載到進程的地址空間,并做好重定位珊搀。你對動態(tài)庫中的函數(shù)的調(diào)用冶忱,直到你第一次調(diào)用時才會進行函數(shù)地址解析。完全動態(tài)鏈接境析,dlopen的時候加載動態(tài)庫囚枪,dlsym的時候進行函數(shù)地址解析派诬,dlclose還能把動態(tài)庫給移出進程的地址空間。
半動態(tài)鏈接和完全動態(tài)鏈接所使用的動態(tài)庫格式是一樣的链沼,沒有啥區(qū)別默赂,不同的是使用它們的方式。
4.3 實例解析
下面我們寫一個hello程序來演示一下隱式靜態(tài)鏈接括勺、顯示靜態(tài)鏈接缆八、半動態(tài)鏈接、完全動態(tài)鏈接疾捍,它們的區(qū)別和用法奈辰。所有的文件截圖如下:
hello.c
#include <stdio.h>
#include <dlfcn.h>
#include "hello_static_implicit.h"
#include "hello_static_explicit.h"
#include "hello_dynamic_partial.h"
void say_hello_direct()
{
printf("say hello direct\n");
}
int main(int argc, char const *argv[])
{
printf("----------------------------------\n");
say_hello_direct();
printf("----------------------------------\n");
say_hello_static_implicit();
printf("----------------------------------\n");
say_hello_static_explicit();
printf("----------------------------------\n");
say_hello_dynamic_partial();
printf("----------------------------------\n");
void *handle = dlopen("./libhello_dynamic_full.so", RTLD_LAZY);
void (*say_hello_dynamic_full)(void);
say_hello_dynamic_full = (void (*)(void))dlsym(handle, "say_hello_dynamic_full");
say_hello_dynamic_full();
dlclose(handle);
printf("----------------------------------\n");
return 0;
}
hello_static_implicit.c
#include <stdio.h>
#include "hello_static_explicit.h"
void say_hello_static_explicit()
{
printf("say hello static explicit\n");
}
其他幾個文件是類似的,就不再貼出代碼了乱豆。
Makefile
# This is hello link program
CFLAGS += -std=c99 -g -Wall
all : hello libhello_dynamic_full.so
hello : hello.o hello_static_implicit.o libhello_static_explicit.a libhello_dynamic_partial.so
gcc -o hello hello.o hello_static_implicit.o -Wl,-rpath=. -L. -lhello_static_explicit -lhello_dynamic_partial
hello.o : hello.c hello_static_implicit.h hello_static_explicit.h hello_dynamic_partial.h
gcc -o hello.o -c hello.c
hello_static_implicit.o : hello_static_implicit.c hello_static_implicit.h
gcc -o hello_static_implicit.o -c hello_static_implicit.c
hello_static_explicit.o : hello_static_explicit.c hello_static_explicit.h
gcc -o hello_static_explicit.o -c hello_static_explicit.c
libhello_static_explicit.a : hello_static_explicit.o
ar rv libhello_static_explicit.a hello_static_explicit.o
libhello_dynamic_partial.so : hello_dynamic_partial.c hello_dynamic_partial.h
gcc -o libhello_dynamic_partial.so -shared hello_dynamic_partial.c
libhello_dynamic_full.so : hello_dynamic_full.c
gcc -o libhello_dynamic_full.so -shared hello_dynamic_full.c
.PHONY :
clean:
rm -f hello *.o *.a *.so
從Makefile中可以看出他們的鏈接關(guān)系奖恰。
下面是程序運行的結(jié)果:
可以看出hello程序可以直接調(diào)用本文件內(nèi)的函數(shù),也可以調(diào)用隱式靜態(tài)鏈接中的函數(shù)宛裕,也可以調(diào)用顯示靜態(tài)鏈接也就是鏈接到靜態(tài)庫的函數(shù)瑟啃,也可以調(diào)用半動態(tài)鏈接中的so的函數(shù),也可以通過dlopen续滋、dlsym進行完全動態(tài)鏈接翰守。前三者在調(diào)用方式上沒有區(qū)別,都是先聲明后調(diào)用疲酌,最后一個不需要聲明函數(shù)蜡峰,但是需要手動解析函數(shù)的地址,并賦值給函數(shù)指針才能調(diào)用朗恳。
我把演示代碼放到GitHub上了湿颅,大家可以參考一下
https://github.com/orangeboyye/hello-link
五、總結(jié)回顧
本文中粥诫,我們先回顧了編譯系統(tǒng)的發(fā)展歷史油航,理解了編譯系統(tǒng)各個組成部分的原理和相互之間的關(guān)系,然后又介紹了各個模塊內(nèi)部的知識怀浆。都是一些簡單的概念性的介紹谊囚,想要深入學習的同學可以去研究參考文獻中的書籍。
參考文獻:
《Compilers Principles Techniques and Tools 2nd》
《Modern Compiler Implementation in C》
《Advanced Compiler Design and Implementation》
《Engineering a Compiler 2nd》
《Crafting a Compiler》
《Parsing Techniques 2nd》
《Linkers & Loaders》
《Learning Linux Binary Analysis》
《Linker and Libraries Guide》
《程序員的自我修養(yǎng)》
《C編譯器剖析》
《編譯系統(tǒng)透視》
《自己動手構(gòu)造編譯系統(tǒng)》