虚拟机管理器又名虚拟机管理程序、虚拟机监控程序、VMM。它使用虚拟化技术,将一台物理机虚拟化为多台虚拟机,每台虚拟机都可以独立一个操作系统。其背后的原理也很简单,它就是一个应用程序,模拟了硬件所提供的功能,比如 CPU、I/O、寄存器、堆栈等。也就是说它使用软件来实现了这一套东西,通常它会定义自己的一套指令集、寄存器、堆栈等。说的直白点,就是在软件层面定义一套规范,并提供这些能力。
Little Computer-3(简称 LC-3)是用于教学的汇编语言,它有着相比于 x86 更为简洁的指令集,同时包含了主流 CPU 的经典思想。LC-3 麻雀虽小,五脏俱全。它的规范不算太多,下面我们实现一个LC-3 CPU的虚拟机管理程序。
1. LC-3 体系结构
-
内存地址空间 16 位,也就是最多可访问 0x0000~0xffff 的范围
-
通用寄存器 8 个,16 位。编号从 000~111,R0 ~ R7
-
三个标志寄存器,N(Negative)、P(Postive)、Z(Zero),每个 1 位
-
PC 寄存器
-
指令长度 16 位,操作码 op 固定高 4 位
-
采用大端字节序
所有指令如下图所示:
2. LC-3 CPU 定义
2.1 内存模拟
LC-3 有 2^16 个地址,每个地址包含一个 word (2 byte, 16 bit)。是大端序存储。所以用 C 表示如下。
#define MEMORY_MAX (1 << 16)
uint16_t memory[MEMORY_MAX]; /* 65536 locations */
2.2 寄存器模拟
LC-3共有 10 个寄存器, 每个都是 16 位, 大部分是通用寄存器。
-
8 个通用寄存器(R0-R7)
-
1 个程序计数器 (PC) 寄存器
-
1 个条件标志 (COND) 寄存器
enum
{
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC, /* program counter */
R_COND,
R_COUNT
};
uint16_t reg[R_COUNT];
2.3 指令集定义
LC-3中只有 16 条指令, 每条指令长 16 位。左侧 4 位存储操作码, 其余位用于存储参数。
enum
{
OP_BR = 0, /* 0000 branch */
OP_ADD, /* 0001 add */
OP_LD, /* 0010 load */
OP_ST, /* 0011 store */
OP_JSR, /* 0100 jump register */
OP_AND, /* 0101 bitwise and */
OP_LDR, /* 0110 load register */
OP_STR, /* 0111 store register */
OP_RTI, /* 1000 unused */
OP_NOT, /* 1001 bitwise not */
OP_LDI, /* 1010 load indirect */
OP_STI, /* 1011 store indirect */
OP_JMP, /* 1100 jump */
OP_RES, /* 1101 reserved (unused) */
OP_LEA, /* 1110 load effective address */
OP_TRAP /* 1111 execute trap */
};
R_COND 寄存器存储条件标志, 提供最近执行结果的信息。这样程序就可以执行逻辑/循环语句, 如 if (x > 0) { ... }。
enum
{
FL_POS = 1 << 0, /* P: positive (greater than zero) */
FL_ZRO = 1 << 1, /* Z: zero */
FL_NEG = 1 << 2, /* N: negative (smaller than zero) */
};
2.4 中断
LC-3 中类似 x86 int 的 TRAP,实现输入输出等功能。
enum
{
TRAP_GETC = 0x20,
TRAP_OUT = 0x21,
TRAP_PUTS = 0x22,
TRAP_IN = 0x23,
TRAP_PUTSP = 0x24,
TRAP_HALT = 0x25
};
LC-3 有两个内存映射寄存器需要实现,它们是键盘状态寄存器 (KBSR)和键盘数据寄存器 (KBDR)。 键盘状态寄存器(KBSR)指示是否有按键被按下,键盘数据寄存器(KBDR)则识别被按下的按键。
enum
{
MR_KBSR = 0xFE00, /* keyboard status */
MR_KBDR = 0xFE02 /* keyboard data */
}
3. LC-3 指令实现
代码是以汇编形式编写的,而汇编是以纯文本编码的人类可读可写形式。我们使用一种称为汇编器的工具,将每一行文本转换成虚拟机可以理解的 16 位二进制指令。这种二进制形式本质上是 16 位指令的数组,被称为机器码,也是虚拟机实际运行的内容。VMM的作用就是解析并处理这些机器码。
3.1 ADD 指令
两个变量相加(+)。ADD DR,SR1,SR2 或者 ADD DR,SR1,imm
case OP_ADD:
{
// 目的寄存器 (DR)
uint16_t r0 = (instr >> 9) & 0x7;
// 源寄存器1 (SR1)
uint16_t r1 = (instr >> 6) & 0x7;
// 立即数标志
uint16_t imm_flag = (instr >> 5) & 0x1;
if (imm_flag == 0) {
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] + reg[r2];
} else {
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] + imm5;
}
update_flags(r0);
}
3.2 AND 指令
与运算。和 ADD 一样,有两种模式,指令格式一模一样。
case OP_AND:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t imm_flag = (instr >> 5) & 0x1;
if (imm_flag) {
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] & imm5;
} else {
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] & reg[r2];
}
update_flags(r0);
}
3.3 NOT 指令
取反指令。取反之后同时更新标志寄存器。
case OP_NOT:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
reg[r0] = ~reg[r1];
update_flags(r0);
}
3.4 BR 指令
根据“条件标志位”进行跳转。跳转参数是相对于 PC 的偏移量。
case OP_BR:
{
uint16_t n_flag = (instr >> 11) & 0x1;
uint16_t z_flag = (instr >> 10) & 0x1;
uint16_t p_flag = (instr >> 9) & 0x1;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
if ((n_flag && (reg[R_COND] & FL_NEG)) ||
(z_flag && (reg[R_COND] & FL_ZRO)) ||
(p_flag && (reg[R_COND] & FL_POS))) {
reg[R_PC] += pc_offset;
}
}
3.5 JMP 指令
跳转指令,只有一个寄存器参数,在 6~8 位。作用是更新 PC 为寄存器的值。
case OP_JMP:
{
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1];
}
3.6 JSR 指令
全称为 Jump to Subroutine,也就是函数调用。它有两种模式,通过指令第 12 位 flag = instr[11] 来区分。两种模式的前置操作均为保存现场:将当前 PC 值保存到 R7 中,目的是为了在函数调用完成之后恢复原 PC 值。
case OP_JSR:
{
uint16_t long_flag = (instr >> 11) & 1;
if (long_flag) {
reg[R_R7] = reg[R_PC];
uint16_t long_pc_offset = sign_extend(instr & 0x7FF, 11);
reg[R_PC] += long_pc_offset; /* JSR 直接跳转 */
} else {
uint16_t tmp = reg[(instr >> 6) & 0x7];
reg[R_R7] = reg[R_PC];
reg[R_PC] = tmp; /* JSRR 寄存器间接跳转 */
}
}
3.7 LD 指令
从 PC + 相对偏移得到地址后,从地址中取出值并赋值目的寄存器。
case OP_LD:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = mem_read(reg[R_PC] + pc_offset);
update_flags(r0);
}
3.8 LDI 指令
间接加载。该指令用于将内存中某个位置的值加载到寄存器中。
case OP_LDI:
{
// 目的寄存器 (DR)
uint16_t r0 = (instr >> 9) & 0x7;
// PC偏移 9bit
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
// 将 pc_offset 加到 PC 上, 然后查看该内存位置获取最终地址
reg[r0] = mem_read(mem_read(reg[R_PC] + pc_offset));
update_flags(r0);
}
3.9 LDR 指令
将 SR1 的值与偏移量相加后的内存位置值加载到目的寄存器中。
case OP_LDR:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
reg[r0] = mem_read(reg[r1] + offset);
update_flags(r0);
}
3.10 LEA 指令
将LABEL标记的地址加载到目的寄存器中。
case OP_LEA:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = reg[R_PC] + pc_offset;
update_flags(r0);
}
3.11 ST 指令
将 SR1 中的值存储到 LABEL 指示的存储位置。
case OP_ST:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(reg[R_PC] + pc_offset, reg[r0]);
}
3.12 STI 指令
将 SR1 中的值存储到 LABEL 存储位置所指示的内存中。
case OP_STI:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(mem_read(reg[R_PC] + pc_offset), reg[r0]);
}
3.13 STR 指令
SR1中的值存储在 SR2 和 offest6 相加后的内存位置中。
case OP_STR:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
mem_write(reg[r1] + offset, reg[r0]);
}
3.14 Trap 中断
LC-3 提供了一些预定义的Trap,用于执行常见任务和与 I/O 设备交互。例如,用于从键盘获取输入和向控制台显示字符串。每个Trap都有一个入口(类似于操作码)。
case OP_TRAP:
{
reg[R_R7] = reg[R_PC];
switch (instr & 0xFF)
{
case TRAP_GETC:
case TRAP_OUT:
case TRAP_PUTS:
case TRAP_IN:
case TRAP_PUTSP:
case TRAP_HALT:
}
}
(1) Trap GETC
从键盘读取一个输入字符并将其存储到 R0 中,而不将该字符回显到控制台。
case TRAP_GETC:
reg[R_R0] = (uint16_t)getchar();
update_flags(R_R0);
(2) Trap OUT
将R0中的字符输出到控制台。
case TRAP_OUT:
putc((char)reg[R_R0], stdout);
fflush(stdout);
(3) Trap PUTS
从 R0 中包含的地址开始,向控制台输出字符串。
case TRAP_PUTS:
{
// 16 bit 表示一个字符
uint16_t* c = mem_addr() + reg[R_R0];
while (*c) {
putc((char)*c, stdout);
++c;
}
fflush(stdout);
}
(4)Trap IN
从键盘上读取一个输入字符,将其存储到 R0 中,并向控制台回传该字符。
case TRAP_IN:
{
char c = getchar();
putc(c, stdout);
fflush(stdout);
reg[R_R0] = (uint16_t)c;
update_flags(R_R0);
}
(5) Trap PUTSP
与 PUTS 类似,不同之处在于它输出字符串时,它先输出低 8 位,再输出 高 8 位。
case TRAP_PUTSP:
{
uint16_t* c = mem_addr() + reg[R_R0];
while (*c) {
char char1 = (*c) & 0xFF;
putc(char1, stdout);
char char2 = (*c) >> 8;
if (char2) {
putc(char2, stdout);
}
++c;
}
fflush(stdout);
}
(6) Trap HALT
结束程序。
case TRAP_HALT:
fflush(stdout);
running = 0; // 跳出循环,结束程序
3.15 保留指令
RTI 和 RES 未使用,不处理即可。不过它们倒是可以用作占位指令。
4. 编译运行
完整代码见:
https://gist.github.com/hbuxiaofei/641d648b045d10a36a12e27c0da00317
(1)编译
在linux终端执行:
$ gcc lc3.c -o lc3-vm
(2)下载编译好的二进制虚拟机程序
2048:https://www.jmeiners.com/lc3-vm/supplies/2048.obj
Rogue: https://www.jmeiners.com/lc3-vm/supplies/rogue.obj
(3)使用 .obj 文件作为参数运行虚拟机
$ lc3-vm 2048.obj
(4)2048游戏
通过 WASD 按键进行操作
参考:
【CPU Design for LC-3 instruction set 】
https://coertvonk.com/inquiries/how-cpu-work/design-30973
【The LC-3】
https://www.cs.utexas.edu/users/fussell/courses/cs310h/lectures/Lecture_10-310h.pdf
【LC-3 Assembly Lab Manual】
https://people.cs.georgetown.edu/~squier/Teaching/HardwareFundamentals/LC3-trunk/docs/LC3-AssemblyManualAndExamples.pdf
【Write your Own Virtual Machine】
https://www.jmeiners.com/lc3-vm/
【Introduction to Computing Systems】
https://highered.mheducation.com/sites/0072467509/index.html