程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 用C說話來完成一個簡略的虛擬機

用C說話來完成一個簡略的虛擬機

編輯:關於C++

用C說話來完成一個簡略的虛擬機。本站提示廣大學習愛好者:(用C說話來完成一個簡略的虛擬機)文章只能為提供參考,不一定能成為您想要的結果。以下是用C說話來完成一個簡略的虛擬機正文


 需要的預備任務及留意事項:

在開端之前須要做以下任務:

  •     一個C編譯器——我應用了 clang 3.4,也能夠用其它支撐 c99/c11 的編譯器;
  •     文本編纂器——我建議應用基於IDE的文本編纂器,我應用 Emacs;
  •     基本編程常識——最根本的變量,流程掌握,函數,數據構造等;
  •     Make 劇本——能使法式更快一點。

為何要寫個虛擬機?

有以下緣由:

  •     想深刻懂得盤算機任務道理。本文將贊助你懂得盤算機底層若何任務,虛擬機供給簡練的籠統層,這不就是一個最好的進修它們道理的辦法嗎?
  •     更深刻懂得一些編程說話是若何任務。例如,當下多種常常應用那些說話的虛擬機。包含JVM,Lua VM,FaceBook 的 Hip—Hop VM(PHP/Hack) 等。
  •     只是由於有興致進修虛擬機。

指令集

我們將要完成一種異常簡略的自界說的指令集。我不會講一些高等的如位移存放器等,願望在讀過這篇文章後控制這些。

我們的虛擬機具有一組存放器,A,B,C,D,E, 和F。這些是通用存放器,也就是說,它們可以用於存儲任何器械。一個法式將會是一個只讀指令序列。這個虛擬機是一個基於客棧的虛擬機,也就是說它有一個可讓我們壓入和彈出值的客棧,同時還有大批可用的存放器。這要比完成一個基於存放器的虛擬機簡略的多。

言歸正傳,上面是我們將要完成的指令集:
 

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

這就是我們的指令集,留意,POP 指令將會打印我們彈出的指令,如許我們就可以夠看到 ADD 指令任務了。我還參加了一個 SET 指令,重要是讓你懂得存放器是可以拜訪和寫入的。你也能夠本身完成像MOV A B(將A的值挪動到B)如許的指令。HTL 指令是為了告知我們法式曾經運轉停止。

虛擬機是若何任務的呢?

如今我們曾經到了本文最症結的部門,虛擬機比你想象的簡略,它們遵守一個簡略的形式:讀取;解碼;履行。起首,我們從指令聚集或代碼中讀取下一條指令,然後將指令解碼並履行解碼後的指令。為簡略起見,我們疏忽了虛擬機的編碼部門,典范的虛擬機將會把一個指令(操作碼和它的操作數)打包成一個數字,然後再解碼這個指令。
項目構造

開端編程之前,我們須要設置好我們的項目。第一,你須要一個C編譯器(我應用 clang 3.4)。還須要一個文件夾來放置我們的項目,我愛好將我的項目放置於~/Dev:
 

$cd ~/Dev/
mkdir mac
cd mac
mkdir src

如上,我們先 cd 進入~/Dev 目次,或許任何你想放置的地位,然後新建一個目次(我稱這個虛擬機為"mac")。然後再 cd 進這個目次並新建我們 src 目次,這個目次用於放置代碼。

Makefile

makefile 絕對直接,我們不須要將甚麼器械分紅多個文件,也不消包括任何器械,所以我們只須要用一些標記來編譯文件:
 

SRC_FILES = main.c
CC_FLAGS = -Wall -Wextra -g -std=c11
CC = clang
 
all:
  ${CC} ${SRC_FILES} ${CC_FLAGS} -o mac

這對今朝來講曾經足夠了,你今後還可以改良它,然則只需它能完成這個任務,我們應當知足了。
指令編程(代碼)

如今開端寫虛擬機的代碼了。第一,我們須要界說法式的指令。為此,我們可使用一個列舉類型enum,由於我們的指令根本上是從0到X的數字。現實上,可以說你是在組裝一個匯編文件,它會應用像 mov 如許的詞,然後翻譯成聲明的指令。
我們可以只寫一個指令文件,例如 PSH, 5 是0, 5,然則如許其實不易讀,所以我們應用列舉器!
 

typedef enum {
  PSH,
  ADD,
  POP,
  SET,
  HLT
} InstructionSet;

如今我們可以將一個測試法式存儲為一個數組。我們寫一個簡略的法式用於測試:將5和6相加,然後將他們打印出來(用POP指令)。假如你情願,你可以界說一個指令將棧頂的值打印出來。

指令應當存儲成一個數組,我將在文檔的頂部界說它;但你也許會將它放在一個頭文件中,上面是我們的測試法式:
 

const int program[] = {
  PSH, 5,
  PSH, 6,
  ADD,
  POP,
  HLT
};

