官术网_书友最值得收藏!

在構(gòu)建虛擬機(jī)和一個(gè)與之匹配的編譯器之前,我們需要先解決“雞生蛋和蛋生雞”的哲學(xué)問(wèn)題:到底應(yīng)該先構(gòu)建哪一個(gè)?是編譯器,為一個(gè)不存在的虛擬機(jī)輸出字節(jié)碼,還是虛擬機(jī),但是此時(shí)又沒(méi)有任何代碼可以執(zhí)行?

我在本書中給的答案是:虛擬機(jī)與編譯器同時(shí)構(gòu)建!

在一個(gè)組件之前構(gòu)建另一個(gè)組件(不管這個(gè)組件是什么)是一件令人沮喪的事,因?yàn)楹茈y預(yù)判后續(xù)事情的進(jìn)展,也無(wú)法了解當(dāng)下所做事情的真正目的。如果優(yōu)先構(gòu)建編譯器并定義字節(jié)碼,那么在不知道后續(xù)虛擬機(jī)如何執(zhí)行它時(shí),你很難理解當(dāng)下的事情為什么如此。在編譯器之前構(gòu)建虛擬機(jī)也存在著天然的缺陷,因?yàn)樽止?jié)碼需要事先定義,虛擬機(jī)才能執(zhí)行。如果不仔細(xì)查看字節(jié)碼旨在表示的源語(yǔ)言結(jié)構(gòu),很難提前定義字節(jié)碼,這意味著無(wú)論如何都要優(yōu)先構(gòu)建編譯器。

當(dāng)然,如果已經(jīng)擁有構(gòu)建其中一個(gè)組件的經(jīng)驗(yàn),并且很清楚地知道你所要的最終效果,那你可以選擇任意一個(gè)組件來(lái)構(gòu)建。但是,對(duì)本書而言,我們的目標(biāo)是學(xué)習(xí)如何同時(shí)從零開(kāi)始構(gòu)建兩個(gè)組件。

這就是我們從小處著手的原因。我們將構(gòu)建一個(gè)只支持少量指令的微型虛擬機(jī),以及一個(gè)與之匹配的微型編譯器,僅用于輸出這些指令。如此一來(lái),我們會(huì)對(duì)自己所構(gòu)建的內(nèi)容及其相互之間如何適配有很深的理解。我們將擁有一個(gè)從零開(kāi)始構(gòu)建而成的可運(yùn)行系統(tǒng)。該系統(tǒng)能提供快速的反饋周期,使我們能調(diào)整、試驗(yàn)并逐步完善虛擬機(jī)和編譯器。整個(gè)旅程因此變得充滿樂(lè)趣!

現(xiàn)在知道了全部的計(jì)劃,而且足夠了解編譯器和虛擬機(jī)的基本原理,因此我們不會(huì)一直迷茫無(wú)助。讓我們開(kāi)始吧!

提及虛擬機(jī),你可能會(huì)聯(lián)想到VMWare或者VirtualBox一類的軟件。這些是模擬計(jì)算機(jī)的程序,包括模擬磁盤驅(qū)動(dòng)器、硬盤驅(qū)動(dòng)器、圖形卡等。例如,它們使你可以在此仿真計(jì)算機(jī)上運(yùn)行其他操作系統(tǒng)。這些確實(shí)是虛擬機(jī),但不是本書要討論的內(nèi)容。這是另一種形式的虛擬機(jī)。

我們即將討論并構(gòu)建的虛擬機(jī)是用來(lái)實(shí)現(xiàn)編程語(yǔ)言特性的。在這類虛擬機(jī)中,有些僅由幾個(gè)函數(shù)組成,有些由幾個(gè)模塊組成,還有些是類和對(duì)象的集合。很難描述這類虛擬機(jī)的表現(xiàn)形式,但是這并不重要。重要的是,它并不是模擬已經(jīng)存在的機(jī)器。它自身就是機(jī)器。

“虛擬”一詞體現(xiàn)在,它僅存在于軟件中,不存在于硬件中,因此它是純抽象的構(gòu)造。“機(jī)(器)”描述了它的行為。這些軟件的結(jié)構(gòu)就像一臺(tái)機(jī)器,但不僅僅是機(jī)器,它們模仿的是計(jì)算機(jī)的硬件行為。

這意味著,為了理解和構(gòu)建虛擬機(jī),我們需要學(xué)習(xí)真實(shí)物理機(jī)的工作原理。

一臺(tái)計(jì)算機(jī)到底如何工作呢?

這聽(tīng)起來(lái)是一個(gè)令人生畏的問(wèn)題,實(shí)際上5分鐘之內(nèi)就可以在一張紙上畫出答案。我不知道你的理解速度有多快,但我無(wú)法提前告訴你我在紙片上畫的內(nèi)容。無(wú)論如何,請(qǐng)讓我嘗試一下。

你在生命中遇到的幾乎每一臺(tái)計(jì)算機(jī)都遵循馮·諾依曼體系結(jié)構(gòu),該體系結(jié)構(gòu)描述了一種用數(shù)量很少的組件構(gòu)建功能強(qiáng)大的計(jì)算機(jī)的方法。

在馮·諾依曼模型中,計(jì)算機(jī)包括兩個(gè)核心部分:一個(gè)是包括算術(shù)邏輯單元(ALU)和多個(gè)處理器寄存器的處理單元,另一個(gè)是包括指令寄存器和程序計(jì)數(shù)器的控制單元。它們統(tǒng)一被稱為中央處理器,通常簡(jiǎn)稱為CPU。除此之外,計(jì)算機(jī)還包括內(nèi)存(RAM)、大容量存儲(chǔ)(硬盤)和輸入輸出(I/O)設(shè)備(鍵盤和顯示器)。

