日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
如何用C語言實現(xiàn)一個虛擬機?

如何用c語言實現(xiàn)一個虛擬機?

作者:佚名 2018-06-22 10:30:56
云計算
虛擬化 由于我喜歡在較低級別(Low-level)的應用中(編譯器,解釋器,解析器,虛擬機等等)工作,所以我覺得寫一篇關于用C編程語言構(gòu)建虛擬機的文章,是非常有必要的。我認為這篇文章除了能夠讓你了解到虛擬機的工作原理外,還可以讓你了解到較低級別的編程過程。

創(chuàng)新互聯(lián)是一家企業(yè)級云計算解決方案提供商,超15年IDC數(shù)據(jù)中心運營經(jīng)驗。主營GPU顯卡服務器,站群服務器,遂寧托管服務器,海外高防服務器,服務器機柜,動態(tài)撥號VPS,海外云手機,海外云服務器,海外服務器租用托管等。

由于我喜歡在較低級別(Low-level)的應用中(編譯器,解釋器,解析器,虛擬機等等)工作,所以我覺得寫一篇關于用C編程語言構(gòu)建虛擬機的文章,是非常有必要的。我認為這篇文章除了能夠讓你了解到虛擬機的工作原理外,還可以讓你了解到較低級別的編程過程。

準備內(nèi)容

  • 使用的編譯器類型:我正在使用的是clang,它是輕量級編譯器,但你可以使用任何現(xiàn)代編譯器;
  • 文本編輯器:我建議當編寫C語言時,通過IDE編輯文本編輯器,我將使用Emacs;
  • 基本的編程知識:比如什么是變量,流量控制,函數(shù),結(jié)構(gòu)等;
  • GNU Make:GNU Make主要用于自動化構(gòu)建可執(zhí)行程序(庫文件),這樣我們就不需要在終端中一遍又一遍地編寫相同的命令來編譯代碼。Make的功能包括:自動化構(gòu)建和安裝;增量編譯及自動更新;適用于多語言,比如c/c++、java、php等;支持自定義功能擴展(只要有意義,都是可以放到makefile中)。

為什么你應該寫一個虛擬機?

以下是你應該編寫虛擬機的一些原因:

1.你需要更深入地了解計算機的工作方式,本文將幫助你了解你的計算機在較低級別的環(huán)境中是如何運行工作的?而虛擬機則提供了一個非常簡單的抽象層;

2.順便了解一些虛擬機的知識;

3.深入了解一下編程語言的工作原理,現(xiàn)在的各種語言都針對虛擬機,比如JVM,Lua VM,F(xiàn)aceBook 的 Hip—Hop VM(PHP/Hack)等。

指令集

指令集會相對簡單,我將簡要介紹一下,例如如何從寄存器中移動值或跳轉(zhuǎn)到其他指令。

假設我們的虛擬機有一組寄存器:A,B,C,D,E和F,且這些都是通用寄存器,這意味著它們可以用于存儲任何東西。這與專用寄存器不同,例如在x86上,ip, flag, ds, …程序是只讀指令集。如果虛擬機是一個基于棧的虛擬機,這意味著它有一個我們可以壓棧和彈出值的棧,另外,該虛擬機還有一些我們也可以使用的寄存器?;跅5奶摂M機比基于寄存器的虛擬機實現(xiàn)起來要簡單得多。

下面是我將要實施的一個指令集的示例:

  
 
 
  1. PSH 5       ; pushes 5 to the stack 
  2. PSH 10      ; pushes 10 to the stack 
  3. ADD         ; pops two values on top of the stack, adds them pushes to stack 
  4. POP         ; pops the value on the stack, will also print it for debugging 
  5. SET A 0     ; sets register A to 0 
  6. HLT         ; stop the program 

以上就是我的指令集,請注意,POP指令將打印我們彈出的指令,其中很多是調(diào)試用的。ADD會將結(jié)果壓棧到棧,所以我們可以從棧中的彈出值來驗證它是否存在。我還在其中包含了一條SET指令,這樣你就可以了解如何訪問和寫入寄存器了。你也可以嘗試執(zhí)行像MOV A,B(將值A移至B)的指令, HLT是顯示我已經(jīng)完成程序執(zhí)行的指令。

虛擬機如何工作?

其實虛擬機比你想象的要簡單,它的工作模式遵循一個簡單的規(guī)律,即“指令周期(instruction cycle)”,整個過程包括讀取、解碼、執(zhí)行三大塊。首先,你要讀取指令集,然后才能解碼指令并執(zhí)行解碼后的指令。

