淺談匯編器、編譯器和解釋器
簡(jiǎn)單介紹一下編程方式的歷史演變。
在計(jì)算機(jī)誕生不久的早期年代,硬件非常昂貴,而程序員比較廉價(jià)。這些廉價(jià)程序員甚至都沒(méi)有“程序員”這個(gè)頭銜,并且常常是由數(shù)學(xué)家或者電氣工程師來(lái)充當(dāng)這個(gè)角色的。早期的計(jì)算機(jī)被用來(lái)快速解決復(fù)雜的數(shù)學(xué)問(wèn)題,所以數(shù)學(xué)家天然就適合“編程”工作。
什么是程序?
首先來(lái)看一點(diǎn)背景知識(shí)。計(jì)算機(jī)自己是做不了任何事情的,它們的任何行為都需要程序來(lái)引導(dǎo)。你可以把程序看成是非常精確的菜譜,這種菜譜讀取一個(gè)輸入,然后生成對(duì)應(yīng)的輸出。菜譜里的各個(gè)步驟由操作數(shù)據(jù)的指令構(gòu)成。聽(tīng)上去有點(diǎn)兒復(fù)雜,不過(guò)你或許知道下面這個(gè)語(yǔ)句是什么意思:
1 + 2 = 3
其中的加號(hào)是“指令”,而數(shù)字 1 和 2 是數(shù)據(jù)。數(shù)學(xué)上的等號(hào)意味著等式兩邊的部分是“等價(jià)”的,不過(guò)在大部分編程語(yǔ)言中對(duì)變量使用等號(hào)是“賦值”的意思。如果計(jì)算機(jī)執(zhí)行上面這個(gè)語(yǔ)句,它會(huì)把這個(gè)加法的結(jié)果(也就是“3”)儲(chǔ)存在內(nèi)存中的某個(gè)地方。
計(jì)算機(jī)知道如何使用數(shù)字進(jìn)行數(shù)學(xué)運(yùn)算,以及如何在內(nèi)存結(jié)構(gòu)中移動(dòng)數(shù)據(jù)。在這里就不對(duì)內(nèi)存進(jìn)行展開(kāi)了,你只需要知道內(nèi)存一般分為兩大類:“速度快/空間小”和“速度慢/空間大”。CPU 寄存器的讀寫(xiě)速度非??欤强臻g非常小,相當(dāng)于一個(gè)速記便簽。主存儲(chǔ)器通常有很大的空間,但是讀寫(xiě)速度就比寄存器差遠(yuǎn)了。在程序運(yùn)行的時(shí)候,CPU 不斷將它所需要用到的數(shù)據(jù)從主存儲(chǔ)器挪動(dòng)到寄存器,然后再把結(jié)果放回到主存儲(chǔ)器。
匯編器
當(dāng)時(shí)的計(jì)算機(jī)很貴,而人力比較便宜。程序員需要耗費(fèi)很多時(shí)間把手寫(xiě)的數(shù)學(xué)表達(dá)式翻譯成計(jì)算機(jī)可以執(zhí)行的指令。最初的計(jì)算機(jī)只有非常糟糕的用戶界面,有些甚至只有前面板上的撥動(dòng)開(kāi)關(guān)。這些開(kāi)關(guān)就代表一個(gè)內(nèi)存“單元”里的一個(gè)個(gè) “0” 和 “1”。程序員需要配置一個(gè)內(nèi)存單元,選擇好儲(chǔ)存位置,然后把這個(gè)單元提交到內(nèi)存里。這是一個(gè)既耗時(shí)又容易出錯(cuò)的過(guò)程。
程序員Betty Jean Jennings (左) 和 Fran Bilas (右) 在操作 ENIAC 的主控制面板
后來(lái)有一名 電氣工程師 認(rèn)為自己的時(shí)間很寶貴,就寫(xiě)了一個(gè)程序,能夠把人們可以讀懂的“菜譜”一樣的輸入轉(zhuǎn)換成計(jì)算機(jī)可以讀懂的版本。這就是最初的“匯編器”,在當(dāng)時(shí)引起了不小的爭(zhēng)議。這些昂貴機(jī)器的主人不希望把計(jì)算資源浪費(fèi)在人們已經(jīng)能做的任務(wù)上(雖然又慢又容易出錯(cuò))。不過(guò)隨著時(shí)間的推移,人們逐漸發(fā)現(xiàn)使用匯編器在速度和準(zhǔn)確性上都勝于人工編寫(xiě)機(jī)器語(yǔ)言,并且計(jì)算機(jī)完成的“實(shí)際工作量”增加了。
盡管匯編器相比在機(jī)器面板上切換比特的狀態(tài)已經(jīng)是很大的進(jìn)步了,這種編程方式仍然非常專業(yè)。上面加法的例子在匯編語(yǔ)言中看起來(lái)差不多是這樣的:
01 MOV R0, 1
02 MOV R1, 2
03 ADD R0, R1, R2
04 MOV 64, R0
05 STO R2, R0
每一行都是一個(gè)計(jì)算機(jī)指令,前面是一個(gè)指令的簡(jiǎn)寫(xiě),后面是指令所操作的數(shù)據(jù)。這個(gè)小小的程序首先會(huì)將數(shù)值 1 “移動(dòng)”到寄存器 R0,然后把 2 移動(dòng)到寄存器 R1。03 行把 R0 和 R1 兩個(gè)寄存器里的數(shù)值相加,然后將結(jié)果儲(chǔ)存在 R2 寄存器里。***,04 行和 05 行決定結(jié)果應(yīng)該被放在主存儲(chǔ)器里的什么位置(在這里是地址 64)。管理內(nèi)存中存儲(chǔ)數(shù)據(jù)的位置是編程過(guò)程中最耗時(shí)也最容易出錯(cuò)的部分之一。
編譯器
匯編器已經(jīng)比手寫(xiě)計(jì)算機(jī)指令要好太多了,不過(guò)早期的程序員還是渴望能夠按照他們所習(xí)慣的方式,像書(shū)寫(xiě)數(shù)學(xué)公式一樣地去寫(xiě)程序。這種需求推動(dòng)了高級(jí)編譯語(yǔ)言的發(fā)展,其中有一些已經(jīng)成為歷史,另一些如今還在使用。比如 ALGO 就已經(jīng)成為歷史了,但是像 Fortran 和 C 這樣的語(yǔ)言仍然在不斷解決實(shí)際問(wèn)題。
ALGO 和 Fortran 編程語(yǔ)言的譜系樹(shù)
這些“高級(jí)”語(yǔ)言使得程序員可以用更簡(jiǎn)單的方式編寫(xiě)程序。在 C 語(yǔ)言中,我們的加法程序就變成了這樣:
int x;
x = 1 + 2;
***個(gè)語(yǔ)句描述了該程序?qū)⒁褂玫囊粔K內(nèi)存。在這個(gè)例子中,這塊內(nèi)存應(yīng)該占一個(gè)整數(shù)的大小,名字是 x
。第二個(gè)語(yǔ)句是加法,雖然是倒著寫(xiě)的。一個(gè) C 語(yǔ)言的程序員會(huì)說(shuō)這是 “X 被賦值為 1 加 2 的結(jié)果”。需要注意的是,程序員并不需要決定在內(nèi)存的什么位置儲(chǔ)存 x
,這個(gè)任務(wù)交給編譯器了。
這種被稱為“編譯器”的新程序可以把用高級(jí)語(yǔ)言寫(xiě)的程序轉(zhuǎn)換成匯編語(yǔ)言,再使用匯編器把匯編語(yǔ)言轉(zhuǎn)換成機(jī)器可讀的程序。這種程序組合常常被稱為“工具鏈”,因?yàn)橐粋€(gè)程序的輸出就直接成為另一個(gè)程序的輸入。
編譯語(yǔ)言相比匯編語(yǔ)言的優(yōu)勢(shì)體現(xiàn)在從一臺(tái)計(jì)算機(jī)遷移到不同型號(hào)或者品牌的另一臺(tái)計(jì)算機(jī)上的時(shí)候。在計(jì)算機(jī)的早期歲月里,包括 IBM、DEC、德州儀器、UNIVAC 以及惠普在內(nèi)的很多公司都在制造除了大量不同類型的計(jì)算機(jī)硬件。這些計(jì)算機(jī)除了都需要連接電源之外就沒(méi)有太多共同點(diǎn)了。它們?cè)趦?nèi)存和 CPU 架構(gòu)上的差異相當(dāng)大,當(dāng)時(shí)經(jīng)常需要人們花費(fèi)數(shù)年來(lái)將一臺(tái)計(jì)算機(jī)的程序翻譯成另一臺(tái)計(jì)算機(jī)的程序。
有了高級(jí)語(yǔ)言,我們只需要把編譯器工具鏈遷移到新的平臺(tái)就行了。只要有可用的編譯器,高級(jí)語(yǔ)言寫(xiě)的程序最多只需要經(jīng)過(guò)小幅修改就可以在新的計(jì)算機(jī)上被重新編譯。高級(jí)語(yǔ)言的編譯是一個(gè)真正的革命性成果。
1983 發(fā)布的 IBM PC XT 是硬件價(jià)格下降的早期例子。
程序員們的生活得到了很好的改善。相比之下,通過(guò)高級(jí)語(yǔ)言表達(dá)他們想要解決的問(wèn)題讓事情變得輕松很多。由于半導(dǎo)體技術(shù)的進(jìn)步以及集成芯片的發(fā)明,計(jì)算機(jī)硬件的價(jià)格急劇下降。計(jì)算機(jī)的速度越來(lái)越快,能力也越來(lái)越強(qiáng),并且還便宜了很多。從某個(gè)時(shí)間點(diǎn)往后(也許是 80 年代末期吧),事情發(fā)生了反轉(zhuǎn),程序員變得比他們所使用的硬件更值錢了。
解釋器
隨著時(shí)間的推移,一種新的編程方式興起了。一種被稱為“解釋器”的特殊程序可以直接讀取一個(gè)程序?qū)⑵滢D(zhuǎn)換成計(jì)算機(jī)指令以立即執(zhí)行。和編譯器差不多,解釋器讀取程序并將它轉(zhuǎn)換成一個(gè)中間形態(tài)。但和編譯器不同的是,解釋器直接執(zhí)行程序的這個(gè)中間形態(tài)。解釋型語(yǔ)言在每一次執(zhí)行的時(shí)候都要經(jīng)歷這個(gè)過(guò)程;而編譯程序只需要編譯一次,之后計(jì)算機(jī)每次只需要執(zhí)行編譯好的機(jī)器指令就可以了。
順便說(shuō)一句,這個(gè)特性就是導(dǎo)致人們感覺(jué)解釋型程序運(yùn)行得比較慢的原因。不過(guò)現(xiàn)代計(jì)算機(jī)的性能出奇地強(qiáng)大,以至于大多數(shù)人無(wú)法區(qū)分編譯型程序和解釋型程序。
解釋型程序(有時(shí)也被成為“腳本”)甚至更容易被移植到不同的硬件平臺(tái)上。因?yàn)槟_本并不包含任何機(jī)器特有的指令,同一個(gè)版本的程序可以不經(jīng)過(guò)任何修改就直接在很多不同的計(jì)算機(jī)上運(yùn)行。不過(guò)當(dāng)然了,解釋器必須得先移植到新的機(jī)器上才行。
一個(gè)很流行的解釋型語(yǔ)言是 perl。用 perl 完整地表達(dá)我們的加法問(wèn)題會(huì)是這樣的:
$x = 1 + 2
雖然這個(gè)程序看起來(lái)和 C 語(yǔ)言的版本差不多,運(yùn)行上也沒(méi)有太大區(qū)別,但卻缺少了初始化變量的語(yǔ)句。其實(shí)還有一些其它的區(qū)別(超出這篇文章的范圍了),但你應(yīng)該已經(jīng)注意到,我們寫(xiě)計(jì)算機(jī)程序的方式已經(jīng)和數(shù)學(xué)家用紙筆手寫(xiě)數(shù)學(xué)表達(dá)式非常接近了。
虛擬機(jī)
***潮的編程方式要數(shù)虛擬機(jī)(經(jīng)常簡(jiǎn)稱 VM)了。虛擬機(jī)分為兩大類:系統(tǒng)虛擬機(jī)和進(jìn)程虛擬機(jī)。這兩種虛擬機(jī)都提供一種對(duì)“真實(shí)的”計(jì)算硬件的不同級(jí)別的抽象,不過(guò)它們的作用域不同。系統(tǒng)虛擬機(jī)是一個(gè)提供物理硬件的替代品的軟件,而進(jìn)程虛擬機(jī)則被設(shè)計(jì)用來(lái)以一種“系統(tǒng)獨(dú)立”的方式執(zhí)行程序。所以在這個(gè)例子里,進(jìn)程虛擬機(jī)(往后我所說(shuō)的虛擬機(jī)都是指這個(gè)類型)的作用域和解釋器的比較類似,因?yàn)橐彩窍葘⒊绦蚓幾g成一個(gè)中間形態(tài),然后虛擬機(jī)再執(zhí)行這個(gè)中間形態(tài)。
虛擬機(jī)和解釋器的主要區(qū)別在于,虛擬機(jī)創(chuàng)造了一個(gè)虛擬的 CPU,以及一套虛擬的指令集。有了這層抽象,我們就可以編寫(xiě)前端工具來(lái)把不同語(yǔ)言的程序編譯成虛擬機(jī)可以接受的程序了。也許***也最知名的虛擬機(jī)就是 Java 虛擬機(jī)(JVM)了。JVM 最初在 1990 年代只支持 Java 語(yǔ)言,但是如今卻可以運(yùn)行 許多 流行的編程語(yǔ)言,包括 Scala、Jython、JRuby、Clojure,以及 Kotlin 等等。還有其它一些不太常見(jiàn)的例子,在這里就不說(shuō)了。我也是最近才知道,我最喜歡的語(yǔ)言 Python 并不是一個(gè)解釋型語(yǔ)言,而是一個(gè) 運(yùn)行在虛擬機(jī)上的語(yǔ)言!
虛擬機(jī)仍然在延續(xù)這樣一個(gè)歷史趨勢(shì):讓程序員在使用特定領(lǐng)域的編程語(yǔ)言解決問(wèn)題的時(shí)候,所需要的對(duì)特定計(jì)算平臺(tái)的了解變得越來(lái)越少了。
就是這樣了
希望你喜歡這篇簡(jiǎn)單介紹軟件背后運(yùn)行原理的短文。有什么其它話題是你想讓我接下來(lái)討論的嗎?在評(píng)論里告訴我吧。