圖1-3是一臺(tái)計(jì)算機(jī)的工作簡(jiǎn)圖。

圖 1-3

在計(jì)算機(jī)打開(kāi)的一瞬間,CPU會(huì)執(zhí)行以下操作。

(1) 從內(nèi)存中預(yù)取指令。程序計(jì)數(shù)器告知CPU從內(nèi)存的哪個(gè)位置獲取下一條指令。

(2) 解析指令。甄別需要執(zhí)行什么操作。

(3) 執(zhí)行指令。這一步可能會(huì)修改寄存器的內(nèi)容,將數(shù)據(jù)從寄存器輸出到內(nèi)存,數(shù)據(jù)在內(nèi)存中移動(dòng),生成輸出,讀取輸入……

隨后計(jì)算機(jī)會(huì)再次從(1)開(kāi)始循環(huán)執(zhí)行。

以上3步稱為取指-解碼-執(zhí)行周期,也稱為指令周期。名詞“周期”來(lái)自于語(yǔ)句“計(jì)算機(jī)的時(shí)鐘速度以每秒的周期數(shù)表示,例如500 MHz”或者“我們?cè)诶速M(fèi)CPU周期”。

這是對(duì)計(jì)算機(jī)原理簡(jiǎn)短且易于理解的描述,但是我們可以使它變得更加簡(jiǎn)單。在本書中,我們不關(guān)注大容量存儲(chǔ)組件,只關(guān)注I/O機(jī)制。我們感興趣的是CPU和內(nèi)存之間的交互。這意味著我們可以為此集中精力,忽略硬盤和顯示器。

我們從以下問(wèn)題開(kāi)始研究:CPU如何處理內(nèi)存的不同部分?或者換句話問(wèn):CPU如何知道在何處存儲(chǔ)和檢索內(nèi)存中的內(nèi)容?

我們首先了解CPU如何取指。作為CPU的一部分,程序計(jì)數(shù)器始終追蹤從何處獲取下一條指令。“計(jì)數(shù)器”的字面意思是:計(jì)算機(jī)直接利用數(shù)字對(duì)內(nèi)存中的不同部分進(jìn)行尋址。

關(guān)于這一點(diǎn),我很想寫“把內(nèi)存想象成一個(gè)巨大的數(shù)組即可”,但是我害怕別人用書敲我的頭:你真是個(gè)傻瓜,內(nèi)存也毫無(wú)疑問(wèn)不是一個(gè)數(shù)組。所以我不會(huì)這么寫。但是,就像程序員使用索引訪問(wèn)數(shù)組元素一樣,CPU也利用數(shù)字作為地址訪問(wèn)內(nèi)存中的內(nèi)容。

計(jì)算機(jī)內(nèi)存并非“數(shù)組元素”,而是被分割成了一個(gè)個(gè)“字”。什么是“字”?它是計(jì)算機(jī)內(nèi)存中的最小可尋址區(qū)域,是尋址時(shí)的基本單位。字的大小取決于CPU的類型,但是在我們使用的計(jì)算機(jī)中,標(biāo)準(zhǔn)字的大小是32位和64位。

假設(shè)有一臺(tái)虛構(gòu)的計(jì)算機(jī),其字的大小為8位,內(nèi)存大小為13字節(jié)。內(nèi)存中一字可以包含一個(gè)ASCII字符,將Hello, World!存儲(chǔ)在內(nèi)存中則如圖1-4所示。

圖 1-4

字母“H”的內(nèi)存地址為0,“e”的內(nèi)存地址為1,第一個(gè)“l(fā)”的內(nèi)存地址為2,“W”的內(nèi)存地址為7,以此類推。我們可以通過(guò)內(nèi)存地址0到12訪問(wèn)Hello, World!的每個(gè)字母。“嘿,CPU,獲取內(nèi)存地址4處的字母”這個(gè)指令會(huì)返回字母“o”。很簡(jiǎn)單吧!看到這里,我知道你在想什么,如果將數(shù)字(內(nèi)存地址)保存到內(nèi)存中的另一個(gè)位置,我們就完成了一個(gè)指針的創(chuàng)建。

這就是在內(nèi)存中尋址數(shù)據(jù)以及CPU如何知道在何處獲取和存儲(chǔ)數(shù)據(jù)的基本思想,但是現(xiàn)實(shí)比這復(fù)雜很多。

前文提到過(guò),不同計(jì)算機(jī)字的大小不同。有的是8位,有的是16位、24位、32位或者64位。有時(shí)CPU使用字的大小與地址大小無(wú)關(guān)。還有些計(jì)算機(jī)做著完全不同的事,它們采用字節(jié)尋址,而不是字尋址。

如果你正在使用字尋址,并希望尋址單字節(jié)(這并不罕見(jiàn)),你不僅需要處理不同的字長(zhǎng),還需要處理偏移量。這種操作的開(kāi)銷很大,必須進(jìn)行優(yōu)化。