下面的法式將會把5和6壓入棧,挪用 ADD 指令,這將會把棧頂的兩個值彈出,相加後將成果壓回棧中,接上去我們彈出成果,由於 POP 指令將會打印這個值,然則你不用本身再做了,我曾經做好並測試過了。最初,HLT 指令停止法式。

很好,如許我們有了本身的法式。如今我們完成了虛擬機的讀取,解碼,求值的形式。然則要記住,我們沒有解碼任何器械,由於我們給出的是原始指令。也就是說我們只須要存眷讀取和求值!我們可以將它們簡化成兩個函數 fetch 和 evaluate。

獲得以後指令

由於我們曾經將我們的法式存成了一個數組,所以很簡略的便可以獲得以後指令。一個虛擬機有一個計數器,普通來講叫做法式計數器,指令指針等等,這些名字是一個意思取決於你的小我愛好。在虛擬機的代碼庫裡,IP 或 PC 如許的簡寫情勢也到處可見。

假如你之前有記得,我說過我們要把法式計數器以存放器的情勢存儲...我們將那末做——在今後。如今,我們只是在我們代碼的最頂端創立一個叫 ip 的變量,而且設置為 0。
 

int ip = 0;

ip 變量代表指令指針。由於我們曾經將法式存成了一個數組,所以應用 ip 變量去指明法式數組中以後索引。例如,假如創立了一個被賦值了法式 ip 索引的變量 x,它將存儲我們法式的第一條指令。

[假定ip為0]
 

int ip = 0;
 