項目結(jié)構(gòu)

在我開始編程之前,需要做一些準備工作。我需要一個文件夾來放置項目,我喜歡將項目放置于~/Dev下。另外,我需要一個C編譯器(我使用的是 clang 3.4)。以下是我在終端中設置我的項目的過程,假設你已經(jīng)擁有一個?/ dev /目錄,不過你可以把它放到任何你想要的位置。

  
 
 
  1. $ cd ~/dev/ 
  2. $ mkdir mac 
  3. $ cd mac 
  4. $ mkdir src 

上面就是我把cd放到我的~/dev目錄的過程,首先,我會創(chuàng)建一個目錄(我稱之為VM“mac”)。然后,我進入該目錄并創(chuàng)建我的src目錄,這個目錄被用于存放代碼。

Makefile文件

我的makefile相對比較簡單,由于該文件不需要將任何東西分隔成多個文件,所以其中也不會包含任何東西,我只需要用一些標志來編譯文件即可。

  
 
 
  1. SRC_FILES = main.c 
  2. CC_FLAGS = -Wall -Wextra -g -std=c11 
  3. CC = clang 
  4.  
  5. all: 
  6.     ${CC} ${SRC_FILES} ${CC_FLAGS} -o mac 

現(xiàn)在這應該足夠了,你以后可以隨時改進它,但只要它能完成這項工作,我們應該沒問題。

指令編程

現(xiàn)在就可以開始編寫虛擬機的代碼了。首先,為了解釋指令編程,我必須用到一個枚舉,因為我們的指令基本上是從0到X的數(shù)字。事實上,匯編程序?qū)@取你的匯編文件,并將所有操作轉(zhuǎn)換為對應的數(shù)字。例如,如果你為mac編寫一個匯編程序,它將把所有MOV操作轉(zhuǎn)換為數(shù)字0。

  
 
 
  1. typedef enum { 
  2.     PSH, 
  3.     ADD, 
  4.     POP, 
  5.     SET, 
  6.     HLT 
  7. } InstructionSet; 

現(xiàn)在我可以將測試程序存儲為一個數(shù)組,然后寫一個簡單的程序用于測試,比如將5和6相加,然后將它們用POP指令打印出來。如果你愿意,你可以定義一個指令將棧頂?shù)闹荡蛴〕鰜怼?/p>

指令應該存儲成一個數(shù)組,我將在文檔的頂部定義它。但你可以把它放在一個頭文件中,以下是我的測試程序。

  
 
 
  1. const int program[] = { 
  2.     PSH, 5, 
  3.     PSH, 6, 
  4.     ADD, 
  5.     POP, 
  6.     HLT 
  7. }; 

上面的程序會將5和6壓棧入棧,執(zhí)行add指令,該指令將彈出棧中的兩個值,將它們加在一起并將結(jié)果壓棧回棧。然后會彈出結(jié)果,出于調(diào)試目的,我的彈出指令將打印這兩個值。

***,HLT指令意味著終止程序。如果我們要控制流程,可以隨時終止程序。不過,如果我們沒有任何指示,我們的虛擬機***也將自然終止。

現(xiàn)在我實現(xiàn)了虛擬機的讀取、解碼、執(zhí)行的過程。但是要記住,我沒有解碼任何東西,因為我給出的是原始指令。

獲取當前指令

因為我已將程序存儲為一個數(shù)組,所以獲取當前指令很簡單。虛擬機有一個計數(shù)器,通常稱為程序計數(shù)器,不過有時也叫做指令指針等,通常它們分別縮寫為PC或IP。

現(xiàn)在,我只需在代碼頂部創(chuàng)建一個名為ip的變量,并將其設置為0。

  
 
 
  1. int ip = 0; 