除此之外,我們直接告訴CPU在內(nèi)存中存儲(chǔ)和檢索數(shù)據(jù)的行為就像是一個(gè)童話。它在概念層面上是正確的,并且在學(xué)習(xí)時(shí)有助于理解,但如今的內(nèi)存訪問(wèn)是抽象化的,并且位于一層又一層的安全和性能優(yōu)化問(wèn)題之后。內(nèi)存不再是能夠隨意訪問(wèn)的區(qū)域,安全規(guī)則和虛擬機(jī)內(nèi)存機(jī)制會(huì)盡力阻止這種情況發(fā)生。

以上就是計(jì)算機(jī)工作方式的簡(jiǎn)單介紹,畢竟這不是本書的重點(diǎn)。之后討論一下虛擬內(nèi)存的工作原理。你可以從本書中了解到,如今的內(nèi)存訪問(wèn)不僅僅是將數(shù)字傳遞給CPU。不僅存在安全規(guī)則,在過(guò)去幾十年中,還出現(xiàn)了一系列關(guān)于內(nèi)存使用的約定,雖然不太嚴(yán)格。

馮·諾依曼體系結(jié)構(gòu)的創(chuàng)新之處在于,計(jì)算機(jī)的內(nèi)存不僅包含數(shù)據(jù),還包含由CPU指令構(gòu)成的程序。對(duì)現(xiàn)在的程序員來(lái)說(shuō),混合數(shù)據(jù)和程序聽(tīng)起來(lái)就是一個(gè)讓人流淚的想法。幾代以前的程序員聽(tīng)到這個(gè)想法應(yīng)該也會(huì)有同樣的反應(yīng),因?yàn)樗麄兯龅氖虑槎际桥?nèi)存使用協(xié)議,以防這種情況發(fā)生。

雖然這些程序與任何其他數(shù)據(jù)存儲(chǔ)在相同的內(nèi)存中,但它們通常不會(huì)存儲(chǔ)在相同的位置。特定的內(nèi)存區(qū)域用于存儲(chǔ)特定的內(nèi)容。這不僅是約定俗成的行為,而且受操作系統(tǒng)、CPU和計(jì)算機(jī)體系結(jié)構(gòu)其余部分的支配。

“無(wú)意義數(shù)據(jù)”,如“文本文件的內(nèi)容”或“HTTP請(qǐng)求的響應(yīng)”,位于內(nèi)存的某個(gè)區(qū)域中。構(gòu)成程序的指令存儲(chǔ)在另一個(gè)區(qū)域中,CPU可以從該區(qū)域直接獲取它們。此外,有一個(gè)區(qū)域保存程序使用的靜態(tài)數(shù)據(jù);還有一個(gè)區(qū)域是空的且未初始化,但屬于保留區(qū)域,程序運(yùn)行后就可以使用它。操作系統(tǒng)內(nèi)核的指令在內(nèi)存中也有自己的特定區(qū)域。

順便多說(shuō)一句,程序和“無(wú)意義數(shù)據(jù)”可能存儲(chǔ)在內(nèi)存中的不同位置,但重要的是它們都存在于同一個(gè)內(nèi)存中。“數(shù)據(jù)和程序都存在于同一個(gè)內(nèi)存中”,聽(tīng)起來(lái)它們好像是不同的,實(shí)際上由指令構(gòu)成的程序也是數(shù)據(jù)的一種。數(shù)據(jù)只有經(jīng)過(guò)CPU從內(nèi)存中預(yù)取、解碼、確認(rèn)正確并執(zhí)行這一過(guò)程,才會(huì)成為指令。如果CPU解碼的數(shù)據(jù)不是有效的指令,那么后果取決于CPU的設(shè)計(jì)。有些會(huì)觸發(fā)事件并給程序一次發(fā)送正確指令的機(jī)會(huì),有些則直接停止執(zhí)行程序。

對(duì)我們來(lái)說(shuō),最有趣的是,這是一個(gè)特定的內(nèi)存區(qū)域,一個(gè)用于存放棧的內(nèi)存區(qū)域。強(qiáng)調(diào)一下,它是。你可能聽(tīng)說(shuō)過(guò)它。“棧溢出”可能是它最著名的工作,其次讓它出名的還有“棧追蹤”。

棧到底是什么呢?它是內(nèi)存中的一個(gè)區(qū)域,以后進(jìn)先出(LIFO)的方式管理數(shù)據(jù),以壓棧和彈棧實(shí)現(xiàn)數(shù)據(jù)的伸縮,就像棧數(shù)據(jù)結(jié)構(gòu)一樣。但與這種通用數(shù)據(jù)結(jié)構(gòu)不同的是,只專注于一個(gè)目的:實(shí)現(xiàn)調(diào)用棧

在這里停一下,這真的讓人很困惑。“棧”“棧數(shù)據(jù)結(jié)構(gòu)”“調(diào)用棧”,這些都不太容易理解,尤其是這些名詞經(jīng)常隨意混合互換使用。但是,值得慶幸的是,如果仔細(xì)分辨這些名稱并注意它們背后的“原理”,事情就會(huì)變得很清晰。因此,讓我們一步步地解釋一次。

我們擁有一個(gè)內(nèi)存區(qū)域,CPU以LIFO方式訪問(wèn)和存儲(chǔ)其中的數(shù)據(jù)。這樣做是為了實(shí)現(xiàn)一個(gè)專門的,叫作調(diào)用棧

