編譯器的自舉原理是什麼?

早期的Pascal編譯器都是圍繞著由Niklaus Wirth提供的一組工具構造出來的,這些工具包括:

1、一個用Pascal語言編寫的編譯器,它生成P代碼形式的輸出(P代碼是一種基於棧的語言,與現代Java編譯器的位元組碼類似);

2、同一個編譯器,但已經被翻譯成了P代碼;

3、一個P代碼解釋器,也是用Pascal編寫的。

——出自《程序設計語言-實踐之路》

問題有三:

這三個工具間是如何協作將Pascal編譯成彙編或者機器碼的?

既然需要依賴其他語言寫的編譯器,不能完全叫自舉吧?

既然是早期的編譯器,那現代編譯器自舉又是如何工作的?


你想創造一門V語言而且用V語言來寫V編譯器的話,你得按照下面的方法做:

1、用C++把那個編譯器(A)寫出來,順便留下很多測試用例。

2、用V語言把那個編譯器寫(B)出來,用A.exe來編譯B,修改直到所有測試用例都通過為止。

3、B.exe來編譯B自己得到B2.exe,修改直到B2.exe所有測試用例都通過為止。這是為了保證,就算B本身有很多bug,至少編譯自己是沒有bug的,從而你就可以走到第四步。

4、當你覺得有信心了,用A.exe把B編譯一遍,就得到了B.exe。然後A的代碼和A.exe都在也不需要存在了,刪掉他們。以後你就不斷的用B.exe來編譯下一個版本的B就好了。就自舉了。


編譯器自舉一般都是編譯器開發的一個里程碑事件,你所舉的例子Pascal編譯器,其第一版編譯器是用Fortran寫的,而這也是常見的編譯器自舉過程的幾乎必走的一條路,即最開始使用X語言(如Fortran)實現Y語言(如Pascal)的編譯器,即解決雞與蛋的問題,所以你的問題二是不正確的,因為我們先要使用其它語言構建出我們的第一版編譯器,之後成熟以後,就可以完全使用已經生成好的編譯器來編譯出我們的新編譯器。

而其實疑惑自舉大部分就在雞與蛋問題上,為什麼可以自己可以編譯自己,上面 @vczh基本上概括了語言的雞與蛋問題。而雞與蛋問題還包括了跨體系結構,即在X結構上有一個C編譯器,而Y結構沒有這樣的C編譯器,那麼這時候要使用的話,使用的就是交叉編譯,而C與Free Pascal都是這樣的。而支持新的語言特性其實也算,即使用C++98寫支持C++11的C++編譯器。而等待時機成熟後,就又可以使用C++11來寫支持C++14的C++編譯器,隨後可以使用C++14來寫支持C++1z的C++編譯器......這就是現代編譯器支持新的語言特性的自舉方式了,所以你看Clang就開始使用C++11的語法來開發新版本的C++編譯器了。

最後再來說你的第一個問題,你說的P代碼其實就是中間代碼輸出形式,類似LLVM IR,而這個是編譯器後端可以識別的形式,而編譯器後端吃進去這樣的中間代碼,又會開始解析,然後根據體系結構的不同開始輸出不同體系結構的彙編指令了,如X86,PPC等,輸出這樣的彙編指令後,就可以交給彙編器等更低級的工具去進行下一步驟了。


說個不尋常的:Knuth 寫 WEB 的第一版源碼就是用 WEB 寫的,然後他人肉(無誤)把 WEB 源碼翻譯成了機器碼。


問題1:

用3(P代碼解釋器)解釋運行2(P代碼的編譯器),然後用這個2來編譯1(生成P代碼的編譯器)。總之就可以得到一個1的P版本,然後自己編譯自己。不過整個的自舉都是建立在P的解釋器上的。一般來說是能夠直接運行在機器上的,才叫純正的編譯器。

問題2:

前面的人都說了,總歸高級語言都是需要翻譯成機器語言的,所以開始的過程要麼手焊機器碼,要不然就藉助其他語言工具。自舉的意思是說整個編譯器(有時候包含庫)的代碼都是自己編譯自己,換句話說,編譯器自己的機器碼和初始構建工具徹底無關。

問題3:

現代的編譯器自舉,基本上就是用一個已經存在的編譯器(包含其他的工具鏈)編譯目標編譯器,也許包括一些庫,然後用得到的編譯器和工具鏈的其他部分組合再次構建。默認GCC就是這麼構建的,它還會再來一次,確認和上一次得到的結果是一樣的(也就是該結果GCC和初始工具的特性無關)

最後,最初用來啟動的編譯器及工具鏈是不要實現任何優化的,並且需要的特性可能比要自舉的編譯器低(例如自舉C++14的編譯器大概不需要已經支持C++14的編譯器來啟動,或者不需要該特性的運行庫)。

總歸編譯器裡面的大部分代碼都是和優化有關的,所以初始編譯器被用到的部分其實不多。最後編譯器本身也算是一個測試吧,如果他是NATIVE的話。

相對於NATIVE,交叉編譯器產生的目標代碼和自己的運行平台是不一致的,也就沒辦法自舉。

不過有例外,像LLVM本身是包含多平台的,所以不但可以自舉,而且還可能用一種更複雜的方式啟動,例如在X86平台用GCC來編譯LLVM,用得到的LLVM編譯ARM上的LLVM,然後用最後的ARM上的LLVM自舉(或者蛋疼的,可以回來構造一個X86的GCC)(另外,這裡面的平台有時候單指指令集,有時候也包含操作系統的區別)。

補充,很多語言其實不一定要自舉,運行庫也可能是其他語言例如C++寫的,但是有可能編譯器和工具鏈的一部分還是需要自己來編譯(這樣有可能比較方便)。


有一件事情是肯定的,那就是自舉的第一步。任何一種語言(Machine code除外)的首個compiler肯定是用其他語言寫出來的,然後在其基礎上再演進才能開始自舉。如果要把所有的所有計算機語言的自舉都回溯到歷史最初狀態,那必然是會回推到Machine code。而Machine code本身是和計算機硬體相關的,再往前推,那就是物理和電氣工程的範疇了


補充一點,自舉還是為了證明,這個編譯器已經成熟到,可以編譯一個較複雜的工程的地步了。


輪子哥講的挺好。

第一個問題無非是 源程序-&>編譯器-&>中間程序-&>解釋器-&>目標程序

第二個問題通過輪子哥的這種方法,其實已經擺脫了原有的語言,如果你是通過C寫的,雖然你寫的編譯器A是C編譯過來的。但是你用的不是A,你用的是B編譯過來的編譯器。B編譯器是用你自己語言寫的。


「世上萬事萬物都要有另一個事物作為它的原因。那麼必然存在一個最初的原因,這個原因就是上帝。」


推薦閱讀:

從哪裡可以得到包含了Python3所支持的所有語法的測試用例?
關於局部數組和全局數組的運行效率?
PLT界在近幾年中對哪些問題的研究有重大進展?
HotSpot JVM參數 -XX:+DoEscapeAnalysis 有什麼影響?

TAG:編譯原理 | 編譯器 |