reference: https://felixangell.com/blog/implementing-a-virtual-machine-in-c/
介紹
這裏寫篇文章介紹一下用C語言實現虛擬機。我喜歡從事底層程序的工作, 比如編譯器、解釋器、解析器和虛擬機等。所以我寫這篇文章來學習一下虛擬機是如何工作的,以此來帶領自己進入底層編程領域。
前提
繼續這篇文章之前你需要有:
- GCC/Clang/… — 我用的是 clang,但是你可以使用任何一種編譯器。
- Text Editor — 我不建議使用ide (當寫 C 的時候), 我用的是 Emacs。
- 基本的編程知識 — 僅僅基礎就行: 變量, 流程控制, 方法, 結構體, 等等。
- GNU Make — 構建系統,有了構建系統,就不用一遍一遍地寫同樣等編譯命令了。
爲什麼要寫一個虛擬機
- 深入理解計算機如何工作。這篇文章帶你通過底層理解計算機。虛擬機對底層抽象出了一個簡單層。沒有比親手做一個更好的學習方法了。
- 學習虛擬機很有趣。
- 可以理解很多語言是如何工作的。很多編程語言都是運行在虛擬機上的,比如java的jvm, lua 的 vm。
指令集
我們先實現一個很簡單的指令集,比如從一個寄存器上取值或者跳轉到另外一個指令。
我們的虛擬機有A, B, C, D, E, 和 F 這幾個寄存器。這些寄存器是通用寄存器,可以存儲任何東西。x86機器上還有一些特殊通途的寄存器,比如 ip 寄存器,ds 寄存器。
一個程序其實質就是一連串的指令。虛擬機是個基於棧的機器,這樣我們就可以在虛擬機裏做入棧出棧操作,也可以使用一些寄存器。相對基於寄存器的虛擬機,基於棧的虛擬機要容易實現的多。
下面我們將要實現的指令:
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
虛擬機如何工作?
虛擬機比你想象的要簡單的多。它遵循一個簡單的模式;“指令循環”,其實就是獲取,解碼,執行。
工程目錄
開始編程之前,先設置好目錄結構。假設虛擬機的名字是 mac,那麼建一個 mac 文件夾,在這個目錄下建一個 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
來定義我們的程序指令:
typedef enum {
PSH,
ADD,
POP,
SET,
HLT
} InstructionSet;
下面寫一個測試程序,輸出 5 + 6
const int program[] = {
PSH, 5,
PSH, 6,
ADD,
POP,
HLT
};
接下來要做的就是所謂的“指令循環”。
獲取指令
和x86機器一樣,虛擬機裏有一個 “Program Counter”,又或稱 “Instruction Pointer”,就是ip寄存器。
先設置
int ip = 0;
獲取一條指令:
int fetch() {
return program[ip];
}
連續獲取:
int ip = 0;
int main() {
int x = fetch(); // PSH
ip++; // increment instruction pointer
int y = fetch(); // 5
}
如何停止呢?用一個標識變量:
#include <stdbool.h>
bool running = true;
int main() {
while (running) {
int x = fetch();
if (x == HLT) running = false;
ip++;
}
}
執行一條指令
void eval(int instr) {
switch (instr) {
case HLT:
running = false;
break;
}
}
放進 main
函數中:
bool running = true;
int ip = 0;
// instruction enum
// eval function
// fetch function
int main() {
while (running) {
eval(fetch());
ip++; // increment the ip every iteration
}
}
棧
爲了執行接下來的push
指令,我們需要一個 棧。這種簡單的數據結構可以使用數組或者鏈表實現,這裏使用數組,因爲簡單。同 ip 表示指令的位置一樣,我們用 sp 表示 棧的位置。
下面表示了程序執行過程中棧結構的變化:
[] // empty
[5] // push 5
[5, 6] // push 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 表示棧的位置:
-> sp -1
psh -> sp 0
psh -> sp 1
psh -> sp 3
sp points here (sp = 2)
|
V
[1, 5, 9]
0 1 2 <- array indices or "addresses"
結合到我們程序中,初始狀態 sp = -1
int ip = 0;
int sp = -1;
int stack[256];
...
接下來,push 指令應該這麼執行:
void eval(int instr) {
switch (instr) {
case HLT: {
running = false;
break;
}
case PSH: {
sp++;
stack[sp] = program[++ip];
break;
}
}
}
pop 指令:
case POP: {
// store the value at the stack in val_popped THEN decrement the stack ptr
int val_popped = stack[sp--];
// print it out!
printf("popped %d\n", val_popped);
break;
}
最後 add 指令:
case ADD: {
// first we pop the stack and store it as 'a'
int a = stack[sp--];
// then we pop the top of the stack and store it as 'b'
int b = stack[sp--];
// we then add the result and push it to the stack
int result = b + a;
sp++; // increment stack pointer **before**
stack[sp] = result; // set the value to the top of the stack
// all done!
break;
}
寄存器
定義寄存器
typedef enum {
A, B, C, D, E, F, PC, SP
NUM_OF_REGISTERS
} Registers;
int registers[NUM_OF_REGISTERS];
如果想往 A 寄存器上存值的時候可以:
register[A] = some_value
如何實現跳轉呢?這個時候,PC 寄存器 和 SP 寄存器就派上用場啦。
重新定義 上面程序中的 ip 和 sp 變量:
#define sp (registers[SP])
#define ip (registers[IP])
實現跳轉,其實就是設置 IP 寄存器的值:
; these are the instructions
PSH 10 ; 0 1
PSH 20 ; 2 3
SET IP 0 ; 4 5 6
完整代碼:
/**
This is almost identical to the articles
VM
**/
#include <stdio.h>
#include <stdbool.h>
bool running = true;
int ip = 0;
int sp = -1;
int stack[256];
typedef enum {
PSH,
ADD,
POP,
HLT
} InstructionSet;
const int program[] = {
PSH, 5,
PSH, 6,
ADD,
POP,
HLT
};
int fetch() {
return program[ip];
}
void eval(int instr) {
switch (instr) {
case HLT: {
running = false;
printf("done\n");
break;
}
case PSH: {
sp++;
stack[sp] = program[++ip];
break;
}
case POP: {
int val_popped = stack[sp--];
printf("popped %d\n", val_popped);
break;
}
case ADD: {
// first we pop the stack and store it as a
int a = stack[sp--];
// then we pop the top of the stack and store it as b
int b = stack[sp--];
// we then add the result and push it to the stack
int result = b + a;
sp++; // increment stack pointer **before**
stack[sp] = result; // set the value to the top of the stack
// all done!
break;
}
}
}
int main() {
while (running) {
eval(fetch());
ip++; // increment the ip every iteration
}
}