為什么需要調(diào)用棧?因?yàn)镃PU(或者是期望CPU按照預(yù)期工作的程序員)需要追蹤某些信息才能執(zhí)行程序。調(diào)用棧對(duì)此會(huì)有所幫助。追蹤什么信息?首先也是最重要的:當(dāng)前正在執(zhí)行哪個(gè)函數(shù),以及接下來(lái)執(zhí)行哪個(gè)指令。當(dāng)前函數(shù)之后需要執(zhí)行的指令信息,被稱為返回地址。這是CPU執(zhí)行當(dāng)前函數(shù)之后返回的地方。如果沒(méi)有這一信息,CPU只會(huì)把程序計(jì)數(shù)器加一并執(zhí)行下一高地址處的指令。而這可能與應(yīng)該發(fā)生的事情完全相反。指令在內(nèi)存中并不是按照?qǐng)?zhí)行順序存放的。想象一下,如果Go語(yǔ)言中的return語(yǔ)句丟失了會(huì)發(fā)生什么——這就是CPU需要追蹤返回地址的原因。調(diào)用棧還有助于保存函數(shù)局部的執(zhí)行相關(guān)數(shù)據(jù):函數(shù)調(diào)用的參數(shù)和僅在函數(shù)中使用的局部變量。

返回地址、參數(shù)和局部變量,理論上我們可以將這些信息保存在內(nèi)存中其他合適的可訪問(wèn)區(qū)域。但事實(shí)證明,使用來(lái)保存是完美的解決方案,因?yàn)楹瘮?shù)調(diào)用通常是嵌套的。當(dāng)進(jìn)入一個(gè)函數(shù)時(shí),相關(guān)數(shù)據(jù)被壓棧。執(zhí)行當(dāng)前函數(shù)時(shí),就不必通過(guò)調(diào)用外部函數(shù)來(lái)訪問(wèn)局部化相關(guān)數(shù)據(jù),只需要訪問(wèn)棧頂相關(guān)元素即可。如果當(dāng)前函數(shù)返回,則將局部化相關(guān)數(shù)據(jù)彈棧(因?yàn)檫@些數(shù)據(jù)不會(huì)再使用)。現(xiàn)在棧頂保留的是所調(diào)用外部函數(shù)的局部化相關(guān)數(shù)據(jù)。非常干凈整潔,對(duì)吧?

這就是為什么需要調(diào)用棧,以及為什么用來(lái)實(shí)現(xiàn)它。現(xiàn)在唯一的問(wèn)題是:為什么選這個(gè)臭名昭著的名字?為什么是?并不是因?yàn)樗鎯?chǔ)的是棧,而是因?yàn)槭褂眠@個(gè)內(nèi)存區(qū)域來(lái)實(shí)現(xiàn)調(diào)用棧是一個(gè)如此牢固且廣泛的約定,以至于現(xiàn)在它已被轉(zhuǎn)換為硬件。甚至某些CPU僅支持壓棧和彈棧的指令。在它們上面運(yùn)行的程序都以這種方式使用這個(gè)內(nèi)存區(qū)域來(lái)實(shí)現(xiàn)此機(jī)制

切記,調(diào)用棧只是一個(gè)概念,它不受特定內(nèi)存區(qū)域特定實(shí)現(xiàn)的約束。沒(méi)有硬件和操作系統(tǒng)強(qiáng)制支持和約束時(shí),在內(nèi)存中的任何一個(gè)區(qū)域都可以實(shí)現(xiàn)調(diào)用棧。事實(shí)上,這就是我們要做的。我們將實(shí)現(xiàn)自己的調(diào)用棧—— 一個(gè)虛擬調(diào)用棧。但在這樣做并從物理機(jī)切換到虛擬機(jī)之前,我們需要理解另一個(gè)概念以做好充分準(zhǔn)備。

現(xiàn)在你已經(jīng)知道棧是如何工作的,那你想象一下執(zhí)行一個(gè)程序時(shí),CPU訪問(wèn)這個(gè)內(nèi)存區(qū)域的頻率。肯定相當(dāng)高。這說(shuō)明CPU訪問(wèn)內(nèi)存的速度決定了程序運(yùn)行的速度。雖然內(nèi)存訪問(wèn)速度很快(眨一次眼的時(shí)間,CPU可以訪問(wèn)主內(nèi)存大約一百萬(wàn)次),但它不是即時(shí)的,仍然有成本。

這就是為什么計(jì)算機(jī)在另一個(gè)地方存儲(chǔ)數(shù)據(jù):處理器寄存器。寄存器是CPU的一部分,訪問(wèn)寄存器的速度要遠(yuǎn)快于訪問(wèn)內(nèi)存的速度。人們可能會(huì)問(wèn),為什么不把所有東西都存在寄存器中?因?yàn)榧拇嫫鞯臄?shù)目很小,而且它們不能容納與內(nèi)存一樣多的數(shù)據(jù),通常每個(gè)寄存器只能存儲(chǔ)一個(gè)字。例如,一個(gè)x86-64架構(gòu)的CPU包含16個(gè)通用寄存器,每個(gè)寄存器可以存儲(chǔ)64位的數(shù)據(jù)。

寄存器用于存儲(chǔ)小且被經(jīng)常訪問(wèn)的數(shù)據(jù)。例如,指向棧頂部的內(nèi)存地址通常存儲(chǔ)在寄存器中——至少是“通常”。寄存器的這種特定用法非常普遍,以至于大多數(shù)CPU有一個(gè)專門用于存儲(chǔ)該指針的指定寄存器,即所謂的棧指針。某些CPU指令的操作數(shù)和結(jié)果也可以存儲(chǔ)在寄存器中。如果CPU需要將兩個(gè)數(shù)字相加,則它們都將存儲(chǔ)在寄存器中,并且相加的結(jié)果也將保存在某個(gè)寄存器中。但這還不是全部。寄存器還有更多用例。如果經(jīng)常訪問(wèn)某一程序中的大量數(shù)據(jù),則可以將其地址存儲(chǔ)到寄存器中,這樣CPU就可以非常快速地訪問(wèn)它。不過(guò),對(duì)我們來(lái)說(shuō)最重要的是棧指針。我們很快會(huì)再次遇見(jiàn)它。

