跳轉到內容

用 C 語言建立虛擬機器/暫存器虛擬機器

來自 Wikibooks,開放世界的開放書籍

第一個示例將是最簡單的架構之一,它將包含以下元件

  1. 一組暫存器(我將任意選擇有 4 個,編號為 0 到 3)。暫存器用作一組讀/寫記憶體單元。
  2. 一個程式,它是一個只讀的 VM 指令序列
  3. 一個執行單元來執行程式。執行單元將按順序讀取每個指令並執行它。

定址模式

[編輯 | 編輯原始碼]

在任何組合語言中,數字都用於多種用途。除了表示標量值外,它們還表示暫存器編號和記憶體位置。在本例中,我將遵循使用字首字元來表示數字所扮演的角色的約定。

  • 暫存器編號以字母r開頭,如r0r1r2
  • 立即數(標量)值以井號#開頭,如#100#200
  • 記憶體地址以符號@開頭,如@1000@1004

指令集

[編輯 | 編輯原始碼]

設計完 VM 後,需要設計 VM 的指令集。指令集僅僅反映了我們想要用這個 VM 做的事情(或者允許其他人用這個 VM 做的事情)。以下是一些用這個 VM 可以做的事情

  • 將一個立即數(常數)載入到一個暫存器中。
  • 對兩個暫存器執行算術求和(即,將兩個數字相加)。
  • 停止機器。

讓我們現在就保持這種簡單性。在獲得工作實現之後,我們可以返回並新增更多指令。

首先,讓我們使用這三個指令編寫一個簡短的程式,看看它們可能是什麼樣子

loadi r0 #100
loadi r1 #200
add r2 r0 r1
halt

如果 VM 執行該程式,以下是對每行發生的事情的描述

  1. 將立即值 100 載入到暫存器r0中。請注意,將值放入暫存器通常稱為載入操作。
  2. 將立即值 200 載入到暫存器r1中。
  3. 將暫存器r0r1的內容相加,並將結果放入暫存器r2中。
  4. 結束程式並停止 VM。

請注意,為了保持與通用約定的統一,我選擇在運算元列表中首先放置目標暫存器。

指令碼

[編輯 | 編輯原始碼]

建立組合語言後,需要確定如何將每個指令表示為一個數字。這在組合語言中的每個指令和指令碼集中的每個指令碼之間建立了一一對應關係。將程式從組合語言轉換為指令碼稱為彙編,而從指令碼轉換回組合語言稱為反彙編

此時我們必須做出幾個選擇

  • 用什麼數字表示每個組合語言指令?
  • 如何對指令運算元進行編碼?
  • 運算元是指令字的一部分(請記住,數字),還是單獨的字(數字)?

首先,為了回答最後一個問題,由於這個 VM 中的指令和暫存器數量很少,即使(為了簡單起見)我使用一個 16 位的指令字,也不難將所有運算元編碼到一個指令字中。因此,用十六進位制表示的 16 位數字有 4 位,為我們提供了 4 個資訊欄位的方便訪問,每個欄位包含 16 種變體(0-9 和 A-F)。

  1. 機器字的第一位將是指令編號。這使我們的 VM 潛在地擁有多達 16 條不同的指令。就當代標準而言,這是一個很小的數量,但對於我們的示例虛擬機器來說已經足夠了。
  2. 接下來的三位將用於運算元。它們可以用作三個 1 位運算元,兩個 1 位和 2 位運算元,或一個 3 位運算元。

做出這些決定後,讓我們現在建立編碼。回想一下,我們有 16 個可用的指令編號。

halt指令將是指令 0,選擇 0 用於該指令有一個重要的原因。由於計算機記憶體中的空閒空間很可能被 0 填充,因此任何失控程式最終都會遇到 0 並嘗試執行此指令,立即停止程式。

剩下的兩條指令可以任意分配:loadi指令可以是指令 1,add指令可以是指令 2。這是我們目前的指令編碼列表

0 = halt
1 = loadi
2 = add

檢查第一個程式指令,我們看到我們現在必須對暫存器和立即值進行編碼

loadi r0 #100

有三個十六進位制數字可用於運算元,因此我們將使用這三個數字中的第一個作為暫存器編號,並將第二個和第三個一起作為立即值。現在我們已經確定了loadi指令的完整編碼

bits 15-12 = 1
bits 11- 8 = register number
bits  7- 0 = immediate value

此指令的暫存器編號為 0,立即值為 100,十六進位制為 64(6 x 16 + 4)。因此,第一個指令的完整 16 位十六進位制指令碼為 1064。

第二個指令以類似的方式彙編。立即值為 200,十六進位制為 C8。第二個指令彙編為 11C8。

第三個指令是 2201。

最後一個指令是 0。

將指令組合在一起,我們得到這 4 個 16 位十六進位制數字作為完整的程式

1064
11C8
2201
0000

現在是時候實現這個設計了。我選擇用 C 語言編寫這個程式,原因有幾個

  • 它是一種非常簡單的語言,可以編寫簡潔的程式。
  • 它是一種具有某些高階特性的低階語言。這使得它成為建立 VM 的一個不錯的選擇。

VM 的主要功能將是一個run函式,它按順序在迴圈內執行以下步驟。迴圈重複執行,直到執行halt指令。

  1. 從程式中獲取下一條指令。
  2. 將指令解碼為其組成部分。
  3. 執行解碼後的指令。

讓我們開始編寫程式。

