這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文在此。
————翻譯分隔線————
編譯器(2)-編譯、轉譯和解釋
第一部分作為這一系列文章的介紹。
在第二部分,在真正深入到編譯實際的步驟之前,我會對一些定義作一些概述。
編譯
編譯是直將編寫的代碼從一個語言翻譯為另一個更低層級語言的過程。一個 C 編譯器其實並不會直接輸出機器碼。而是將 C 代碼翻譯為組合語言。彙編編譯器擷取這些內容編譯為機器碼。C# 和 Java 會翻譯為位元組碼。位元組碼在虛擬機器啟動並執行時候才會被轉換為機器碼。
理解這其中的差異非常重要。
編譯經常會伴隨中間代碼(IR)或中繼語言的使用。彙編是一個很常見的中繼語言。LLVM 的 IR 通常叫做 LLVM IR。C 也會作為中繼語言出現。
轉譯
對照來說,轉譯是將代碼從一個語言翻譯到另一個同樣層級的語言。例如將 Go 翻譯為 Javascript。
不要它與類似 Scala 或 Clojure 這樣的語言的行為混淆了。這些語言都是直接編譯為 Java 位元組碼,然後使用 JVM 來執行它們。由於 Java 位元組碼是一個更低層次的抽象,所以它們不被認為是轉譯。在編譯到位元組碼之前首先翻譯為 Java,這種可以認為是使用轉譯的語言。
將 Go 或 Lisp 翻譯到 C 也不是真正意義上的轉譯,儘管界限不是很清晰。C 雖然相對於彙編來說是層級要高,但是它仍然是一個低層級語言。
解釋
解譯器通常與指令碼語言相關。通常直接進行解釋,而不是將一個語言翻譯為另一種語言或 IR。
在某些情況下,這意味著字面直譯,掃描碼然後解釋執行,直到出錯為止。另一些情況下,意味著掃描整個代碼,校正並將全部資訊儲存到一個樹狀資料結構,以便執行。
某種意義上來說,這種類型的解譯器在運行時,每次都必須“編譯”指令碼或原始碼,這使得它變慢。這是因為每次執行,它都需要完成編譯過程的全部步驟,而不是只運行一次這個過程。
然而現代解譯器通常不這麼做。如果你對細節感興趣,你可能需要研究以下 JIT(Just In Time) 編譯。簡單來說,指令碼僅會被解釋一次,就像編譯器那樣,然後儲存在某些中間表中,這樣可以更快的進行載入。這一過程同樣允許設計者進行最佳化來給代碼進一步提速。
二進位編譯
編譯器的目標通常就是建立一個可執行檔二進位檔案,或者某些及其可以讀取和執行的對象。
但這不僅僅是建立機器碼這麼簡單。編譯實際上只是第一步。
這裡有一個 GCC 和 Go 編譯其代碼的基本步驟:
使用 GCC 的 C:
- 通過 GNU C 編譯器(gcc)將 C 翻譯為彙編;
- 通過 GNU 彙編器(gas)將彙編翻譯為機器碼;
- 通過 GNU 連結器(ld)將機器碼和標準庫進行連結,產生特定架構下的二進位檔案。
在 x86-32 上 Go 使用 Go 編譯器:
- 通過 Go 編譯器(8g)將 Go 翻譯為彙編;
- 通過 Plan 9 彙編器(8a)將彙編翻譯為機器碼;
- 通過 Plan 9 連結器(8l)將機器碼和標準庫進行連結,產生特定架構下的二進位檔案。
如你所見,編譯器只是整個過程的第一步。在這一系列文章中,我們只會處理第一步。彙編編譯和連結已經超出了這些文章的範疇。
別著急!我們的編譯器會產生可執行檔二進位檔案!
接下來
現在我們已經瞭解了一些定義,並且瞭解了讓代碼執行的過程,可以在第三部分來看看編譯器更多的一些細節了。