淺談彙編器、編譯器和直譯器
簡單介紹一下程式設計方式的歷史演變。
在計算機誕生不久的早期年代,硬體非常昂貴,而程式設計師比較廉價。這些廉價程式設計師甚至都沒有“程式設計師”這個頭銜,並且常常是由數學家或者電氣工程師來充當這個角色的。早期的計算機被用來快速解決複雜的數學問題,所以數學家天然就適合“程式設計”工作。
什麼是程式?
首先來看一點背景知識。計算機自己是做不了任何事情的,它們的任何行為都需要程式來引導。你可以把程式看成是非常精確的菜譜,這種菜譜讀取一個輸入,然後生成對應的輸出。菜譜裡的各個步驟由運算元據的指令構成。聽上去有點兒複雜,不過你或許知道下面這個語句是什麼意思:
1 + 2 = 3
其中的加號是“指令”,而數字 1 和 2 是資料。數學上的等號意味著等式兩邊的部分是“等價”的,不過在大部分程式語言中對變數使用等號是“賦值”的意思。如果計算機執行上面這個語句,它會把這個加法的結果(也就是“3”)儲存在記憶體中的某個地方。
計算機知道如何使用數字進行數學運算,以及如何在記憶體結構中移動資料。在這裡就不對記憶體進行展開了,你只需要知道記憶體一般分為兩大類:“速度快/空間小”和“速度慢/空間大”。CPU 暫存器的讀寫速度非常快,但是空間非常小,相當於一個速記便籤。主儲存器通常有很大的空間,但是讀寫速度就比暫存器差遠了。在程式執行的時候,CPU 不斷將它所需要用到的資料從主儲存器挪動到暫存器,然後再把結果放回到主儲存器。
彙編器
當時的計算機很貴,而人力比較便宜。程式設計師需要耗費很多時間把手寫的數學表示式翻譯成計算機可以執行的指令。最初的計算機只有非常糟糕的使用者介面,有些甚至只有前皮膚上的撥動開關。這些開關就代表一個記憶體“單元”裡的一個個 “0” 和 “1”。程式設計師需要配置一個記憶體單元,選擇好儲存位置,然後把這個單元提交到記憶體裡。這是一個既耗時又容易出錯的過程。
程式設計師Betty Jean Jennings (左) 和 Fran Bilas (右) 在操作 ENIAC 的主控制皮膚
後來有一名 電氣工程師 認為自己的時間很寶貴,就寫了一個程式,能夠把人們可以讀懂的“菜譜”一樣的輸入轉換成計算機可以讀懂的版本。這就是最初的“彙編器”,在當時引起了不小的爭議。這些昂貴機器的主人不希望把計算資源浪費在人們已經能做的任務上(雖然又慢又容易出錯)。不過隨著時間的推移,人們逐漸發現使用匯編器在速度和準確性上都勝於人工編寫機器語言,並且計算機完成的“實際工作量”增加了。
儘管彙編器相比在機器皮膚上切換位元的狀態已經是很大的進步了,這種程式設計方式仍然非常專業。上面加法的例子在組合語言中看起來差不多是這樣的:
01 MOV R0, 1
02 MOV R1, 2
03 ADD R0, R1, R2
04 MOV 64, R0
05 STO R2, R0
每一行都是一個計算機指令,前面是一個指令的簡寫,後面是指令所操作的資料。這個小小的程式首先會將數值 1 “移動”到暫存器 R0,然後把 2 移動到暫存器 R1。03 行把 R0 和 R1 兩個暫存器裡的數值相加,然後將結果儲存在 R2 暫存器裡。最後,04 行和 05 行決定結果應該被放在主儲存器裡的什麼位置(在這裡是地址 64)。管理記憶體中儲存資料的位置是程式設計過程中最耗時也最容易出錯的部分之一。
編譯器
彙編器已經比手寫計算機指令要好太多了,不過早期的程式設計師還是渴望能夠按照他們所習慣的方式,像書寫數學公式一樣地去寫程式。這種需求推動了高階編譯語言的發展,其中有一些已經成為歷史,另一些如今還在使用。比如 ALGO 就已經成為歷史了,但是像 Fortran 和 C 這樣的語言仍然在不斷解決實際問題。
ALGO 和 Fortran 程式語言的譜系樹
這些“高階”語言使得程式設計師可以用更簡單的方式編寫程式。在 C 語言中,我們的加法程式就變成了這樣:
int x;
x = 1 + 2;
第一個語句描述了該程式將要使用的一塊記憶體。在這個例子中,這塊記憶體應該佔一個整數的大小,名字是 x
。第二個語句是加法,雖然是倒著寫的。一個 C 語言的程式設計師會說這是 “X 被賦值為 1 加 2 的結果”。需要注意的是,程式設計師並不需要決定在記憶體的什麼位置儲存 x
,這個任務交給編譯器了。
這種被稱為“編譯器”的新程式可以把用高階語言寫的程式轉換成組合語言,再使用匯編器把組合語言轉換成機器可讀的程式。這種程式組合常常被稱為“工具鏈”,因為一個程式的輸出就直接成為另一個程式的輸入。
編譯語言相比組合語言的優勢體現在從一臺計算機遷移到不同型號或者品牌的另一臺計算機上的時候。在計算機的早期歲月裡,包括 IBM、DEC、德州儀器、UNIVAC 以及惠普在內的很多公司都在製造除了大量不同型別的計算機硬體。這些計算機除了都需要連線電源之外就沒有太多共同點了。它們在記憶體和 CPU 架構上的差異相當大,當時經常需要人們花費數年來將一臺計算機的程式翻譯成另一臺計算機的程式。
有了高階語言,我們只需要把編譯器工具鏈遷移到新的平臺就行了。只要有可用的編譯器,高階語言寫的程式最多隻需要經過小幅修改就可以在新的計算機上被重新編譯。高階語言的編譯是一個真正的革命性成果。
1983 釋出的 IBM PC XT 是硬體價格下降的早期例子。
程式設計師們的生活得到了很好的改善。相比之下,透過高階語言表達他們想要解決的問題讓事情變得輕鬆很多。由於半導體技術的進步以及整合晶片的發明,計算機硬體的價格急劇下降。計算機的速度越來越快,能力也越來越強,並且還便宜了很多。從某個時間點往後(也許是 80 年代末期吧),事情發生了反轉,程式設計師變得比他們所使用的硬體更值錢了。
直譯器
隨著時間的推移,一種新的程式設計方式興起了。一種被稱為“直譯器”的特殊程式可以直接讀取一個程式將其轉換成計算機指令以立即執行。和編譯器差不多,直譯器讀取程式並將它轉換成一箇中間形態。但和編譯器不同的是,直譯器直接執行程式的這個中間形態。解釋型語言在每一次執行的時候都要經歷這個過程;而編譯程式只需要編譯一次,之後計算機每次只需要執行編譯好的機器指令就可以了。
順便說一句,這個特性就是導致人們感覺解釋型程式執行得比較慢的原因。不過現代計算機的效能出奇地強大,以至於大多數人無法區分編譯型程式和解釋型程式。
解釋型程式(有時也被成為“指令碼”)甚至更容易被移植到不同的硬體平臺上。因為指令碼並不包含任何機器特有的指令,同一個版本的程式可以不經過任何修改就直接在很多不同的計算機上執行。不過當然了,直譯器必須得先移植到新的機器上才行。
一個很流行的解釋型語言是 perl。用 perl 完整地表達我們的加法問題會是這樣的:
$x = 1 + 2
雖然這個程式看起來和 C 語言的版本差不多,執行上也沒有太大區別,但卻缺少了初始化變數的語句。其實還有一些其它的區別(超出這篇文章的範圍了),但你應該已經注意到,我們寫計算機程式的方式已經和數學家用紙筆手寫數學表示式非常接近了。
虛擬機器
最新潮的程式設計方式要數虛擬機器(經常簡稱 VM)了。虛擬機器分為兩大類:系統虛擬機器和程式虛擬機器。這兩種虛擬機器都提供一種對“真實的”計算硬體的不同級別的抽象,不過它們的作用域不同。系統虛擬機器是一個提供物理硬體的替代品的軟體,而程式虛擬機器則被設計用來以一種“系統獨立”的方式執行程式。所以在這個例子裡,程式虛擬機器(往後我所說的虛擬機器都是指這個型別)的作用域和直譯器的比較類似,因為也是先將程式編譯成一箇中間形態,然後虛擬機器再執行這個中間形態。
虛擬機器和直譯器的主要區別在於,虛擬機器創造了一個虛擬的 CPU,以及一套虛擬的指令集。有了這層抽象,我們就可以編寫前端工具來把不同語言的程式編譯成虛擬機器可以接受的程式了。也許最流行也最知名的虛擬機器就是 Java 虛擬機器(JVM)了。JVM 最初在 1990 年代只支援 Java 語言,但是如今卻可以執行 許多 流行的程式語言,包括 Scala、Jython、JRuby、Clojure,以及 Kotlin 等等。還有其它一些不太常見的例子,在這裡就不說了。我也是最近才知道,我最喜歡的語言 Python 並不是一個解釋型語言,而是一個 執行在虛擬機器上的語言!
虛擬機器仍然在延續這樣一個歷史趨勢:讓程式設計師在使用特定領域的程式語言解決問題的時候,所需要的對特定計算平臺的瞭解變得越來越少了。
就是這樣了
希望你喜歡這篇簡單介紹軟體背後執行原理的短文。有什麼其它話題是你想讓我接下來討論的嗎?在評論裡告訴我吧。
via: https://opensource.com/article/19/5/primer-assemblers-compilers-interpreters
作者:Erik O'Shaughnessy 選題:lujun9972 譯者:chen-ni 校對:wxy
相關文章
- 淺談編譯器優化編譯優化
- 編譯器如何生成彙編編譯
- 源語言、目標語言、翻譯器、編譯器、直譯器編譯
- gcc 編譯器與 clang 編譯器GC編譯
- 【譯】使用 Python 編寫虛擬機器直譯器Python虛擬機
- WebAssembly 系列(三)編譯器如何生成彙編Web編譯
- [譯]iOS編譯器iOS編譯
- 編譯器的自展和自舉、交叉編譯編譯
- JavaScript 編寫的迷你 Lisp 直譯器JavaScriptLisp
- CUDAFORTRAN編譯器編譯
- vue編譯器Vue編譯
- 編譯器的編譯基本過程編譯
- 一張圖解析 編譯器編譯流程圖解編譯
- 在C,C++,java和python執行時直譯器和編譯器的區別C++JavaPython編譯
- CMM編譯器和C編譯器過程呼叫實現的比較編譯
- 從編譯原理看一個直譯器的實現編譯原理
- FreeBSD中的GNU C編譯器--編譯器GCC(轉)編譯GC
- Vue3原始碼分析——編譯模組和編譯器Vue原始碼編譯
- Go編譯器簡介【譯】Go編譯
- 淺談交叉編譯編譯
- CSS預編譯器CSS編譯
- 富文字編譯器編譯
- JavaScript編譯器BabelJavaScript編譯Babel
- 編譯器相關編譯
- Oracle編譯器警告Oracle編譯
- 程式碼線上編譯器(上)- 編輯及編譯編譯
- JVM 模板直譯器之如何根據位元組碼生成彙編碼?JVM
- 前端與編譯原理——用 JS 寫一個 JS 直譯器前端編譯原理JS
- 前端與編譯原理——用JS寫一個JS直譯器前端編譯原理JS
- 用 Haskell 編寫 CEK 風格的直譯器Haskell
- objc系列譯文(6.2):編譯器OBJ編譯
- 翻譯的未來:翻譯機器和譯後編譯編譯
- 說說 方舟編譯器編譯
- ARM 之主流編譯器編譯
- stylus(css預編譯器)CSS編譯
- Java反編譯器剖析Java編譯
- GCC編譯器的使用GC編譯
- Rust 編譯器入門Rust編譯