- 用Go語言自制編譯器
- (德)索斯藤·鮑爾
- 8880字
- 2022-06-17 10:46:10
1.2 虛擬機與物理機
提及虛擬機,你可能會聯想到VMWare或者VirtualBox一類的軟件。這些是模擬計算機的程序,包括模擬磁盤驅動器、硬盤驅動器、圖形卡等。例如,它們使你可以在此仿真計算機上運行其他操作系統。這些確實是虛擬機,但不是本書要討論的內容。這是另一種形式的虛擬機。
我們即將討論并構建的虛擬機是用來實現編程語言特性的。在這類虛擬機中,有些僅由幾個函數組成,有些由幾個模塊組成,還有些是類和對象的集合。很難描述這類虛擬機的表現形式,但是這并不重要。重要的是,它并不是模擬已經存在的機器。它自身就是機器。
“虛擬”一詞體現在,它僅存在于軟件中,不存在于硬件中,因此它是純抽象的構造。“機(器)”描述了它的行為。這些軟件的結構就像一臺機器,但不僅僅是機器,它們模仿的是計算機的硬件行為。
這意味著,為了理解和構建虛擬機,我們需要學習真實物理機的工作原理。
1.2.1 物理機
一臺計算機到底如何工作呢?
這聽起來是一個令人生畏的問題,實際上5分鐘之內就可以在一張紙上畫出答案。我不知道你的理解速度有多快,但我無法提前告訴你我在紙片上畫的內容。無論如何,請讓我嘗試一下。
你在生命中遇到的幾乎每一臺計算機都遵循馮·諾依曼體系結構,該體系結構描述了一種用數量很少的組件構建功能強大的計算機的方法。
在馮·諾依曼模型中,計算機包括兩個核心部分:一個是包括算術邏輯單元(ALU)和多個處理器寄存器的處理單元,另一個是包括指令寄存器和程序計數器的控制單元。它們統一被稱為中央處理器,通常簡稱為CPU。除此之外,計算機還包括內存(RAM)、大容量存儲(硬盤)和輸入輸出(I/O)設備(鍵盤和顯示器)。
圖1-3是一臺計算機的工作簡圖。

圖 1-3
在計算機打開的一瞬間,CPU會執行以下操作。
(1) 從內存中預取指令。程序計數器告知CPU從內存的哪個位置獲取下一條指令。
(2) 解析指令。甄別需要執行什么操作。
(3) 執行指令。這一步可能會修改寄存器的內容,將數據從寄存器輸出到內存,數據在內存中移動,生成輸出,讀取輸入……
隨后計算機會再次從(1)開始循環執行。
以上3步稱為取指-解碼-執行周期,也稱為指令周期。名詞“周期”來自于語句“計算機的時鐘速度以每秒的周期數表示,例如500 MHz”或者“我們在浪費CPU周期”。
這是對計算機原理簡短且易于理解的描述,但是我們可以使它變得更加簡單。在本書中,我們不關注大容量存儲組件,只關注I/O機制。我們感興趣的是CPU和內存之間的交互。這意味著我們可以為此集中精力,忽略硬盤和顯示器。
我們從以下問題開始研究:CPU如何處理內存的不同部分?或者換句話問:CPU如何知道在何處存儲和檢索內存中的內容?
我們首先了解CPU如何取指。作為CPU的一部分,程序計數器始終追蹤從何處獲取下一條指令。“計數器”的字面意思是:計算機直接利用數字對內存中的不同部分進行尋址。
關于這一點,我很想寫“把內存想象成一個巨大的數組即可”,但是我害怕別人用書敲我的頭:你真是個傻瓜,內存也毫無疑問不是一個數組。所以我不會這么寫。但是,就像程序員使用索引訪問數組元素一樣,CPU也利用數字作為地址訪問內存中的內容。
計算機內存并非“數組元素”,而是被分割成了一個個“字”。什么是“字”?它是計算機內存中的最小可尋址區域,是尋址時的基本單位。字的大小取決于CPU的類型,但是在我們使用的計算機中,標準字的大小是32位和64位。
假設有一臺虛構的計算機,其字的大小為8位,內存大小為13字節。內存中一字可以包含一個ASCII字符,將Hello, World!
存儲在內存中則如圖1-4所示。

