乐趣区

关于虚拟机:LC3-虚拟机学习总结

2023 年春节前看到不少公众号在刷虚拟机实现的文章,所以过年在家静下心来看了看,也本人试了试,感觉挺乏味的。此处写一篇总结,算是给本人一个交代。

零 先聊聊背景

cpu 其实并不了解高级语言代码,它只能了解汇编指令。简略来说(此处懒得画图,用 markdown 代替了,下同):

                  c 语言编译器                   执行
我写的 c 语言代码 -----------> cpu 可执行的汇编指令 <----- cpu

然而 cpu 业界也不是铁板一块,最典型的比方 x86 架构和 arm 架构,它们的汇编命令并不相同。简略来说:

                  c 语言 x86 编译器                                                             执行
我写的 c 语言代码 -----------------> x86 cpu 可执行的汇编指令文件 <-- x86 cpu
                  c 语言 arm 编译器                                                             执行
                -----------------> arm cpu 可执行的汇编指令文件 <-- arm cpu

其实还有很多其它品种的汇编指令,不一一列举。
这样造成了软件中跨平台的困局,由此诞生了一类用于抹平它们区别的软件:虚拟机。简略来说:

           虚拟机编译器                  执行            执行
高级语言代码 -----------> 虚拟机汇编指令 <----- x86 虚拟机 <---- x86 cpu
                                      执行            执行
                                    <----- arm 虚拟机 <---- arm cpu

理论状况更加简单,还要波及到操作系统问题:

             虚拟机编译器                执行                    执行
高级语言代码 -----------> 虚拟机汇编指令 <---- windows x86 虚拟机 <--- windows x86 零碎
                                      执行                    执行
                                     <---- windows arm 虚拟机 <--- windows arm 零碎 

虚拟机软件是有平台辨别的,然而虚拟机汇编指令是跨平台的。最驰名的虚拟机比方 jvm:

          java 编译器                 执行
java 代码 -----------> class 文件 <----- windows x86 jvm
                                    执行
                                 <----- windows arm jvm
                                    执行
                                 <----- linux x86 jvm

虚拟机的呈现很好的抹平了不同操作系统或者底层架构的不对立。java 所谓的“一次编译,到处执行”就是这么来的。

一 什么是 LC-3

LC-3 是一套计算机架构,也有一套本人的汇编指令码,然而相比起 x86 / arm / jvm 这类成熟的商业产品,会简略很多很多,所以次要也是用于教学应用。LC-3 的整套架构是齐备的,实践上能够用于构筑任何软件我的项目,然而性能其实很弱。
LC-3 虚拟机就是能够执行 LC-3 汇编指令的软件。

二 LC-3 虚拟机中的次要概念

1 寄存器 – register