現(xiàn)在,你可以深呼吸并放松一下,因?yàn)槲锢頇C(jī)的工作原理大概就是上面描述的這樣。了解了寄存器和棧指針,有關(guān)物理機(jī)工作原理的知識(shí)就介紹完了。是時(shí)候開(kāi)始抽象化了,我們將從物理機(jī)走向虛擬機(jī)。

直截了當(dāng)?shù)卣f(shuō),虛擬機(jī)是由軟件實(shí)現(xiàn)的計(jì)算機(jī)。它是模擬計(jì)算機(jī)工作的軟件實(shí)體。當(dāng)然,“軟件實(shí)體”并不能表示虛擬機(jī)的全部,但我使用這個(gè)詞的主要目的是想說(shuō)明,虛擬機(jī)可以表示所有:一個(gè)函數(shù)、一個(gè)結(jié)構(gòu)體、一個(gè)對(duì)象、一個(gè)模塊,甚至整個(gè)程序。它能表示什么并不重要,重要的是它擔(dān)當(dāng)什么角色。

虛擬機(jī)跟物理機(jī)一樣,有特定的運(yùn)行循環(huán),即通過(guò)循環(huán)執(zhí)行“取指取解碼解執(zhí)行”來(lái)完成運(yùn)轉(zhuǎn)。它有一個(gè)程序計(jì)數(shù)器,可以獲取指令,然后解析并執(zhí)行它。與物理機(jī)類似,它同樣擁有棧,有時(shí)是調(diào)用棧,有時(shí)是寄存器。所有的一切全部?jī)?nèi)置在軟件中。

多說(shuō)無(wú)益,代碼為上。下面是一個(gè)用幾十行JavaScript代碼完成的虛擬機(jī):

let virtualMachine = function(program) {
let programCounter = 0;
let stack = [];
let stackPointer = 0;
while (programCounter < program.length) {
let currentInstruction = program[programCounter];
switch (currentInstruction) {
case PUSH:
stack[stackPointer] = program[programCounter+1];
stackPointer++;
programCounter++;
break;
case ADD:
right = stack[stackPointer-1]
stackPointer--;
left = stack[stackPointer-1]
stackPointer--;
stack[stackPointer] = left + right;
stackPointer++;
break;
case MINUS:
right = stack[stackPointer-1]
stackPointer--;
left = stack[stackPointer-1]
stackPointer--;
stack[stackPointer] = left - right;
stackPointer++;
break;
}
programCounter++;
}
console.log("stacktop: ", stack[stackPointer-1]);
}

它擁有一個(gè)程序計(jì)數(shù)器programCounter、一個(gè)棧stack,以及一個(gè)棧指針stackPointer。它有一個(gè)運(yùn)行循環(huán),只要程序中有指令,它就會(huì)執(zhí)行。先取出程序計(jì)數(shù)器指向的指令,然后解析并執(zhí)行它。這個(gè)循環(huán)每迭代一次,就是虛擬機(jī)的一個(gè)“循環(huán)周期”。

我們可以為這個(gè)虛擬機(jī)構(gòu)建一個(gè)程序并執(zhí)行它:

let program = [
PUSH, 3,
PUSH, 4,
ADD,
PUSH, 5,
MINUS
];
virtualMachine(program);

你是否能識(shí)別出這些指令中編碼的表達(dá)式?是這樣的:

(3 + 4) - 5

如果你沒(méi)有識(shí)別出也沒(méi)關(guān)系,你很快就能理解這一切。一旦習(xí)慣在棧上進(jìn)行算術(shù)運(yùn)算,這個(gè)program就不難理解。首先PUSH34添加到棧頂,然后ADD將它從棧頂彈出,相加后將結(jié)果壓棧,接著PUSH5添加到棧頂,然后MINUS將棧頂?shù)?個(gè)元素減去5,之后將結(jié)果壓棧。

循環(huán)完成后,虛擬機(jī)會(huì)將存在棧頂?shù)慕Y(jié)果打印出來(lái):

$ node virtual_machine.js
stacktop:  2

現(xiàn)在,這是一個(gè)可以正常工作的虛擬機(jī),只是它相當(dāng)簡(jiǎn)單。可以預(yù)見(jiàn),它并沒(méi)有展示出虛擬機(jī)的全部功能。構(gòu)建一個(gè)虛擬機(jī),可以像前文那樣,用約50行代碼,也可以用5萬(wàn)行甚至更多。二者之間的主要區(qū)別是功能和性能的不同選擇。

一個(gè)最重要的設(shè)計(jì)選擇是使用棧式虛擬機(jī)還是寄存器式虛擬機(jī)。這個(gè)選擇非常重要,因?yàn)樘摂M機(jī)是根據(jù)此架構(gòu)進(jìn)行分類的,就像編程語(yǔ)言從根源上分為“編譯型”和“解釋型”一樣。簡(jiǎn)單來(lái)說(shuō),棧式虛擬機(jī)和寄存器式虛擬機(jī)的區(qū)別是:虛擬機(jī)是利用棧(前文例子所演示的那樣)還是利用寄存器(虛擬寄存器)來(lái)完成計(jì)算。關(guān)于哪種選擇更好(讀取速度更快)的爭(zhēng)論一直存在,因?yàn)樾枰獧?quán)衡取舍并針對(duì)不同選擇做好準(zhǔn)備。