圖 1-4
字母“H”的內存地址為0,“e”的內存地址為1,第一個“l”的內存地址為2,“W”的內存地址為7,以此類推。我們可以通過內存地址0到12訪問Hello, World!
的每個字母。“嘿,CPU,獲取內存地址4處的字母”這個指令會返回字母“o”。很簡單吧!看到這里,我知道你在想什么,如果將數字(內存地址)保存到內存中的另一個位置,我們就完成了一個指針的創建。
這就是在內存中尋址數據以及CPU如何知道在何處獲取和存儲數據的基本思想,但是現實比這復雜很多。
前文提到過,不同計算機字的大小不同。有的是8位,有的是16位、24位、32位或者64位。有時CPU使用字的大小與地址大小無關。還有些計算機做著完全不同的事,它們采用字節尋址,而不是字尋址。
如果你正在使用字尋址,并希望尋址單字節(這并不罕見),你不僅需要處理不同的字長,還需要處理偏移量。這種操作的開銷很大,必須進行優化。
除此之外,我們直接告訴CPU在內存中存儲和檢索數據的行為就像是一個童話。它在概念層面上是正確的,并且在學習時有助于理解,但如今的內存訪問是抽象化的,并且位于一層又一層的安全和性能優化問題之后。內存不再是能夠隨意訪問的區域,安全規則和虛擬機內存機制會盡力阻止這種情況發生。
以上就是計算機工作方式的簡單介紹,畢竟這不是本書的重點。之后討論一下虛擬內存的工作原理。你可以從本書中了解到,如今的內存訪問不僅僅是將數字傳遞給CPU。不僅存在安全規則,在過去幾十年中,還出現了一系列關于內存使用的約定,雖然不太嚴格。
馮·諾依曼體系結構的創新之處在于,計算機的內存不僅包含數據,還包含由CPU指令構成的程序。對現在的程序員來說,混合數據和程序聽起來就是一個讓人流淚的想法。幾代以前的程序員聽到這個想法應該也會有同樣的反應,因為他們所做的事情都是努力建立內存使用協議,以防這種情況發生。
雖然這些程序與任何其他數據存儲在相同的內存中,但它們通常不會存儲在相同的位置。特定的內存區域用于存儲特定的內容。這不僅是約定俗成的行為,而且受操作系統、CPU和計算機體系結構其余部分的支配。
“無意義數據”,如“文本文件的內容”或“HTTP請求的響應”,位于內存的某個區域中。構成程序的指令存儲在另一個區域中,CPU可以從該區域直接獲取它們。此外,有一個區域保存程序使用的靜態數據;還有一個區域是空的且未初始化,但屬于保留區域,程序運行后就可以使用它。操作系統內核的指令在內存中也有自己的特定區域。
順便多說一句,程序和“無意義數據”可能存儲在內存中的不同位置,但重要的是它們都存在于同一個內存中。“數據和程序都存在于同一個內存中”,聽起來它們好像是不同的,實際上由指令構成的程序也是數據的一種。數據只有經過CPU從內存中預取、解碼、確認正確并執行這一過程,才會成為指令。如果CPU解碼的數據不是有效的指令,那么后果取決于CPU的設計。有些會觸發事件并給程序一次發送正確指令的機會,有些則直接停止執行程序。
對我們來說,最有趣的是,這是一個特定的內存區域,一個用于存放棧的內存區域。強調一下,它是棧。你可能聽說過它。“棧溢出”可能是它最著名的工作,其次讓它出名的還有“棧追蹤”。
棧到底是什么呢?它是內存中的一個區域,以后進先出(LIFO)的方式管理數據,以壓棧和彈棧實現數據的伸縮,就像棧數據結構一樣。但與這種通用數據結構不同的是,棧只專注于一個目的:實現調用棧。
在這里停一下,這真的讓人很困惑。“棧”“棧數據結構”“調用棧”,這些都不太容易理解,尤其是這些名詞經常隨意混合互換使用。但是,值得慶幸的是,如果仔細分辨這些名稱并注意它們背后的“原理”,事情就會變得很清晰。因此,讓我們一步步地解釋一次。
我們擁有一個內存區域,CPU以LIFO方式訪問和存儲其中的數據。這樣做是為了實現一個專門的棧,叫作調用棧。
為什么需要調用棧?因為CPU(或者是期望CPU按照預期工作的程序員)需要追蹤某些信息才能執行程序。調用棧對此會有所幫助。追蹤什么信息?首先也是最重要的:當前正在執行哪個函數,以及接下來執行哪個指令。當前函數之后需要執行的指令信息,被稱為返回地址。這是CPU執行當前函數之后返回的地方。如果沒有這一信息,CPU只會把程序計數器加一并執行下一高地址處的指令。而這可能與應該發生的事情完全相反。指令在內存中并不是按照執行順序存放的。想象一下,如果Go語言中的return
語句丟失了會發生什么——這就是CPU需要追蹤返回地址的原因。調用棧還有助于保存函數局部的執行相關數據:函數調用的參數和僅在函數中使用的局部變量。
返回地址、參數和局部變量,理論上我們可以將這些信息保存在內存中其他合適的可訪問區域。但事實證明,使用棧來保存是完美的解決方案,因為函數調用通常是嵌套的。當進入一個函數時,相關數據被壓棧。執行當前函數時,就不必通過調用外部函數來訪問局部化相關數據,只需要訪問棧頂相關元素即可。如果當前函數返回,則將局部化相關數據彈棧(因為這些數據不會再使用)。現在棧頂保留的是所調用外部函數的局部化相關數據。非常干凈整潔,對吧?
這就是為什么需要調用棧,以及為什么用棧來實現它。現在唯一的問題是:為什么選這個臭名昭著的名字?為什么是棧?并不是因為它存儲的是棧,而是因為使用這個內存區域來實現調用棧是一個如此牢固且廣泛的約定,以至于現在它已被轉換為硬件。甚至某些CPU僅支持壓棧和彈棧的指令。在它們上面運行的程序都以這種方式使用這個內存區域來實現此機制。
切記,調用棧只是一個概念,它不受特定內存區域特定實現的約束。沒有硬件和操作系統強制支持和約束時,在內存中的任何一個區域都可以實現調用棧。事實上,這就是我們要做的。我們將實現自己的調用棧—— 一個虛擬調用棧。但在這樣做并從物理機切換到虛擬機之前,我們需要理解另一個概念以做好充分準備。
現在你已經知道棧是如何工作的,那你想象一下執行一個程序時,CPU訪問這個內存區域的頻率。肯定相當高。這說明CPU訪問內存的速度決定了程序運行的速度。雖然內存訪問速度很快(眨一次眼的時間,CPU可以訪問主內存大約一百萬次),但它不是即時的,仍然有成本。
這就是為什么計算機在另一個地方存儲數據:處理器寄存器。寄存器是CPU的一部分,訪問寄存器的速度要遠快于訪問內存的速度。人們可能會問,為什么不把所有東西都存在寄存器中?因為寄存器的數目很小,而且它們不能容納與內存一樣多的數據,通常每個寄存器只能存儲一個字。例如,一個x86-64架構的CPU包含16個通用寄存器,每個寄存器可以存儲64位的數據。
寄存器用于存儲小且被經常訪問的數據。例如,指向棧頂部的內存地址通常存儲在寄存器中——至少是“通常”。寄存器的這種特定用法非常普遍,以至于大多數CPU有一個專門用于存儲該指針的指定寄存器,即所謂的棧指針。某些CPU指令的操作數和結果也可以存儲在寄存器中。如果CPU需要將兩個數字相加,則它們都將存儲在寄存器中,并且相加的結果也將保存在某個寄存器中。但這還不是全部。寄存器還有更多用例。如果經常訪問某一程序中的大量數據,則可以將其地址存儲到寄存器中,這樣CPU就可以非常快速地訪問它。不過,對我們來說最重要的是棧指針。我們很快會再次遇見它。
現在,你可以深呼吸并放松一下,因為物理機的工作原理大概就是上面描述的這樣。了解了寄存器和棧指針,有關物理機工作原理的知識就介紹完了。是時候開始抽象化了,我們將從物理機走向虛擬機。
1.2.2 什么是虛擬機
直截了當地說,虛擬機是由軟件實現的計算機。它是模擬計算機工作的軟件實體。當然,“軟件實體”并不能表示虛擬機的全部,但我使用這個詞的主要目的是想說明,虛擬機可以表示所有:一個函數、一個結構體、一個對象、一個模塊,甚至整個程序。它能表示什么并不重要,重要的是它擔當什么角色。
虛擬機跟物理機一樣,有特定的運行循環,即通過循環執行“取指取解碼解執行”來完成運轉。它有一個程序計數器,可以獲取指令,然后解析并執行它。與物理機類似,它同樣擁有棧,有時是調用棧,有時是寄存器。所有的一切全部內置在軟件中。
多說無益,代碼為上。下面是一個用幾十行JavaScript代碼完成的虛擬機:
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]);
}
它擁有一個程序計數器programCounter
、一個棧stack
,以及一個棧指針stackPointer
。它有一個運行循環,只要程序中有指令,它就會執行。先取出程序計數器指向的指令,然后解析并執行它。這個循環每迭代一次,就是虛擬機的一個“循環周期”。
我們可以為這個虛擬機構建一個程序并執行它:
let program = [
PUSH, 3,
PUSH, 4,
ADD,
PUSH, 5,
MINUS
];
virtualMachine(program);
你是否能識別出這些指令中編碼的表達式?是這樣的:
(3 + 4) - 5
如果你沒有識別出也沒關系,你很快就能理解這一切。一旦習慣在棧上進行算術運算,這個program
就不難理解。首先PUSH
將3
和4
添加到棧頂,然后ADD
將它從棧頂彈出,相加后將結果壓棧,接著PUSH
將5
添加到棧頂,然后MINUS
將棧頂第2個元素減去5
,之后將結果壓棧。
循環完成后,虛擬機會將存在棧頂的結果打印出來:
$ node virtual_machine.js
stacktop: 2
現在,這是一個可以正常工作的虛擬機,只是它相當簡單。可以預見,它并沒有展示出虛擬機的全部功能。構建一個虛擬機,可以像前文那樣,用約50行代碼,也可以用5萬行甚至更多。二者之間的主要區別是功能和性能的不同選擇。
一個最重要的設計選擇是使用棧式虛擬機還是寄存器式虛擬機。這個選擇非常重要,因為虛擬機是根據此架構進行分類的,就像編程語言從根源上分為“編譯型”和“解釋型”一樣。簡單來說,棧式虛擬機和寄存器式虛擬機的區別是:虛擬機是利用棧(前文例子所演示的那樣)還是利用寄存器(虛擬寄存器)來完成計算。關于哪種選擇更好(讀取速度更快)的爭論一直存在,因為需要權衡取舍并針對不同選擇做好準備。
一般認為棧式虛擬機及其相應的編譯器更易于構建。虛擬機需要的組件更少,其執行的指令也更加簡單,因為它們“僅”使用了棧。缺點在于,需要執行指令的頻率更高,因為所有操作必須通過壓棧和彈棧才能完成。這就限制了人們可以采用性能優化的基本規則的程度:與其嘗試做得更快,不如先嘗試做得更少。
構建寄存器式虛擬機需要做更多的工作,因為寄存器是輔助添加的。它也擁有棧,不過不像棧式虛擬機那樣頻繁地使用棧,只是仍然有必要實現調用棧。寄存器式虛擬機的優點是指令可以使用寄存器,因此與棧式虛擬機相比,其指令密度更高。指令可以直接使用寄存器,而不必將它們放到棧上,保證壓棧和彈棧的順序正確。一般來說,與棧式虛擬機相比,寄存器式虛擬機使用的指令更少。這會帶來更好的性能。但是,構建產生這樣密集指令的編譯器需要花費更多精力。正如前文所述,需要權衡取舍。
除了以上主要架構選擇之外,構建虛擬機還涉及許多其他決策。如何使用內存,以及如何確定值的中間表示(在上一本書中,為Monkey求值器構建對象系統時已經討論過)也是很重要的決策。此外還有無盡微小的決策,就像蜿蜒的兔子洞,可能會讓你迷失其中。讓我們選一個,一探究竟。
在上文的例子中,我們利用switch
表達式完成了虛擬機運行循環中的分派工作。在虛擬機中,分派意味著在指令執行之前,為該指令選擇一個合理的實現。在switch
表達式中,指令的實現緊接著case
語句。MINUS
負責兩個值相減,ADD
負責兩個值相加。這就是分派。雖然switch
表達式似乎是唯一的選擇,但實際差之甚遠。
switch
表達式只是兔子洞的入口而已。當尋求更高的性能時,你需要走到更深處。之后你發現,分派會使用跳轉表,會使用GOTO
表達式,會使用直接或間接的線程代碼,因為不管你是否相信,在case
分支足夠多的情況下(數百個或更多),switch
可能是這些解決方案中最慢的一種。為了減少分派的開銷,從性能的角度來看,switch
語句的性能表現就像是取指-解碼-執行過程中的取指-解碼部分消失了。以上足以讓你體會到兔子洞到底有多深。
現在,我們大致了解了什么是虛擬機,以及構建虛擬機的整個過程。如果你仍然不明白一些細節,不用擔心。為了構建自己的虛擬機,我們會再次討論許多主題和想法,當然,還有兔子洞。
1.2.3 為什么要構建虛擬機
讓我們分析一下剛剛學到的內容。為什么要構建虛擬機來實現編程語言?必須承認,這是困擾我時間最長的問題。即使在構建了一些小型虛擬機并閱讀了一些大型虛擬機的源代碼之后,我仍然在思考:為什么?
當實現一種編程語言時,我們希望它是通用的,能夠執行所有可能遇到的程序,而不僅僅是我們提供的示例函數。通用計算是我們追求的目標,而計算機為此提供了堅實的模型。
如果基于此模型來構建編程語言,它將擁有與計算機相同的計算能力。當然這也是使程序執行最快的一種方式。
但是,如果像計算機一樣執行程序是最好且最快的方式,為什么不讓計算機自身來執行程序,反而要構建一個虛擬機呢?答案是:可移植性!我們可以為我們的編程語言編寫一個編譯器,以便在計算機上本地執行翻譯后的程序。這些程序確實很快。但是對于每一種不同的計算機體系結構,我們都需要為其重新構建一個新的編譯器。這將帶來大量的工作。所以,我們可以將程序轉換成虛擬機指令。虛擬機本身可以在與其實現語言一樣多的架構上運行。對于Go編程語言而言,它非常便于移植。
通過虛擬機來實現編程語言,還有一個我認為極具吸引力的理由:虛擬機是領域特定的。這使它們與非虛擬機完全不同。計算機為我們提供了一個滿足所有計算需求的通用解決方案,并且不是領域特定的。這正是我們對一臺計算機的需求,因為要在其上運行各種程序。但是,如果我們不需要一臺通用的計算機怎么辦?如果程序員只需要計算機為其提供部分功能子集,又該怎么辦呢?
作為程序員,我們知道任何功能都需要付出代價。復雜度的增加和性能的下降只是常見的兩種代價。當今的計算機具有很多功能。x86-64的CPU支持900~4000條指令,具體數字取決于你如何計算指令數。這包括兩個操作數進行按位XOR的至少6種方法。這使計算機變得方便和通用。但這不是免費的。像其他所有功能一樣,多功能性也需要付出代價。回想前文中那個微型虛擬機里涉及的switch
表達式,花一秒鐘的時間來思考增加3997個case
分支會對性能有什么影響。如果不確定虛擬機是否真的會變慢,那請問問自己,為該虛擬機維護代碼或編程的難度怎樣。好消息是我們可以扭轉這一局面。如果擯棄不需要的功能,會速度更快,復雜性更低,維護性更強,結構更輕便。這就是虛擬機發揮作用的地方。
虛擬機就像一臺定制計算機。它擁有自定義組件和自定義的機器語言。相當于它優化為只能使用單一的編程語言。所有不必要的功能都被裁剪,剩下的都是高度專業化的功能。由于不需要像通用計算機那樣通用,因此它的功能更集中。你可以集中精力使這臺高度專業化和定制化的機器發揮最大作用,并盡可能地快。高度專業化和領域特定性與裁剪不必要的功能一樣重要。
當看到虛擬機執行的指令時,這些為什么如此重要就變得愈加清晰,而這些正是我們前文一直避而不談的東西。還記得我們為微型虛擬機提供的信息嗎?如下所示:
let program = [
PUSH, 3,
PUSH, 4,
ADD,
PUSH, 5,
MINUS
];
virtualMachine(program);
現在,是否已經理解了呢?什么是PUSH
,什么是ADD
,什么又是MINUS
?下面是它們的定義:
const PUSH = 'PUSH';
const ADD = 'ADD';
const MINUS = 'MINUS';
PUSH
、ADD
和MINUS
只是引用字符串的常量。沒有任何神奇之處。是不是很失望?這些定義就像玩具一樣,僅與虛擬機的其余部分一起用于說明。它們并沒有回答這里出現的更大、更有趣的問題:虛擬機究竟執行了什么操作?
1.2.4 字節碼
虛擬機執行字節碼。就像計算機執行的機器碼一樣,字節碼也是由機器指令構成的。之所以叫字節碼,是因為所有指令的操作碼大小均為一字節。
“操作碼”是指令的“操作”部分。前文提到的PUSH
就是一種操作碼,不過在我們的示例代碼中,它是一個多字節操作碼,不是一字節的。在正常的實現中,PUSH
只是一個引用操作碼的名稱,該操作碼本身是一字節寬。這些名稱(PUSH
或者POP
)被稱為助記符。它們的存在價值是幫助程序員記住操作碼。
操作碼的操作數(也稱作參數)也包含在字節碼中。操作數緊跟著操作碼,它們彼此并列在一起。不過操作數的大小并不一定是一字節。如果操作數是一個大于255的整數,那么就需要多個字節來表示它。有些操作碼有多個操作數,有的只有一個操作數,有些甚至一個操作數都沒有。不管字節碼被設計成寄存器式還是棧式,它都有重大影響。
你可以把字節碼想象成一系列的操作碼和操作數,一個接一個并排分布在內存中,如圖1-5所示。

