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虚拟机通过执行指令的循环来模拟计算机的工作过程。每一次循环可以简述为以下步骤:
取指(Fetch) : 从PC指向的内存位置获取下一条指令。
解码(Decode) : 解码指令,确定要执行的操作类型和操作数。
执行(Execute) : 根据指令类型执行相应的操作(如加载数据、执行算术运算、跳转等)。
从0到1开发一个简单的virtual machine 我们已经对LC3有了一个简单的了解,现在可以着手去做具体实现了。
其实我们要实现的核心就三部分:
初始化
指令的解析
运行
初始化 就是我们要做的一些准备工作,在文章开始我们了解到: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指令为例,有两种模式:
寄存器模式 0001 DR SR1 0 00 SR2,实现的是DR = SR1 + SR2
立即数模式 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