一般認(rèn)為棧式虛擬機(jī)及其相應(yīng)的編譯器更易于構(gòu)建。虛擬機(jī)需要的組件更少,其執(zhí)行的指令也更加簡(jiǎn)單,因?yàn)樗鼈儭皟H”使用了。缺點(diǎn)在于,需要執(zhí)行指令的頻率更高,因?yàn)樗胁僮鞅仨毻ㄟ^(guò)壓棧和彈棧才能完成。這就限制了人們可以采用性能優(yōu)化的基本規(guī)則的程度:與其嘗試做得更快,不如先嘗試做得更少。

構(gòu)建寄存器式虛擬機(jī)需要做更多的工作,因?yàn)榧拇嫫魇?strong>輔助添加的。它也擁有棧,不過(guò)不像棧式虛擬機(jī)那樣頻繁地使用棧,只是仍然有必要實(shí)現(xiàn)調(diào)用棧。寄存器式虛擬機(jī)的優(yōu)點(diǎn)是指令可以使用寄存器,因此與棧式虛擬機(jī)相比,其指令密度更高。指令可以直接使用寄存器,而不必將它們放到棧上,保證壓棧和彈棧的順序正確。一般來(lái)說(shuō),與棧式虛擬機(jī)相比,寄存器式虛擬機(jī)使用的指令更少。這會(huì)帶來(lái)更好的性能。但是,構(gòu)建產(chǎn)生這樣密集指令的編譯器需要花費(fèi)更多精力。正如前文所述,需要權(quán)衡取舍。

除了以上主要架構(gòu)選擇之外,構(gòu)建虛擬機(jī)還涉及許多其他決策。如何使用內(nèi)存,以及如何確定值的中間表示(在上一本書中,為Monkey求值器構(gòu)建對(duì)象系統(tǒng)時(shí)已經(jīng)討論過(guò))也是很重要的決策。此外還有無(wú)盡微小的決策,就像蜿蜒的兔子洞,可能會(huì)讓你迷失其中。讓我們選一個(gè),一探究竟。

在上文的例子中,我們利用switch表達(dá)式完成了虛擬機(jī)運(yùn)行循環(huán)中的分派工作。在虛擬機(jī)中,分派意味著在指令執(zhí)行之前,為該指令選擇一個(gè)合理的實(shí)現(xiàn)。在switch表達(dá)式中,指令的實(shí)現(xiàn)緊接著case語(yǔ)句。MINUS負(fù)責(zé)兩個(gè)值相減,ADD負(fù)責(zé)兩個(gè)值相加。這就是分派。雖然switch表達(dá)式似乎是唯一的選擇,但實(shí)際差之甚遠(yuǎn)。

switch表達(dá)式只是兔子洞的入口而已。當(dāng)尋求更高的性能時(shí),你需要走到更深處。之后你發(fā)現(xiàn),分派會(huì)使用跳轉(zhuǎn)表,會(huì)使用GOTO表達(dá)式,會(huì)使用直接或間接的線程代碼,因?yàn)椴还苣闶欠裣嘈牛?code>case分支足夠多的情況下(數(shù)百個(gè)或更多),switch可能是這些解決方案中最慢的一種。為了減少分派的開(kāi)銷,從性能的角度來(lái)看,switch語(yǔ)句的性能表現(xiàn)就像是取指-解碼-執(zhí)行過(guò)程中的取指-解碼部分消失了。以上足以讓你體會(huì)到兔子洞到底有多深。

現(xiàn)在,我們大致了解了什么是虛擬機(jī),以及構(gòu)建虛擬機(jī)的整個(gè)過(guò)程。如果你仍然不明白一些細(xì)節(jié),不用擔(dān)心。為了構(gòu)建自己的虛擬機(jī),我們會(huì)再次討論許多主題和想法,當(dāng)然,還有兔子洞。

讓我們分析一下剛剛學(xué)到的內(nèi)容。為什么要構(gòu)建虛擬機(jī)來(lái)實(shí)現(xiàn)編程語(yǔ)言?必須承認(rèn),這是困擾我時(shí)間最長(zhǎng)的問(wèn)題。即使在構(gòu)建了一些小型虛擬機(jī)并閱讀了一些大型虛擬機(jī)的源代碼之后,我仍然在思考:為什么?

當(dāng)實(shí)現(xiàn)一種編程語(yǔ)言時(shí),我們希望它是通用的,能夠執(zhí)行所有可能遇到的程序,而不僅僅是我們提供的示例函數(shù)。通用計(jì)算是我們追求的目標(biāo),而計(jì)算機(jī)為此提供了堅(jiān)實(shí)的模型。

如果基于此模型來(lái)構(gòu)建編程語(yǔ)言,它將擁有與計(jì)算機(jī)相同的計(jì)算能力。當(dāng)然這也是使程序執(zhí)行最快的一種方式。