寄存器是虚拟机的调度工作台,用于长期寄存内存中的数据或者计算结果。LC-3 中有十个寄存器位,其中前八个是失常存放数据的寄存器,第九个是 pc 指针,第十个是条件指针。
这十个寄存器在 LC-3 中是十个 16 位无符号数字。在 c 语言中是 uint16,在 java 中是 short(留神是无符号的,须要转换,因为 java 里没有无符号数),在 rust 里是 u16。在实现的时候通常会写成一个数组。
上面具体解释一下 pc 指针和条件指针。

  • pc 指针(pc)
    pc 指针是用于指向指令码编号的指针。
    举个例子,一个汇编文件内共计有 100 条汇编指令,pc 指针刚开始指向 0,每执行完一条就自增 1,始终到完结是 99。如果中途呈现 while / for / if else 这样的语句,可能呈现 pc 指针回前或者往后的状况。
  • 条件指针(condition)
    条件指针的值只有三种:0 / 2 / 4。
    代码逻辑中通常会存在 while(xxx) 或者 if(xxx) 这样的条件判断。
    条件判断最终会有三种后果:true / false / equals。这三种后果就对应了条件指针的三种值。
    所以条件指针的外围是用于寄存逻辑判断的后果,用于解决 pc 指针的跳转。

    2 内存 – memory

    LC-3 中有 65536 个内存槽,每个内存槽能够存储一个 16 位无符号数字。
    内存槽数量和寄存器数量都是标准中定义的,临时无需探寻其原理或者扩展性,因为 LC-3 次要用于教学。
    在实际当中,内存通常是一个数组示意。

    3 指令类型

    LC-3 中一共 16 个指令,指令码对立是四位,一共能够演绎成四类:

  • 数值的数学运算(operate)

    • add – 指令码 0001,用于两个变量相加(+)
    • and – 指令码 0101,用于两个变量相与(&)
    • not – 指令码 1001,用于一个变量取反(c 语言中的 ~,rust 中的 !)
  • 将数据从寄存器存储到内存(store)

    • st – 指令码 0011,用于将寄存器的值写入内存中,内存地址应用 pc 指针和偏移量定位
    • sti – 指令码 0110,用于将寄存器的值写入内存中,在 st 指令的根底上减少一次内存定位
    • str – 指令吗 0111,用于将寄存器的值写入内存中,内存地址应用常量和偏移量定位
  • 将数据从内存提取到寄存器(load)

    • ldi – 指令码 1010,用于将内存的值写入寄存器中,内存地址应用 pc 指针和偏移量定位
    • ld – 指令码 0010,用于将内存的值写入寄存器中,内存地址应用常量和偏移量定位
    • lea – 指令码 1110,用于将内存一个内存地址写入寄存器中,内存地址应用 pc 指针和偏移量定位

    (须要留神的是,ldi 写入的是内存中的值,lea 写入的是内存地址)

  • 业务逻辑,定向 pc 指针(logic)

    • br – 指令码 0000,应用 condition 指针来判断是否要挪动 pc 指针
    • jmp – 指令码 1100,将 pc 指针挪动到一个指定的数字上
    • jsr – 指令码 0100,用偏移量或者常量来挪动 pc 指针
  • 其它无奈归类的指令(other)

    • trap – 指令码 1111,用来和硬件对接,输入输出字符串
    • res – 指令码 1101,预留的指令,临时没有用
    • rti – 指令码 1000,临时没搞清楚是干啥用的

    4 其它指令相干的概念

    这些概念在前面的解析指令的过程中会用到。

    opCode     - 4  位,代表操作的指令码
    DR         - 3  位,存储后果的寄存器地址
    pcOffset9  - 9  位,有符号的 pc 指针的偏移 9 位
    pcOffset11 - 11 位,有符号的 pc 指针的偏移 11 位
    offset6    - 6  位,有符号的内存指针偏移量
    SR1        - 3  位,第一个寄存器地址,取反等操作只须要一个寄存器地址就够了
    SR2        - 3  位,第二个寄存器地址,比方相加运算就须要两个寄存器配合(因为两个变量)baseR      - 3  位,代表一个无符号的整数
    flag       - 1  位,代表指令模式
    imm5       - 5  位,代表一个有符号的整数
    trapvect8  - 8  位,用于 trap 指令中确认性能 

    5 指令的解析 — 以 add 指令为例

    以 add 指令为例,它的作用是从某个寄存器 A 内获取值,而后和另一个数字相加,并存放到另一个寄存器 B 中。
    add 指令有两种组成形式:

  • 第一种

    |  0001  | --- | --- |  0  |  00   | --- |
      opCode    DR   SR1   flag  有效位   SR2

    在这种方组成形式中,opCode 是固定的,占四位;DR 是存储相加后果的寄存器地址,占三位;SR1 是第一个获取值的寄存器地址,占三位;flag 占一位,固定是 0;SR2 是第二个获取值的寄存器地址,占三位。
    解决逻辑的伪代码是:

    register[DR] = register[SR1] + register[SR2]
  • 第二种

    | 0001 | --- | --- |   1   | ----- |
     opCode   DR   SR1    flag    imm5

    在这种方组成形式中,opCode 是固定的,占四位;DR 是存储相加后果的寄存器地址,占三位;SR1 是第一个获取值的寄存器地址,占三位;flag 占一位,固定是 1;imm5 是一个有符号的负数,占五位。
    解决逻辑的伪代码是:

    register[DR] = register[SR1] + imm5

    三 rust 实现

    应用 rust 实现的 LC-3 虚拟机。
    备注:此为 2023 年春节期间的学习作,代码较为毛糙,只为了解和学习虚拟机原理,并练习 rust 语言。
    gitee 仓库地址:https://gitee.com/mikylin/rvm_lc3
    (代码格调被 java 带偏了,可能写的不太 rust)

    1 读取文件

    此处读取文件,将指令集写入到虚拟机的内存中。

    /// 将汇编文件加载到内存中
    pub fn load_file(vm: &mut L3vm, file_name: String) {
    
      // 获取文件绝对路径
      let mut name = get_file_name(file_name);
    
      let f = File::open(name).expect("couldn't open file");
      let mut buf = BufReader::new(f);
    
      // 文件第一个字符依照常规数字 16880,标注了 pc 指针地位是 3000
      let mut mem_addr = buf.read_u16::<BigEndian>().expect("error");
      loop {
          // 大端读取
          match buf.read_u16::<BigEndian>() {Ok(instruction) => {
                  // 写入内存
                  vm.memory_write(mem_addr, instruction);
    
                  // 是否开启 debug 日志,用于察看每次写入的是什么数字
                  if vm.is_debug() {println!("addr: {}, instr: {}, pc: {}", mem_addr, build_instruction(instruction), vm.pc());
                  }
    
                  // 内存地址加 1,下一次循环会读取新的内存地址
                  mem_addr += 1;
              }
              Err(e) => {if e.kind() == std::io::ErrorKind::UnexpectedEof {return;}
                  panic!("{}", e);
              }
          }
      }
    }
    
    /// 解决文件名,如果应用相对路径的话,须要批改成绝对路径
    /// 如何解决相对路径还没理解过
    fn get_file_name(file_name: String) -> String {let mut name = file_name.clone();
      if file_name.starts_with(".") {name = root();
          println!("root: {}", name);
          name.push_str(&file_name[1..]);
      }
      println!("file name: {}", name);
      name
    
    }
    
    /// 此处须要引入 project_root 我的项目,获取可执行文件所在的绝对路径
    fn root() -> String {let current_path = project_root::get_project_root().unwrap();
      current_path.to_str().unwrap().to_string()}

    2 VM

    VM 是虚拟机的主体,负责管理寄存器和内存。

    /// 虚拟机结构体
    pub struct L3vm {memory: [u16; MEMORY_COUNT],    // 内存槽
      register: [u16; REG_COUNT],          // 寄存器槽
      debug: bool,                    // 是否开启 debug 日志
    }
    
    impl L3vm {
      /// 创立虚拟机
      pub fn new(debug: bool) -> Self {
    
          // 寄存器数组
          let mut register: [u16; REG_COUNT] = [0; REG_COUNT];
    
          // 初始化 pc 指针
          register[R_PC as usize] = PC_INIT;
    
          // 内存数组
          let memory: [u16; MEMORY_COUNT] = [0; MEMORY_COUNT];
    
          // 创立虚拟机对象
          L3vm {
              memory,
              register,
              debug
          }
      }
    
      pub fn is_debug(&self) -> bool {self.debug}
    
      /// 读取寄存器
      pub fn register(&mut self, index: u16) -> u16 {
          if index < 0 || index >= 10 {panic!("registers index must in 0 to 10, but [{}]!", index)
          }
          self.register[index as usize]
      }
    
      /// 写入寄存器
      pub fn register_write(&mut self, index: u16, val: u16) {
          if index < 0 || index >= 10 {panic!("registers index must in 0 to 10, but [{}]!", index)
          }
          self.register[index as usize] = val
      }
    
      /// 读取 pc 指针
      pub fn pc(&mut self) -> u16 {self.register(R_PC)
      }
    
      /// 写入 pc 指针
      pub fn pc_write(&mut self, val: u16) {self.register_write(R_PC, val);
      }
    
      /// 读取 condition 指针
      pub fn condition(&mut self) -> u16 {self.register(R_COND)
      }
    
      /// 读取内存
      pub fn memory(&mut self, address: u16) -> u16 {
    
          // 当要读取的内存地址是监控键盘按下的非凡地址的时候,须要做非凡写入操作
          // 这块逻辑是用于配合 trap 指令
          if address == MR_KBSR {
              // 获取键盘操作
              let mut buffer = [0; 1];
              std::io::stdin().read_exact(&mut buffer).unwrap();
              let b = buffer[0];
              if b != 0 {self.memory_write(MR_KBSR, 1 << 15);
                  self.memory_write(MR_KBDR, b as u16);
              } else {self.memory_write(MR_KBSR, 0)
              }
          }
          self.memory[address as usize]
      }
    
      /// 写入内存
      pub fn memory_write(&mut self, address: u16, val: u16) {self.memory[address as usize] = val;
      }
    }

    3 指令路由

    pub fn execute(vm: &mut L3vm) {while vm.pc() < MEMORY_COUNT as u16 {
    
          // 每次都须要将 pc + 1,用以跳转到下一条指令中
          let pc = vm.pc();
          let instruction = vm.memory(pc);
          vm.pc_write(pc + 1);
    
          // 用操作前四位操作码,依据操作码路由相干办法
          match instruction >> 12 {OP_ADD  => operate::add(vm, instruction),
              OP_AND  => operate::and(vm, instruction),
              OP_NOT  => operate::not(vm, instruction),
              OP_BR   => logic::br(vm, instruction),
              OP_JMP  => logic::jmp(vm, instruction),
              OP_JSR  => logic::jsr(vm, instruction),
              OP_LD   => load::ld(vm, instruction),
              OP_LDI  => load::ldi(vm, instruction),
              OP_LEA  => load::lea(vm, instruction),
              OP_ST   => store::st(vm, instruction),
              OP_STI  => store::sti(vm, instruction),
              OP_STR  => store::str(vm, instruction),
              OP_TRAP => other::trap(vm, instruction),
              OP_RES  => other::res(vm, instruction),
              OP_RTI  => other::rti(vm, instruction),
              _ => {println!("not support op code.");
              }
          }
    
          // debug 日志
          if vm.is_debug() {print_log(instruction, vm);
          }
      }
    }

    整个指令是一个 u16 的数字,将其二进制化之后能够晓得,前四位为 opCode,应用 opCode 能够路由到对应的指令实现里。

    4 指令实现

    来看一个最简略的指令 — not。
    not 指令用于将一个数字取反码。

    /// not
    /// 将一个变量进行取反操作
    ///
    ///
    ///
    /// 指令组成 1:/// |  1001    | ---  |  ---  | ------ |
    ///  opCode       DR     SR1     有效位
    pub fn not(vm: &mut L3vm, instruction: u16) {
      // DR
      let dr = (instruction >> 9) & P_3;
      // SR1
      let sr1 = (instruction >> 6) & P_3;
      // 从 SR1 中获取值
      let v1 = vm.register(sr1);
      // 取反并存入
      vm.register_write(dr, !v1);
      // 更新 condition 指针
      setcc(vm, dr)
    }

    opCode 稳固为 1001。
    DR 和 SR1 都代表一个寄存器的地址,用 register[DR] 或者 register[SR1] 都能够获取一个寄存器槽,对其进行读取和写入操作。
    此处的业务逻辑是从 register[SR1] 中获取一个值,对其取反之后写入 register[DR] 中。
    其它的指令大多数都很相似,不赘述,指令每个局部的意义参考上述第二局部的第四大节(其它指令相干的概念)。

退出移动版