虛擬機實現(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 <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
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章