圖 1-5
圖1-5能幫助理解基本意思。字節碼是幾乎毫無可讀性的二進制格式,無法像讀文本一樣閱讀。助記符,例如PUSH
,并不會顯示在實際的字節碼中。取而代之的是它所引用的操作碼,這些操作碼以數字表示,具體是什么數字完全取決于定義字節碼的人。例如,PUSH
助記符由0
表示,POP
則由23
表示。
操作數同樣依賴于它自身的值決定用多少字節來進行編碼。如果操作數需要多字節來表示,編碼順序就顯得格外重要。目前存在兩種編碼順序:大端編碼和小端編碼。小端編碼的意思是原始數據中的低位放在最前面并存儲在最低的內存中。大端編碼則相反:高位存儲在最低的內存中。
假如我們是字節碼設計者,我們將PUSH
助記符用1
表示,ADD
用2
表示,整型采用大端存儲。對上面實例進行編碼并布局在內存中,情況如圖1-6所示。

圖 1-6
我們剛剛所做的——將一個人類可讀的字節碼轉換成二進制數據——由叫作匯編器的程序完成。你在非虛擬的機器代碼中可能聽說過它們,這里也是一樣。匯編語言是字節碼的可讀版本,包含助記符和可讀操作數,匯編器能將其轉換為二進制字節碼。反之,將二進制表示轉換成可讀表示的程序,稱為反匯編器。
對于字節碼純技術部分的介紹到此為止。任何更進一步的探索都會變得更專業、更具體。字節碼格式過于多樣化和專業化,我們無法在此處給出更通用的說明和描述。就像執行字節碼的虛擬機一樣,字節碼在創建時也需要有一個具體的目標。
字節碼是一種領域特定的語言。它是定制虛擬機的定制機器語言。這就是它的魔力所在。字節碼可以是專業化的,它不是通用的,不必支持所有可能的情況。它只需支持可以編譯為字節碼的源語言所需要的功能。
不僅如此,除了僅支持少數指令之外,字節碼還包括在領域特定虛擬機上下文中才有意義的領域特定指令。例如,Java虛擬機(JVM)的字節碼包括以下指令:invokeinterface
用于調用接口方法,getstatic
用于獲取類的靜態字段,new
用于為指定的類創建對象。Ruby的字節碼有:putself
指令用于將self
壓入棧,send
用于向對象發送消息,putobject
用于將對象壓入棧。Lua的字節碼具有訪問和操作表和元組的專用指令。在x86-64的通用指令集中找不到以上任何指令。
這種通過使用自定義字節碼格式實現專業化的能力是構建虛擬機的重要原因之一。這不僅使編譯、維護和調試變得更加容易,而且所得到的代碼也更加密集,因為它表達某些內容所使用的指令更少,從而使代碼執行起來更快。
現在,如果所有關于自定義虛擬機、量身定制的機器代碼、手工構建編譯器的討論都沒能引起你的興趣,那么這是你放棄本書的最后機會。我們將正式開始。
- 少兒人工智能趣味入門:Scratch 3.0動畫與游戲編程
- Practical Data Analysis Cookbook
- 小創客玩轉圖形化編程
- Android開發精要
- 精通搜索分析
- Python忍者秘籍
- 批調度與網絡問題的組合算法
- C++程序設計
- 會當凌絕頂:Java開發修行實錄
- Wearable:Tech Projects with the Raspberry Pi Zero
- Building a Media Center with Raspberry Pi
- 從零開始學UI設計·基礎篇
- Learning iOS Penetration Testing
- Java程序性能優化實戰
- IBM DB2 9.7 Advanced Application Developer Cookbook