虚拟机实现(C语言)

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  

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
    }
}

你可能感兴趣的:(C)