但是,如果像計(jì)算機(jī)一樣執(zhí)行程序是最好且最快的方式,為什么不讓計(jì)算機(jī)自身來(lái)執(zhí)行程序,反而要構(gòu)建一個(gè)虛擬機(jī)呢?答案是:可移植性!我們可以為我們的編程語(yǔ)言編寫一個(gè)編譯器,以便在計(jì)算機(jī)上本地執(zhí)行翻譯后的程序。這些程序確實(shí)很快。但是對(duì)于每一種不同的計(jì)算機(jī)體系結(jié)構(gòu),我們都需要為其重新構(gòu)建一個(gè)新的編譯器。這將帶來(lái)大量的工作。所以,我們可以將程序轉(zhuǎn)換成虛擬機(jī)指令。虛擬機(jī)本身可以在與其實(shí)現(xiàn)語(yǔ)言一樣多的架構(gòu)上運(yùn)行。對(duì)于Go編程語(yǔ)言而言,它非常便于移植。

通過(guò)虛擬機(jī)來(lái)實(shí)現(xiàn)編程語(yǔ)言,還有一個(gè)我認(rèn)為極具吸引力的理由:虛擬機(jī)是領(lǐng)域特定的。這使它們與非虛擬機(jī)完全不同。計(jì)算機(jī)為我們提供了一個(gè)滿足所有計(jì)算需求的通用解決方案,并且不是領(lǐng)域特定的。這正是我們對(duì)一臺(tái)計(jì)算機(jī)的需求,因?yàn)橐谄渖线\(yùn)行各種程序。但是,如果我們不需要一臺(tái)通用的計(jì)算機(jī)怎么辦?如果程序員只需要計(jì)算機(jī)為其提供部分功能子集,又該怎么辦呢?

作為程序員,我們知道任何功能都需要付出代價(jià)。復(fù)雜度的增加和性能的下降只是常見(jiàn)的兩種代價(jià)。當(dāng)今的計(jì)算機(jī)具有很多功能。x86-64的CPU支持900~4000條指令,具體數(shù)字取決于你如何計(jì)算指令數(shù)。這包括兩個(gè)操作數(shù)進(jìn)行按位XOR的至少6種方法。這使計(jì)算機(jī)變得方便和通用。但這不是免費(fèi)的。像其他所有功能一樣,多功能性也需要付出代價(jià)。回想前文中那個(gè)微型虛擬機(jī)里涉及的switch表達(dá)式,花一秒鐘的時(shí)間來(lái)思考增加3997個(gè)case分支會(huì)對(duì)性能有什么影響。如果不確定虛擬機(jī)是否真的會(huì)變慢,那請(qǐng)問(wèn)問(wèn)自己,為該虛擬機(jī)維護(hù)代碼或編程的難度怎樣。好消息是我們可以扭轉(zhuǎn)這一局面。如果擯棄不需要的功能,會(huì)速度更快,復(fù)雜性更低,維護(hù)性更強(qiáng),結(jié)構(gòu)更輕便。這就是虛擬機(jī)發(fā)揮作用的地方。

虛擬機(jī)就像一臺(tái)定制計(jì)算機(jī)。它擁有自定義組件和自定義的機(jī)器語(yǔ)言。相當(dāng)于它優(yōu)化為只能使用單一的編程語(yǔ)言。所有不必要的功能都被裁剪,剩下的都是高度專業(yè)化的功能。由于不需要像通用計(jì)算機(jī)那樣通用,因此它的功能更集中。你可以集中精力使這臺(tái)高度專業(yè)化和定制化的機(jī)器發(fā)揮最大作用,并盡可能地快。高度專業(yè)化和領(lǐng)域特定性與裁剪不必要的功能一樣重要。

當(dāng)看到虛擬機(jī)執(zhí)行的指令時(shí),這些為什么如此重要就變得愈加清晰,而這些正是我們前文一直避而不談的東西。還記得我們?yōu)槲⑿吞摂M機(jī)提供的信息嗎?如下所示:

let program = [
PUSH, 3,
PUSH, 4,
ADD,
PUSH, 5,
MINUS
];
virtualMachine(program);

現(xiàn)在,是否已經(jīng)理解了呢?什么是PUSH,什么是ADD,什么又是MINUS?下面是它們的定義:

const PUSH = 'PUSH';
const ADD = 'ADD';
const MINUS = 'MINUS';

PUSHADDMINUS只是引用字符串的常量。沒(méi)有任何神奇之處。是不是很失望?這些定義就像玩具一樣,僅與虛擬機(jī)的其余部分一起用于說(shuō)明。它們并沒(méi)有回答這里出現(xiàn)的更大、更有趣的問(wèn)題:虛擬機(jī)究竟執(zhí)行了什么操作?

虛擬機(jī)執(zhí)行字節(jié)碼。就像計(jì)算機(jī)執(zhí)行的機(jī)器碼一樣,字節(jié)碼也是由機(jī)器指令構(gòu)成的。之所以叫字節(jié)碼,是因?yàn)樗兄噶畹牟僮鞔a大小均為一字節(jié)。

“操作碼”是指令的“操作”部分。前文提到的PUSH就是一種操作碼,不過(guò)在我們的示例代碼中,它是一個(gè)多字節(jié)操作碼,不是一字節(jié)的。在正常的實(shí)現(xiàn)中,PUSH只是一個(gè)引用操作碼的名稱,該操作碼本身是一字節(jié)寬。這些名稱(PUSH或者POP)被稱為助記符。它們的存在價(jià)值是幫助程序員記住操作碼。

