在计算领域,VM(虚拟机)是一个术语,指的是模拟/虚拟化计算机系统/架构的系统。
一般来说,虚拟机有两类:
在本文中,我们将开发一个简单的进程虚拟机,旨在在独立于平台的环境中执行简单的计算机程序。 我们的虚拟机基于 LC-3 计算机架构,并且能够解释和执行 LC3 汇编代码(的子集)。
Little Computer 3,或 LC-3,是一种计算机教育编程语言,一种汇编语言,一种低级编程语言。 它具有相对简单的指令集,但可用于编写中等复杂的汇编程序,并且是 C 编译器的可行目标。 该语言比 x86 汇编语言简单,但具有许多与更复杂语言类似的功能。 这些功能使其值得入门教学,因此它最常用于向计算机科学和计算机工程学生教授编程和计算机体系结构基础知识
为简单起见,我们特意从以下功能中剥离了 LC-3 实现:中断处理、优先级、进程、状态寄存器 (PSR)、特权模式、管理程序堆栈、用户堆栈。 我们将只虚拟化最基本的硬件,并且我们将通过陷阱与外界(stdin、stdout)进行交互。
我们受 LC-3 启发的 VM 与当今大多数通用计算机一样,基于冯·诺依曼计算机模型,它将具有三个主要组件:CPU、主存储器、输入/输出设备。
我们的虚拟机功能如下:
我们的机器有 W=UINT16_MAX 个字,每个字 N=16 位。从 C 的角度来看,我们的内存可以定义为:
uint16_t PC_START = 0x3000;
uint16_t mem[UINT16_MAX+1] = {0};
我们的 VM 共有 10 个寄存器,每个寄存器 16 位:
从代码的角度来看,我们可以按如下方式实现它们:
enum regist { R0 = 0, R1, R2, R3, R4, R5, R6, R7, RPC, RCND, RCNT };
uint16_t reg[RCNT] = {0};
指令就像我们向虚拟机发出的命令。
为了提取操作码本身,我们可以编写一个实用宏来应用简单的按位技巧:
#define OPC(i) ((i)>>12)
我们可以在 C 中执行的一个好技巧(从数据建模的角度来看)是将所有可能的指令(及其关联的 C 函数)保存在数组中。 索引将代表实际的操作码(毕竟,操作码是从 0 到 15 的数字),并且该值将是指向相应 C 函数的指针。
#define NOPS (16) // number of instructions
typedef void (*op_ex_f)(uint16_t instruction);
//
// ... other operations here
//
static inline void add(uint16_t i) { /* code here */ }
static inline void and(uint16_t i) { /* code here */ }
//
// ... other operations here
//
op_ex_f op_ex[NOPS] = {
br, add, ld, st, jsr, and, ldr, str, rti, not, ldi, sti, jmp, res, lea, trap
};
…
我们只缺少两个功能:主循环和加载程序的能力。
我们虚拟机的主循环如下所示:
bool running=true;
uint16_t PC_START = 0x3000;
void start(uint16_t offset) {
reg[RPC] = PC_START + offset; // The RPC is set
while(running) {
uint16_t i = mr(reg[RPC]++); // We extract instructions from the memory
// location pointed by RPC
// We (auto)increment RPC
op_ex[OPC(i)](i); // We execute each instruction
}
}
现在,唯一缺少的是将程序加载到我们的虚拟机中的能力,在这方面我们将编写一个 ld_img 方法,能够将二进制文件直接加载到我们的主内存中:
void ld_img(char *fname, uint16_t offset) {
// Open (binary) file containing the VM program
FILE *in = fopen(fname, "rb");
if (NULL==in) {
fprintf(stderr, "Cannot open file %s.\n", fname);
exit(1);
}
// The position from were we start copying the file
// to the main memory
uint16_t *p = mem + PC_START + offset;
// Load the program in memory
fread(p, sizeof(uint16_t), (UINT16_MAX-PC_START), in);
// Close the file stream
fclose(in);
}
该方法返回 void 并接受两个输入参数:
我们虚拟机的主要方法如下所示:
int main(int argc, char **argv) {
ld_img(argv[1], 0x0);
start(0x0);
return 0;
}
我们的第一个程序,将从键盘读取两个数字并将它们的总和打印到标准输出。
0xF026 // 1111 0000 0010 0110 TRAP tinu16 ;read an uint16_t in R0
0x1220 // 0001 0010 0010 0000 ADD R1,R0,x0 ;add contents of R0 to R1
0xF026 // 1111 0000 0010 0110 TRAP tinu16 ;read an uint16_t in R0
0x1240 // 0001 0010 0010 0000 ADD R1,R1,R0 ;add contents of R0 to R1
0x1060 // 0001 0000 0110 0000 ADD R0,R1,x0 ;add contents of R1 to R0
0xF027 // 1111 0000 0010 0111 TRAP toutu16 ;show the contents of R0 to stdout
0xF025 // 1111 0000 0010 0101 HALT ;halt
语法对用户不友好,不是吗?我们的程序其实就是这一系列数字:0xF026 0x1220 0xF026 0x1240 0x1060 0xF027 0xF025。但如果我们仔细观察,就会发现在这些数字中我们一直在编码汇编指令。
例如,让我们看一下这个数字0xF026。其二进制表示为1111 0000 0010 0110。很容易看出1111是trap的编码,TRAPVECT是100111,对应tinu16。
或者为了更直观的表示,我们来分析 0x1220:
0x1220 ->
0001 001 000 1 00000
ADD R1 R0 IMM5=0
#include
#include
uint16_t program[] = {
/*mem[0x3000]=*/ 0xF026, // 1111 0000 0010 0110 TRAP trp_in_u16 ;read an uint16_t from stdin and put it in R0
/*mem[0x3002]=*/ 0x1220, // 0001 0010 0010 0000 ADD R1,R0,x0 ;add contents of R0 to R1
/*mem[0x3003]=*/ 0xF026, // 1111 0000 0010 0110 TRAP trp_in_u16 ;read an uint16_t from stdin and put it in R0
/*mem[0x3004]=*/ 0x1240, // 0001 0010 0010 0000 ADD R1,R1,R0 ;add contents of R0 to R1
/*mem[0x3006]=*/ 0x1060, // 0001 0000 0110 0000 ADD R0,R1,x0 ;add contents of R1 to R0
/*mem[0x3007]=*/ 0xF027, // 1111 0000 0010 0111 TRAP trp_out_u16;show the contents of R0 to stdout
/*mem[0x3006]=*/ 0xF025, // 1111 0000 0010 0101 HALT ;halt
};
int main(int argc, char** argv) {
char *outf = "sum.obj";
FILE *f = fopen(outf, "wb");
if (NULL==f) {
fprintf(stderr, "Cannot write to file %s\n", outf);
}
size_t writ = fwrite(program, sizeof(uint16_t), sizeof(program), f);
fprintf(stdout, "Written size_t=%lu to file %s\n", writ, outf);
fclose(f);
return 0;
}