reference: https://felixangell.com/blog/implementing-a-virtual-machine-in-c/
这里写篇文章介绍一下用C语言实现虚拟机。我喜欢从事底层程序的工作, 比如编译器、解释器、解析器和虚拟机等。所以我写这篇文章来学习一下虚拟机是如何工作的,以此来带领自己进入底层编程领域。
继续这篇文章之前你需要有:
我们先实现一个很简单的指令集,比如从一个寄存器上取值或者跳转到另外一个指令。
我们的虚拟机有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 很简单,以为我们没有很多分散在不同文件里的代码。
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
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
#include
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
}
}