操作碼的操作數(shù)(也稱作參數(shù))也包含在字節(jié)碼中。操作數(shù)緊跟著操作碼,它們彼此并列在一起。不過(guò)操作數(shù)的大小并不一定是一字節(jié)。如果操作數(shù)是一個(gè)大于255的整數(shù),那么就需要多個(gè)字節(jié)來(lái)表示它。有些操作碼有多個(gè)操作數(shù),有的只有一個(gè)操作數(shù),有些甚至一個(gè)操作數(shù)都沒(méi)有。不管字節(jié)碼被設(shè)計(jì)成寄存器式還是棧式,它都有重大影響。

你可以把字節(jié)碼想象成一系列的操作碼和操作數(shù),一個(gè)接一個(gè)并排分布在內(nèi)存中,如圖1-5所示。

圖 1-5

圖1-5能幫助理解基本意思。字節(jié)碼是幾乎毫無(wú)可讀性的二進(jìn)制格式,無(wú)法像讀文本一樣閱讀。助記符,例如PUSH,并不會(huì)顯示在實(shí)際的字節(jié)碼中。取而代之的是它所引用的操作碼,這些操作碼以數(shù)字表示,具體是什么數(shù)字完全取決于定義字節(jié)碼的人。例如,PUSH助記符由0表示,POP則由23表示。

操作數(shù)同樣依賴于它自身的值決定用多少字節(jié)來(lái)進(jìn)行編碼。如果操作數(shù)需要多字節(jié)來(lái)表示,編碼順序就顯得格外重要。目前存在兩種編碼順序:大端編碼小端編碼。小端編碼的意思是原始數(shù)據(jù)中的低位放在最前面并存儲(chǔ)在最低的內(nèi)存中。大端編碼則相反:高位存儲(chǔ)在最低的內(nèi)存中。

假如我們是字節(jié)碼設(shè)計(jì)者,我們將PUSH助記符用1表示,ADD2表示,整型采用大端存儲(chǔ)。對(duì)上面實(shí)例進(jìn)行編碼并布局在內(nèi)存中,情況如圖1-6所示。

圖 1-6

我們剛剛所做的——將一個(gè)人類可讀的字節(jié)碼轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)——由叫作匯編器的程序完成。你在非虛擬的機(jī)器代碼中可能聽(tīng)說(shuō)過(guò)它們,這里也是一樣。匯編語(yǔ)言是字節(jié)碼的可讀版本,包含助記符和可讀操作數(shù),匯編器能將其轉(zhuǎn)換為二進(jìn)制字節(jié)碼。反之,將二進(jìn)制表示轉(zhuǎn)換成可讀表示的程序,稱為反匯編器。

對(duì)于字節(jié)碼純技術(shù)部分的介紹到此為止。任何更進(jìn)一步的探索都會(huì)變得更專業(yè)、更具體。字節(jié)碼格式過(guò)于多樣化和專業(yè)化,我們無(wú)法在此處給出更通用的說(shuō)明和描述。就像執(zhí)行字節(jié)碼的虛擬機(jī)一樣,字節(jié)碼在創(chuàng)建時(shí)也需要有一個(gè)具體的目標(biāo)。

字節(jié)碼是一種領(lǐng)域特定的語(yǔ)言。它是定制虛擬機(jī)的定制機(jī)器語(yǔ)言。這就是它的魔力所在。字節(jié)碼可以是專業(yè)化的,它不是通用的,不必支持所有可能的情況。它只需支持可以編譯為字節(jié)碼的源語(yǔ)言所需要的功能。

不僅如此,除了僅支持少數(shù)指令之外,字節(jié)碼還包括在領(lǐng)域特定虛擬機(jī)上下文中才有意義的領(lǐng)域特定指令。例如,Java虛擬機(jī)(JVM)的字節(jié)碼包括以下指令:invokeinterface用于調(diào)用接口方法,getstatic用于獲取類的靜態(tài)字段,new用于為指定的類創(chuàng)建對(duì)象。Ruby的字節(jié)碼有:putself指令用于將self壓入棧,send用于向?qū)ο蟀l(fā)送消息,putobject用于將對(duì)象壓入棧。Lua的字節(jié)碼具有訪問(wèn)和操作表和元組的專用指令。在x86-64的通用指令集中找不到以上任何指令。

這種通過(guò)使用自定義字節(jié)碼格式實(shí)現(xiàn)專業(yè)化的能力是構(gòu)建虛擬機(jī)的重要原因之一。這不僅使編譯、維護(hù)和調(diào)試變得更加容易,而且所得到的代碼也更加密集,因?yàn)樗磉_(dá)某些內(nèi)容所使用的指令更少,從而使代碼執(zhí)行起來(lái)更快。

現(xiàn)在,如果所有關(guān)于自定義虛擬機(jī)、量身定制的機(jī)器代碼、手工構(gòu)建編譯器的討論都沒(méi)能引起你的興趣,那么這是你放棄本書的最后機(jī)會(huì)。我們將正式開(kāi)始。

主站蜘蛛池模板: 黑山县| 文水县| 邢台市| 辽阳市| 香港 | 邵阳县| 江门市| 都兰县| 利辛县| 峨眉山市| 健康| 常州市| 兰考县| 象山县| 观塘区| 西安市| 天水市| 司法| 阜新| 砚山县| 千阳县| 郸城县| 乌拉特后旗| 乌拉特中旗| 侯马市| 新疆| 嵩明县| 信阳市| 南皮县| 开鲁县| 汝阳县| 南宫市| 株洲县| 洞头县| 乌鲁木齐县| 营山县| 淳安县| 开江县| 南宫市| 桓台县| 桃园市|