int main() {
  int instr = program[ip];
  return 0;

假如我們打印變量 instr,原來應是 PSH 的它將顯示為0,由於在他是我們列舉裡的第一個值。我們也能夠寫一個取回函數像如許:
 

int fetch() {
  return program[ip];
}

這個函數將會前往以後被挪用指令。太棒了,那末假如我們想要下一條指令呢?很輕易,我們只需增長指令指針就行了:
 

int main() {
  int x = fetch(); // PSH
  ip++; // increment instruction pointer
  int y = fetch(); // 5
}

那末如何讓它本身動起來呢?我們曉得一個法式直到它履行 HLT 指令才會停滯。是以我們應用一個無窮的輪回連續直到以後指令為HLT。
 

// INCLUDE <stdbool.h>!
bool running = true;
 
int main() {
  while (running) {
    int x = fetch();
    if (x == HLT) running = false;
    ip++;
  }
}

這任務的很好,然則有點紛亂。我們正在輪回每條指令,檢討能否 HLT,假如是就停滯輪回,不然“吃失落”指令接著輪回。

斷定一條指令

是以這就是我們虛擬機的主體,但是我們想要確切的評判每條指令,而且使它更簡練一些。好的,這個簡略的虛擬機,你可以寫一個“偉大”的 switch 聲明。讓 switch 中的每個 case 對應一條我們界說在列舉中的指令。這個 eval 函數將應用一個簡略的指令的參數來斷定。我們在函數中不會應用任何指令指針遞增除非我們想操作數糟蹋操作數。
 

void eval(int instr) {
  switch (instr) {
    case HLT:
      running = false;
      break;
  }
}

是以假如我們在回到主函數,便可以像如許應用我們的 eval 函數任務:
 

bool running = true;
int ip = 0;
 
// instruction enum here
 
// eval function here
 
// fetch function here
 
int main() {
  while (running) {
    eval(fetch());
    ip++; // increment the ip every iteration
  }
}

棧!

很好,那會很完善的完成這個任務。如今,在我們參加其他指令之前,我們須要一個棧。榮幸的是,棧是很輕易完成的,我們僅僅須要應用一個數組罷了。數組會被設置為適合的年夜小,如許它就可以包括256個值了。我們也須要一個棧指針(常被縮寫為sp)。這個指針會指向棧數組。

為了讓我們對它有一個加倍抽象化的印象,讓我們來看看這個用數組完成的棧吧:
 

[] // empty
 
PSH 5 // put 5 on **top** of the stack
[5]
 
PSH 6
[5, 6]
 
POP
[5]
 
POP
[] // empty
 
PSH 6
[6]
 
PSH 5
[6, 5]

那末,在我們的法式裡產生了甚麼呢?
 

PSH, 5,
PSH, 6,
ADD,
POP,
HLT

我們起首把5壓入了棧

 [5]

然後壓入6:
 

[5, 6]

接著添加指令,掏出這些值,把它們加在一路並把成果壓入棧中:
 

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

那末我們的棧指針在哪起感化呢?棧指針(或許說sp)普通是被設置為-1,這意味著這個指針是空的。請記住一個數組是從0開端的,假如沒有初始化sp的值,那末他會被設置為C編譯器放在那的一個隨機值。

假如我們將3個值壓棧,那末sp將釀成2。所以這個數組保留了三個值:
 
sp指向這裡(sp = 2)
       |
       V
[1, 5, 9]
 0  1  2 <- 數組下標

如今我們從棧上出棧一次,我們僅須要減小棧頂指針。好比我們接上去把9出棧,那末棧頂將變成5:
 
sp指向這裡(sp = 1)
    |
    V
[1, 5]
 0  1 <- 數組下標

所以,當我們想曉得棧頂內容的時刻,只須要檢查sp確當前值。OK,你能夠想曉得棧是若何任務的,如今我們用C說話完成它。很簡略,和ip一樣,我們也應當界說一個sp變量,記得把它賦為-1!再界說一個名為stack的數組,代碼以下:
 

int ip = 0;
int sp = -1;
int stack[256]; // 用數組或合適此處的其它構造
 
// 其它C代碼

如今假如我們想入棧一個值,我們先增長棧頂指針,接著設置以後sp處的值(我們方才增長的)。留意:這兩步的次序很主要!
 

// 壓棧5
 
// sp = -1
sp++; // sp = 0
stack[sp] = 5; // 棧頂如今變成5

所以,在我們的履行函數eval()裡,可以像如許完成push出棧指令:
 

void eval(int instr) {
  switch (instr) {
    case HLT: {
      running = false;
      break;
    }
    case PSH: {
      sp++;
      stack[sp] = program[++ip];
      break;
    }
  }
}

如今你看到,它和我們之前完成的eval()函數有一些分歧。起首,我們把每一個case語句塊放到年夜括號裡。你能夠不太懂得這類用法,它可讓你在每條case的感化域裡界說變量。固然如今不須要界說變量,但未來會用到。而且它可以很輕易得讓一切的case語句塊堅持分歧的作風。

其次是奇異的表達式program[++ip]。它做了甚麼?呃,我們的法式存儲在一個數組裡,PSH指令須要取得一個操作數。操作數實質是一個參數,就像當你挪用一個函數時,你可以給它傳遞一個參數。這類情形我們稱作壓棧數值5。我們可以經由過程增長指令指針(譯者注:普通也叫做法式計數器)ip來獲得操作數。當ip為0時,這意味著履行到了PSH指令,接上去我們願望獲得下一條指令——即壓棧的數值。這可以經由過程ip自增的辦法完成(留意:增長ip的地位非常主要,我們願望在獲得指令前自增,不然我們只是拿到了PSH指令),接上去須要跳到下一條指令不然會激發奇異的毛病。固然我們也能夠把sp++簡化到stack[++sp]裡。

關於POP指令,完成異常簡略。只須要減小棧頂指針,然則我普通願望可以或許在出棧的時刻打印出棧值。


我省略了完成其它指令的代碼和swtich語句,僅列出POP指令的完成:

 

// 記得#include <stdio.h>!
 
case POP: {
  int val_popped = stack[sp--];
  printf("popped %d\n", val_popped);
  break;
}

如今,POP指令可以或許任務了!我們方才做的只是把棧頂放到變量val_popped裡,接著棧頂指針減一。假如我們起首棧頂減一,那末將獲得一些有效值,由於sp能夠取值為0,那末我們能夠把stack[-1]賦給val_popped,平日這不是一個好主張。

最初是ADD指令。這條指令能夠要消費你一些腦細胞,同時這也是我們須要用年夜括號{}完成case語句內感化域的緣由。
 

case ADD: {
  // 起首我們出棧,把數值存入變量a
  int a = stack[sp--];
 
  // 接著我們出棧,把數值存入變量b
 
  // 接著兩個變量相加,再把成果入棧
  int result = a + b;
  sp++; // 棧頂加1 **放在賦值之前**
  stack[sp] = result; // 設置棧頂值
 
  // 完成!
  break;
}


存放器

存放器是虛擬機中的選配件,很輕易完成。之條件到過我們能夠須要六個存放器:A,B,C,D,E和F。和完成指令集一樣,我們也用一個列舉來完成它們。
 

typedef enum {
  A, B, C, D, E, F,
  NUM_OF_REGISTERS
} Registers;

小技能:列舉的最初放置了一個數 NUM_OF_REGISTERS。經由過程這個數可以獲得存放器的個數,即使你又添加了其它的存放器。如今我們須要一個數組為存放器寄存數值:

 

int registers[NUM_OF_REGISTERS];

接上去你可以讀取存放器內的值:
 

printf("%d\n", registers[A]); // 打印存放器A的值

修訂

我沒有在存放器花太多心思,但你應當可以或許寫出一些操作存放器的指令。好比,假如你想完成任何分支跳轉,可以經由過程把指令指針(譯者注:或叫法式計數器)和/或棧頂指針存到存放器裡,或許經由過程完成分支指令。

前者完成起來絕對快捷、簡略。我們可以如許做,增長代表IP和SP的存放器:
 

typedef enum {
  A, B, C, D, E, F, PC, SP,
  NUM_OF_REGISTERS
} Registers;

如今我們須要完成代碼來應用指令指針和棧頂指針。一個簡略的方法——刪失落下面界說的sp和ip變量,用宏界說完成它們:

 

#define sp (registers[SP])
#define ip (registers[IP])  

譯者注:此處應同Registers列舉中堅持分歧,IP應改成PC

這個修正適可而止,你不須要重寫許多代碼,同時它任務的很好。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved