🥬C11编写简易16位虚拟机
C/C++ | C11 | 虚拟机
虚拟机
在计算领域,VM(虚拟机)是一个术语,指的是模拟/虚拟化计算机系统/架构的系统。
一般来说,虚拟机有两类:
系统虚拟机提供真实机器的完整替代品。 它们实现了足够的功能,允许操作系统在它们上运行。 它们可以共享和管理硬件,有时多个环境可以在同一台物理机器上运行而不会互相妨碍。
处理虚拟机更简单,旨在在与平台无关的环境中执行计算机程序。 JVM 是进程虚拟机的一个很好的例子。
在本文中,我们将开发一个简单的进程虚拟机,旨在在独立于平台的环境中执行简单的计算机程序。 我们的虚拟机基于 LC-3 计算机架构,并且能够解释和执行 LC3 汇编代码(的子集)。
Little Computer 3,或 LC-3,是一种计算机教育编程语言,一种汇编语言,一种低级编程语言。 它具有相对简单的指令集,但可用于编写中等复杂的汇编程序,并且是 C 编译器的可行目标。 该语言比 x86 汇编语言简单,但具有许多与更复杂语言类似的功能。 这些功能使其值得入门教学,因此它最常用于向计算机科学和计算机工程学生教授编程和计算机体系结构基础知识
为简单起见,我们特意从以下功能中剥离了 LC-3 实现:中断处理、优先级、进程、状态寄存器 (PSR)、特权模式、管理程序堆栈、用户堆栈。 我们将只虚拟化最基本的硬件,并且我们将通过陷阱与外界(stdin、stdout)进行交互。
我们受 LC-3 启发的 VM 与当今大多数通用计算机一样,基于冯·诺依曼计算机模型,它将具有三个主要组件:CPU、主存储器、输入/输出设备。
实现
我们的虚拟机功能如下:
我们将程序加载到主存中
在RPC寄存器中,我们保存当前需要执行的指令
我们从指令中获取操作码(前 4 位),并据此解码其余参数
我们执行与给定指令相关的方法
我们增加 RPC 并继续下一条指令
内存
我们的机器有 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
};
加法
逻辑位“与”
ld - 加载 RPC + 偏移量
ldi - 间接加载
ldr - 加载+偏移量
lea - 加载有效地址
not - 按位求补
st - 存储
sti - 间接存储
str - 存储+偏移量
jump - 跳转
...
加载和执行程序
我们只缺少两个功能:主循环和加载程序的能力。
我们虚拟机的主循环如下所示:
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 <stdio.h>
#include <stdlib.h>
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;
}
源代码
Last updated