从0到1构建一个简单的虚拟机

github地址:x1aon1ng/my_lc3_vm: A LC-3 Virtual Machine

LC3介绍

LC-3(Little Computer 3)虚拟机是一个经典的教学用虚拟机。LC-3虚拟机具有一组特定的硬件组成部分和指令集,支持基本的算术操作、逻辑操作、内存操作、输入输出等功能。

寄存器

LC-3有8个通用寄存器(R0 到 R7),用于存储数据和中间结果。每个寄存器都是16位宽,能够存储一个16位的数据。

程序计数器(PC)

程序计数器(PC)用于存储下一条指令的内存地址。它是程序执行过程中至关重要的组件,每次执行完一条指令后,PC会自动增加,指向下一条指令。

状态寄存器(PSR)

状态寄存器保存计算机状态信息。它包含条件代码(N、Z、P,分别表示负数、零和正数)和中断标志位等。

内存

LC-3的内存大小为65536个内存单元,每个内存单元是16位宽。内存从地址0开始,到地址65535结束。

指令

数据传输指令

  • LD (Load): 从内存加载数据到寄存器。
  • ST (Store): 将寄存器中的数据存储到内存中。
  • LDR (Load Register): 从内存加载数据到寄存器,使用寄存器作为偏移量。
  • STR (Store Register): 将寄存器中的数据存储到内存,使用寄存器作为偏移量。

算术和逻辑指令

  • ADD (Add): 执行加法操作。
  • AND (And): 执行按位与操作。
  • NOT (Not): 执行按位取反操作。
  • LEA (Load Effective Address): 计算内存地址并将其加载到寄存器。

控制流指令

  • BR (Branch): 基于条件码进行跳转(如跳转到指定地址)。
  • JSR (Jump to Subroutine): 跳转到子程序并保存返回地址。
  • JSRR (Jump to Subroutine Register): 跳转到由寄存器指定的地址,并保存返回地址。
  • RET (Return): 从子程序返回。

输入输出指令

  • TRAP: 执行系统调用,用于输入输出操作(如读取键盘输入、打印字符等)。

中断

LC-3支持中断机制,允许程序响应外部事件。TRAP指令是LC-3虚拟机与操作系统进行交互的主要方式之一。

指令执行流程

LC-3虚拟机通过执行指令的循环来模拟计算机的工作过程。每一次循环可以简述为以下步骤:

  1. 取指(Fetch): 从PC指向的内存位置获取下一条指令。
  2. 解码(Decode): 解码指令,确定要执行的操作类型和操作数。
  3. 执行(Execute): 根据指令类型执行相应的操作(如加载数据、执行算术运算、跳转等)。

从0到1开发一个简单的virtual machine

我们已经对LC3有了一个简单的了解,现在可以着手去做具体实现了。

其实我们要实现的核心就三部分:

  1. 初始化
  2. 指令的解析
  3. 运行

初始化

就是我们要做的一些准备工作,在文章开始我们了解到:LC-3虚拟机具有10个寄存器,其中R0-R7为通用寄存器,还有一个PC程序计数器,一个COND作为条件标志,因此我们这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//寄存器
typedef enum {
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC, // pc
R_COND, // 条件标志寄存器
R_COUNT
} reg_names;

其中状态寄存器包含条件代码(N、Z、P,分别表示负数、零和正数)和中断标志位等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//条件标志
typedef enum {
FL_POS = 1 << 0, // 正
FL_ZRO = 1 << 1, // 零
FL_NEG = 1 << 2 // 负
} cond_flags;
// Trap
typedef enum {
TRAP_GETC = 0x20, // get character from keyboard
TRAP_OUT = 0x21, // output a character
TRAP_PUTS = 0x22, // output a word string
TRAP_IN = 0x23, // input a string
TRAP_PUTSP = 0x24, // output a byte string
TRAP_HALT = 0x25 // halt the program
} trap_codes;

接下来就是操作码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 操作码
typedef enum {
OP_BR = 0, // branch
OP_ADD, // add
OP_LD, // load
OP_ST, // store
OP_JSR, // jump register
OP_AND, // and
OP_LDR, // load register
OP_STR, // store register
OP_RTI, // 从中断返回
OP_NOT, // not
OP_LDI, // load indirect
OP_STI, // store indirect
OP_JMP, // jump
OP_RES, // 保留
OP_LEA, // load effective address
OP_TRAP // trap
} op_codes;

指令解析

LC-3的指令是十六位的,结构如下

1
2
3
4
15 14 13 12 11 10 9  8  7  6  5  4  3  2  1  0
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ OPCODE │ DR │ SR1 │mode│ SR2 │000000│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

运算指令以add指令为例,有两种模式:

  1. 寄存器模式 0001 DR SR1 0 00 SR2,实现的是DR = SR1 + SR2
  2. 立即数模式 0001 DR SR1 1 imm5,实现的是DR = SR1 + imm5

首先是两种模式的区分通过立即数标志位mode实现,根据指令结构通过移位和按位与操作获取对应内容

然后根据不同mode获取寄存器内容或者立即数,进行相加即可,最后更新标志位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void execute_add(uint16_t instr){
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);
}

数据传输指令以ld为例,具体实现就是解码获取R0和偏移量,计算地址获取数据存储到R0中,存储到寄存器即可

1
2
3
4
5
6
void execute_ld(uint16_t instr){
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t address = sign_extend(instr & 0x1FF, 9);
reg[r0] = mem_read(reg[R_PC] + address);
update_flags(r0);
}

控制流指令以br为例,解析指令获取条件标志为和pc偏移量,更新pc实现程序的跳转

1
2
3
4
5
6
7
void execute_br(uint16_t instr){
uint16_t flag = (instr >> 9) & 0x7; // 提取条件标志位(n,z,p)
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9); // 符号扩展9位偏移量为16位
if (flag & reg[R_COND]) { // 检查条件标志是否匹配当前条件码
reg[R_PC] += pc_offset; // 更新PC值,实现跳转
}
}

接着就是对Trap的处理,LC-3提供了一些预定义的Trap用于执行常见的任务并和I/O设备交互,针对不同trap进行相应操作即可,比如

1
2
3
4
5
6
void trap_in()
{
printf("Enter a character: ");
char c = getchar();
reg[R_R0] = (uint16_t)c;
}

辅助函数

在指令解析的过程中用到了一些辅助的函数,比如

符号拓展函数:比如imm5是一个只有5为的值,在进行加法时候需要将其拓展到16位以匹配另一个数字,对于正数直接填充0即可,但是负数就不行了,需要填充1

1
2
3
4
5
6
7
8
9
uint16_t sign_extend(uint16_t x, int bit_count){
// 检查符号位(最高有效位)
if((x >> (bit_count - 1)) & 1){
// 如果符号位为1,则进行符号扩展
// 将高位设置为1以保持负数的正确表示
x |= (0xFFFF << bit_count);
}
return x;
}

标志位更新函数

1
2
3
4
5
6
7
8
9
void update_flags(uint16_t r){ 
if (reg[r] == 0){
reg[R_COND] = FL_ZRO;
}else if(reg[r] >> 15) {
reg[R_COND] = FL_NEG;
}else {
reg[R_COND] = FL_POS;
}
}

内存读写函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint16_t mem_read(uint16_t address){
if (address >= MEMORY_MAX){
perror("Out of memory");
exit(1);
}
return memory[address];
}

void mem_write(uint16_t address, uint16_t val){
if (address >= MEMORY_MAX){
perror("Out of memory");
exit(1);
}
memory[address] = val;
}

读取文件,将二进制程序文件加载到虚拟机的内存中,这里需要注意的就是LC-3是大端序的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void read_image_file(FILE *file){ 
// 读取起始地址(origin),指示程序应加载到内存的哪个位置
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = (origin << 8) | (origin >> 8); // 转换字节序(大小端转换)

// 计算最大可读取的字数,避免超出内存范围
uint16_t max_read = MEMORY_MAX - origin;
uint16_t* p = memory + origin; // 指向内存中起始地址的位置
size_t read = fread(p, sizeof(uint16_t), max_read, file); // 从文件读取数据到内存

// 对所有读取的16位数据进行字节序转换
while(read-- > 0){
*p = (*p << 8) | (*p >> 8); // 交换高低字节
p++; // 移动到下一个16位位置
}
}