這個ip代表指令指針,由于我已經(jīng)將程序本身存儲為一個整數(shù)數(shù)組,固ip變量作為數(shù)組中的索引,表示當前正在執(zhí)行的指令。

  
 
 
  1. int ip = 0; 
  2.  
  3. int main() { 
  4.     int instr = program[ip]; 
  5.     return 0; 

如果打印變量instr,則PSH將顯示為0,因為變量instr是我們枚舉里的***個值。不過,我們也可以寫一個取回函數(shù),如下所示:

  
 
 
  1. int fetch() { 
  2.     return program[ip]; 

該函數(shù)在被調(diào)用時將返回當前指令。那么,如果我們想要下一條指令呢?我們只要增加指令指針即可。

  
 
 
  1. int main() { 
  2.     int x = fetch(); // PSH 
  3.     ip++; // increment instruction pointer 
  4.     int y = fetch(); // 5 

那么我們該如何實現(xiàn)自動化呢?我們知道程序只有執(zhí)行通過HLT指令時,才會停止,所以該程序本身就是一個***循環(huán)。

  
 
 
  1. #include   
  2.  
  3. bool running = true; 
  4.  
  5. int main() { 
  6.     while (running) { 
  7.        int x = fetch(); 
  8.        if (x == HLT) running = false; 
  9.        ip++; 
  10.     } 

我目前要做的是循環(huán)遍歷每條指令,檢查指令的值是否為HLT,如果是,則停止循環(huán),否則就不斷重復。

一條指令的評估

這是虛擬機的運行要點,其實虛擬機非常簡單,你可以編寫一個巨大的switch語句。而這么做就是為了加快運行速度,與之相對的是,對于所有指令和使用execute方法的某個抽象類或接口,都要使用HashMap。

switch語句中的每個case都是我們在枚舉中定義的指令,這個eval函數(shù)將使用一個簡單的指令參數(shù)來評估指令。除非你正在使用操作數(shù),否則不要在此函數(shù)中執(zhí)行任何指令指針增量。

  
 
 
  1. void eval(int instr) { 
  2.     switch (instr) { 
  3.     case HLT: 
  4.         running = false; 
  5.         break; 
  6.     } 

我會將此函數(shù)添加回虛擬機的主循環(huán)中:

  
 
 
  1. bool running = true; 
  2. int ip = 0; 
  3.  
  4. // instruction enum 
  5. // eval function 
  6. // fetch function 
  7.  
  8. int main() { 
  9.     while (running) { 
  10.         eval(fetch()); 
  11.         ip++; // increment the ip every iteration 
  12.     } 

不過在添加其他指令之前,我們需要一個棧。棧是一個非常簡單的數(shù)據(jù)結(jié)構(gòu)。我們將使用這個數(shù)組而不是一個鏈表。因為我的棧是固定大小的,所以不必擔心大小的調(diào)整。使用數(shù)組而不是鏈接列表,在緩存效率方面會更占優(yōu)勢。

與我們?nèi)绾螕碛幸粋€用于索引程序數(shù)組的ip類似,現(xiàn)在我們需要一個棧指針(sp)來顯示我們在棧數(shù)組中的位置。

下面是我的一個棧的數(shù)據(jù)結(jié)構(gòu)的詳細列表:

  
 
 
  1. [] // empty 
  2.  
  3. PSH 5 // put 5 on **top** of the stack 
  4. [5] 
  5.  
  6. PSH 6 // 6 on top of the stack 
  7. [5, 6] 
  8.  
  9. POP // pop the 6 off the top 
  10. [5] 
  11.  
  12. POP // pop the 5 
  13. [] // empty 
  14.  
  15. PSH 6 // push a 6... 
  16. [6] 
  17.  
  18. PSH 5 // etc.. 
  19. [6, 5] 

讓我們按照棧來分解我們的程序::

  
 
 
  1. PSH, 5, 
  2. PSH, 6, 
  3. ADD, 
  4. POP, 
  5. HLT 

首先我把5壓棧到棧上:

  
 
 
  1. [5] 

然后把6壓棧到棧上:

  
 
 
  1. [5, 6] 

然后add指令將彈出這些值并將它們加在一起,***將結(jié)果壓棧到棧上。

  
 
 
  1. [5, 6] 
  2.  
  3. // pop the top value, store it in a variable called a 
  4. a = pop; // a contains 6 
  5. [5] // stack contents 
  6.  
  7. // pop the top value, store it in a variable called b 
  8. b = pop; // b contains 5 
  9. [] // stack contents 
  10.  
  11. // now we add b and a. Note we do it backwards, in addition 
  12. // this doesn't matter, but in other potential instructions 
  13. // for instance divide 5 / 6 is not the same as 6 / 5 
  14. result = b + a; 
  15. push result // push the result to the stack 
  16. [11] // stack contents 

你看出來了嗎,我的棧指針在哪里起了作用?棧指針被設置為-1,這意味著它是空的。數(shù)組在C中為零索引,所以如果sp為0,它將被設置為C編譯器在其中引發(fā)的隨機數(shù),因為數(shù)組的內(nèi)存未被清零。

如果現(xiàn)在我壓棧3個值,則sp將為2.因此,這里是一個包含3個值的數(shù)組:

  
 
 
  1.       -> sp -1 
  2.    psh -> sp 0 
  3.    psh -> sp 1 
  4.    psh -> sp 3 
  5.  
  6.  sp points here (sp = 2) 
  7.       | 
  8.       V 
  9. 1, 5, 9] 
  10. 0  1  2 <- array indices or "addresses" 

現(xiàn)在我從棧上出棧一次,就需要減小棧頂指針。比如我接下來要把9出棧,那么棧頂將變?yōu)?。

  
 
 
  1. sp points here (sp = 1) 
  2.         | 
  3.         V 
  4.     [1, 5] 
  5.      0  1 <- these are the array indices 

所以,當我想知道棧頂內(nèi)容的時候,只需要查看sp的當前值,希望你現(xiàn)在應該知道棧是如何工作的。

現(xiàn)在我們用C語言實現(xiàn)它,用C語言實現(xiàn)一個棧是很簡單的,和ip一樣,我們也應該定義sp變量和數(shù)組,這個數(shù)組就是棧。

  
 
 
  1. int ip = 0; 
  2. int sp = -1; 
  3. int stack[256]; 
  4.  
  5. ... 

現(xiàn)在,如果我們想將壓棧一個值,就要增加棧指針,然后在當前sp中設置該值。

這個命令的順序是非常重要的,如果你先設置值,再增加sp,那你會得到一些不好的行為,因為我們在索引-1處寫入了內(nèi)存。

  
 
 
  1. // sp = -1 
  2. sp++; // sp = 0 
  3. stack[sp] = 5; // set value at stack[0] -> 5 
  4. // top of stack is now [5] 

在我們的eval函數(shù)中,可以像以下這樣進行壓棧。

  
 
 
  1. void eval(int instr) { 
  2.     switch (instr) { 
  3.         case HLT: { 
  4.             running = false; 
  5.             break; 
  6.         } 
  7.         case PSH: { 
  8.             sp++; 
  9.             stack[sp] = program[++ip]; 
  10.             break; 
  11.         } 
  12.     } 

可以很明顯看到,這與以前的eval函數(shù)有一些區(qū)別。首先,我們把每個case語句塊放到大括號里。你可能不理解這樣做的用意,它可以讓你在每條case的作用域里定義變量。雖然現(xiàn)在不需要定義變量,但將來會用到,并且這樣做可以很容易得讓所有的case語句塊保持一致的風格。

其次,是program[++ip] 表達式,該表達式是負責PSH指令所需的操作數(shù)。因為我們的程序存儲在一個數(shù)組里,PSH指令需要獲得一個操作數(shù)。操作數(shù)本質(zhì)是一個參數(shù),就像當你調(diào)用一個函數(shù)時,你可以給它傳遞一個參數(shù)。這種情況我們稱作壓棧數(shù)值5(PSH, 5)。我們可以通過增加指令指針ip來獲取操作數(shù)。當ip為0時,這意味著執(zhí)行到了PSH指令,接下來我們希望取得壓棧的數(shù)值。這可以通過ip自增的方法實現(xiàn),實現(xiàn)后,就需要跳到下一條指令否則會引發(fā)奇怪的錯誤。當然我們也可以把sp++簡化到stack[++sp]里。

  
 
 
  1. program = [ PSH, 5, PSH, 6, ] 
  2.             0    1  2    3 
  3.  
  4. when pushing: 
  5. ip starts at 0 (PSH) 
  6. ip++, so ip is now 1 (5) 
  7. sp++, allocate some space on the stack 
  8. stack[sp] = program[ip], put the value 5 on the stack 

POP指令非常簡單,只需對堆棧指針進行遞減操作即可。但是,如果你想讓pop指令打印剛剛彈出的值即出棧值,還要做很多工作。

  
 
 
  1. case POP: { 
  2.     // store the value at the stack in val_popped THEN decrement the stack ptr 
  3.     int val_popped = stack[sp--]; 
  4.  
  5.     // print it out! 
  6.     printf("popped %d\n", val_popped); 
  7.     break; 

***是添加ADD指令,ADD指令,是一種計算機指令,含義為兩數(shù)相加(不帶進位)。這可能會讓你覺得有點棘手,而這正是case語句塊放到大括號里的技巧所在,因為我們現(xiàn)在要引入了一些變量了。

  
 
 
  1. case ADD: { 
  2.     // first we pop the stack and store it as 'a' 
  3.     int a = stack[sp--]; 
  4.  
  5.     // then we pop the top of the stack and store it as 'b' 
  6.     int b = stack[sp--]; 
  7.  
  8.     // we then add the result and push it to the stack 
  9.     int result = b + a; 
  10.     sp++; // increment stack pointer **before** 
  11.     stack[sp] = result; // set the value to the top of the stack 
  12.  
  13.     // all done! 
  14.     break; 

在具體操作之前,請注意,這里的某些操作的順序很重要!

  
 
 
  1. 5 / 4 != 4 / 5 

棧是LIFO,全稱First in, First out,先進先出。也就是說,如果先進棧5再進棧4,就會先出棧4,然后出棧5。如果我們確實執(zhí)行了pop() / pop(),那么這將會給我們錯誤的表達式,因此,確保順序正確是至關重要的。

寄存器

寄存器是虛擬機中的選配件,很容易實現(xiàn)。之前提到過我們可能需要六個寄存器:A,B,C,D,E和F。和實現(xiàn)指令集一樣,我們也用一個枚舉來實現(xiàn)它們。

  
 
 
  1. typedef enum { 
  2.    A, B, C, D, E, F, 
  3.    NUM_OF_REGISTERS 
  4. } Registers; 

不過這里有一個小技巧,枚舉的***會出現(xiàn)NUM_OF_REGISTERS。通過這個函數(shù)可以獲取寄存器的大小,即便你又添加了其它的寄存器,也可以獲得他們的大小。

我會將我的寄存器存儲在一個數(shù)組中,這是因為我要使用枚舉,A = 0,B = 1,C = 2等。所以當我想設置寄存器A時,就像說register [A] = some_value一樣簡單。

  
 
 
  1. int registers[NUM_OF_REGISTERS]; 

打印寄存器A中的值:

  
 
 
  1. printf("%d\n", registers[A]); // prints the value at the register A 

指令指針

記住有一條分支指令的指針是要指向當前指令的,由于現(xiàn)在是虛擬機源代碼,所以***的辦法是將指令指針作為一個寄存器,這樣你就可以從虛擬機程序中進行讀取和各種操作。

  
 
 
  1. typedef enum { 
  2.     A, B, C, D, E, F, PC, SP, 
  3.     NUM_OF_REGISTERS 
  4. } Registers; 

現(xiàn)在我需要移植代碼來實際使用這些指令和堆棧指針,最快的方法是刪除棧頂?shù)膕p和ip變量,并用以下的定義替換它們。

  
 
 
  1. #define sp (registers[SP]) 
  2. #define ip (registers[IP]) 

這樣你就不必重寫很多代碼了,就能***地運行了。不過缺點是,不能很好地擴展,并且它會混淆一些代碼,所以我建議不要使用這種方法,但對于一個簡單的虛擬機來說,用一下也無妨。

當涉及到分支代碼時,我會給你一個提示。有了新的IP寄存器后,我們可以通過向這個IP寫入不同的值來進行分支。試試下面這個例子,看看它能做什么。

  
 
 
  1. PSH 10 
  2. SET IP 0 

這類似于很多人都熟悉的基本程序:

  
 
 
  1. 10 PRINT "Hello, World" 
  2. 20 GOTO 10 

但是,由于我們不斷地進行進棧,所以一旦進棧的量超過空間量,就將發(fā)生棧溢出。

請注意,每個 ‘word’是一個指令,所以程序以下所示。

  
 
 
  1.               ;  these are the instructions 
  2. PSH 10        ;  0 1 
  3. PSH 20        ;  2 3 
  4. SET IP 0      ;  4 5 6 

如果我們想跳到第二組指令,我們將IP寄存器設置為2而不是0。

總結(jié)

閱讀完本文后,如果你在項目根目錄中運行make,則可以執(zhí)行虛擬機:./mac。

你可以在這里查看github上的源代碼,如果你想用MOV和SET指令來看虛擬機的更新版本,那么檢查一下mac-improved目錄,我們在本文中實現(xiàn)的虛擬機的源代碼位于mac.c中


文章標題:如何用C語言實現(xiàn)一個虛擬機?
當前地址:http://www.5511xx.com/article/djpgceh.html