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