运行

运行其实就是不断的进行 取值 -> 解码 ->执行 的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
int run_vm(){
int running = 1;
while (running) {
uint16_t instr = mem_read(reg[R_PC]++);//取指
uint16_t op = instr >> 12; //解码
//执行
switch (op) {
case OP_BR:
execute_br(instr);
break;
case OP_ADD:
execute_add(instr);
break;
case OP_LD:
execute_ld(instr);
break;
case OP_ST:
execute_st(instr);
break;
case OP_JSR:
execute_jsr(instr);
break;
case OP_AND:
execute_and(instr);
break;
case OP_LDR:
execute_ldr(instr);
break;
case OP_STR:
execute_str(instr);
break;
case OP_RTI:
printf("Aborting: RTI instruction not implemented\n");
exit(1);
case OP_NOT:
execute_not(instr);
break;
case OP_LDI:
execute_ldi(instr);
break;
case OP_STI:
execute_sti(instr);
break;
case OP_JMP:
execute_jmp(instr);
break;
case OP_RES:
printf("Aborting: RES instruction not implemented\n");
exit(1);
case OP_LEA:
execute_lea(instr);
break;
case OP_TRAP:
execute_trap(instr);
if ((instr & 0xFF) == TRAP_HALT) {
running = 0;
}
break;
default:
printf("Aborting: Unknown opcode 0x%x\n", op);
exit(1);
}

}
return 0;
}

方法入口

初始化寄存器,读取入参,运行即可

还有一点就是调用了disable/restore_input_buffering()函数用于禁用和恢复输入缓冲,让程序可以实现即时的响应用户输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "vm.h"
#include <stdio.h>
#include <stdlib.h>

// 声明run_vm函数
int run_vm(void);

int main(int argc, char *argv[]){
if(argc < 2){
printf("ERROR: No image file given\n");
exit(2);
}

// 初始化寄存器
for(int i = 0; i < R_COUNT; i++){
reg[i] = 0;
}

reg[R_COND] = FL_ZRO;
reg[R_PC] = PC_START;

disable_input_buffering();

for(int i = 1; i < argc; i++){
if(read_image(argv[i]) == 0){
printf("Failed to load image: %s\n", argv[i]);
exit(1);
}
}

int result = run_vm();
restore_input_buffering();

return result;
}

运行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yus1an@yus1an-virtual-machine:~/Downloads/lc3_vm$ make
gcc -Wall -Wextra -std=c99 -O2 -c main.c -o main.o
gcc -Wall -Wextra -std=c99 -O2 -c vm.c -o vm.o
vm.c: In function ‘mem_read’:
vm.c:59:17: warning: comparison is always false due to limited range of data type [-Wtype-limits]
59 | if (address >= MEMORY_MAX){
| ^~
vm.c: In function ‘mem_write’:
vm.c:70:17: warning: comparison is always false due to limited range of data type [-Wtype-limits]
70 | if (address >= MEMORY_MAX){
| ^~
vm.c: In function ‘read_image_file’:
vm.c:98:5: warning: ignoring return value of ‘fread’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
98 | fread(&origin, sizeof(origin), 1, file);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
gcc -Wall -Wextra -std=c99 -O2 -o lc3-vm main.o vm.o
yus1an@yus1an-virtual-machine:~/Downloads/lc3_vm$ ls
hello_n1ng.obj lc3-vm main.c main.o Makefile vm.c vm.h vm.o
yus1an@yus1an-virtual-machine:~/Downloads/lc3_vm$ ./lc3-vm hello_n1ng.obj
Hello N1ng !

参考:https://www.jmeiners.com/lc3-vm/


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 3049155267@qq.com

💰

×

Help us with donation