暫存器

[編輯 | 編輯原始碼]

要實現的第一個也是最簡單的部分是 4 個暫存器集。

# define NUM_REGS 4
int regs[ NUM_REGS ];

這個 VM 中的暫存器是有符號整數,根據您的計算機和作業系統,寬度為 16 位、32 位或 64 位。對於本示例,確切的尺寸無關緊要。

程式指令

[編輯 | 編輯原始碼]

完全彙編的程式可以輕鬆地儲存在一個整數陣列中。

int prog[] = { 0x1064, 0x11C8, 0x2201, 0x0000 };

取值函式從程式中檢索下一個字。為了做到這一點,我們必須引入一個稱為程式計數器(有時也稱為指令指標)的變數。在從函式返回指令字之前,必須遞增程式計數器,以便它引用程式陣列中的下一條指令。

int pc = 0; /* program counter */

int fetch()
{
  return prog[ pc++ ];
}

譯碼函式將完全解碼每條指令。它將確定每個指令的三個運算元暫存器和立即值,即使某個指令沒有使用該部分。實際上,這使得函式更簡單。指令編號和運算元儲存在變數中。

int instrNum = 0;
/* operands: */
int reg1     = 0;
int reg2     = 0;
int reg3     = 0;
int imm      = 0;

/* decode a word */
void decode( int instr )
{
  instrNum = (instr & 0xF000) >> 12;
  reg1     = (instr & 0xF00 ) >>  8;
  reg2     = (instr & 0xF0  ) >>  4;
  reg3     = (instr & 0xF   );
  imm      = (instr & 0xFF  );
}

執行函式實際執行指令。首先需要一個執行標誌,halt指令可以將其設定為 0。

/* the VM runs until this flag becomes 0 */
int running = 1;

現在是eval函式。它只是一個switch階梯。此函式使用譯碼函式儲存值的指令碼和運算元變數。請注意,在每個指令 case 中,我都會顯示指令。這使得在程式執行時易於跟蹤。

/* evaluate the last decoded instruction */
void eval()
{
  switch( instrNum )
  {
    case 0:
      /* halt */
      printf( "halt\n" );
      running = 0;
      break;
    case 1:
      /* loadi */
      printf( "loadi r%d #%d\n", reg1, imm );
      regs[ reg1 ] = imm;
      break;
    case 2:
      /* add */
      printf( "add r%d r%d r%d\n", reg1, reg2, reg3 );
      regs[ reg1 ] = regs[ reg2 ] + regs[ reg3 ];
      break;
  }
}

run 函式非常簡單:獲取,然後解碼,最後執行。

void run()
{
  while( running )
  {
    int instr = fetch();
    decode( instr );
    eval();
  }
}

主函式

[編輯 | 編輯原始碼]

main 函式簡單地呼叫 run 函式。

int main( int argc, const char * argv[] )
{
  run();
  return 0;
}

完整程式原始碼

[編輯 | 編輯原始碼]

為了方便起見,我已將完整的程式原始碼包含在一個單獨的程式碼塊中。請注意,我添加了一個 showRegs 函式,以便在指令之間檢視所有暫存器的值。這讓我能夠驗證 VM 和彙編程式是否正常工作。

# include <stdio.h>

# define NUM_REGS 4
unsigned regs[ NUM_REGS ];

unsigned program[] = { 0x1064, 0x11C8, 0x2201, 0x0000 };

/* program counter */
int pc = 0;

/* fetch the next word from the program */
int fetch()
{
  return program[ pc++ ];
}

/* instruction fields */
int instrNum = 0;
int reg1     = 0;
int reg2     = 0;
int reg3     = 0;
int imm      = 0;

/* decode a word */
void decode( int instr )
{
  instrNum = (instr & 0xF000) >> 12;
  reg1     = (instr & 0xF00 ) >>  8;
  reg2     = (instr & 0xF0  ) >>  4;
  reg3     = (instr & 0xF   );
  imm      = (instr & 0xFF  );
}

/* the VM runs until this flag becomes 0 */
int running = 1;

/* evaluate the last decoded instruction */
void eval()
{
  switch( instrNum )
  {
    case 0:
      /* halt */
      printf( "halt\n" );
      running = 0;
      break;
    case 1:
      /* loadi */
      printf( "loadi r%d #%d\n", reg1, imm );
      regs[ reg1 ] = imm;
      break;
    case 2:
      /* add */
      printf( "add r%d r%d r%d\n", reg1, reg2, reg3 );
      regs[ reg1 ] = regs[ reg2 ] + regs[ reg3 ];
      break;
  }
}

/* display all registers as 4-digit hexadecimal words */
void showRegs()
{
  int i;
  printf( "regs = " );
  for( i=0; i<NUM_REGS; i++ )
    printf( "%04X ", regs[ i ] );
  printf( "\n" );
}

void run()
{
  while( running )
  {
    showRegs();
    int instr = fetch();
    decode( instr );
    eval();
  }
  showRegs();
}

int main( int argc, const char * argv[] )
{
  run();
  return 0;
}

示例執行

[編輯 | 編輯原始碼]

以下是程式的輸出結果。

regs = 0000 0000 0000 0000
loadi r0 #100
regs = 0064 0000 0000 0000
loadi r1 #200
regs = 0064 00C8 0000 0000
add r2 r0 r1
regs = 0064 00C8 012C 0000
halt
regs = 0064 00C8 012C 0000
華夏公益教科書