我们编写了任何人都不需要的模拟器

大家好


很久以前,就有一种编写某种处理器的仿真器的愿望。
还有什么比发明自行车更好的呢?


自行车的名称是V16,是通过加上单词Virtual(虚拟)和实际上的位深度来表示的。



从哪里开始?


当然,您需要从对处理器的描述开始。


一开始,我计划编写一个DCPU-16仿真器,但是Internet上有如此多的奇迹,因此我决定只专注于“舔”最基本的DCPU-16 1.1。


建筑学


内存和端口


  • V16寻址RAM的128Kb(65536字),也可以用作设备缓冲区和堆栈。
  • 堆栈以地址FFFF开始,因此,RSP的标准值为0xFFFF
  • V16 I / O端口有256个,它们的长度均为16位。 通过指令IN b, aOUT b, a进行读取和写入。

寄存器


V16有两组通用寄存器:主要寄存器和备用寄存器。
一个处理器只能使用一组,因此您可以使用XCR指令在一组之间切换。


使用说明


所有指令的最大长度为三个字,并且首先完全定义
第一个字分为三个值:低字节是操作码,高字节是两个4位值的形式,是对操作数的描述。


打断


这里的中断不过是一个表,该表具有处理器将CALL指令复制到的地址。 如果地址值为零,则中断不执行任何操作,仅复位HF标志。


取值范围内容描述
0x0 ... 0x3案例作为价值
0x4 ... 0x7在以下位置注册为值
0x8 ... 0xB寄存器+常量作为地址值
0xC作为值的常数
0xD常量作为值
0xERIP寄存器为只读值
0xFRSP注册为值

所有这些都应翻译成伪代码和单词的示例:


 MOV RAX, 0xABCD ; 350D ABCD MOV [RAX], 0x1234 ; 354D 1234 

周期数


V16可以1、2或3小节执行一条指令。 每个存储器访问是一个单独的时钟周期。 指令灵通!


让我们开始写作!


基本处理器结构的实现


  1. 一组寄存器。 只有四个寄存器,但是情况有所改善,处理器中有两个这样的寄存器组。 使用XCR指令进行切换。


     typedef struct Regs { uint16_t rax, rbx; //Primary Accumulator, Base Register uint16_t rcx, rdx; //Counter Register, Data Register } regs_t; 

  2. 标志 与DCPU-16不同,V16具有条件跳转,子例程调用并从中返回。 目前,处理器具有8个标志,其中5个是条件标志。


     //  ,    stdbool.h typedef struct Flags { bool IF, IR, HF; bool CF, ZF; bool EF, GF, LF; } flags_t; 

  3. 实际上,处理器本身。 它还描述了中断地址表,可以将其称为描述符并找到对x86的另一个引用。


     typedef struct CPU { //CPU Values uint16_t ram[V16_RAMSIZE]; //Random Access Memory uint16_t iop[V16_IOPSIZE]; //Input-Output Ports uint16_t idt[V16_IDTSIZE]; //Interrupt vectors table (Interrupt Description Table) flags_t flags; //Flags regs_t reg_m, reg_a; //Main and Alt register files regs_t * reg_current; //Current register file uint16_t rip, rsp, rex; //Internal Registers: Instruction Pointer, Stack Pointer, EXtended Accumulator //Emulator values bool reg_swapped; //Is current register file alt bool running; //Is cpu running uint32_t cycles; //RAM access counter } cpu_t; 

  4. 操作数。 获取值时,我们需要先读取,然后更改,然后将值写回到获取值的位置。


     typedef struct Opd { uint8_t code : 4; uint16_t value; uint16_t nextw; } opd_t; 


用于结构的功能


当描述了所有结构时,就需要使这些结构具有淬灭代码的神奇力量的功能。


 cpu_t * cpu_create(void); //   void cpu_delete(cpu_t *); //   void cpu_load(cpu_t *, const char *); // ROM   void cpu_rswap(cpu_t *); //   uint16_t cpu_nextw(cpu_t *); //RAM[RIP++]. Nuff said void cpu_getop(cpu_t *, opd_t *, uint8_t); //  void cpu_setop(cpu_t *, opd_t *, uint16_t); //  void cpu_tick(cpu_t *); //   void cpu_loop(cpu_t *); // ,    

另外,我没有提到带有操作代码的大型枚举,但这不是必需的,仅是为了了解所有这些混乱情况中所需要的。


勾号()函数


此外,还有对静态函数的调用,这些静态函数仅用于从tick()进行调用。


 void cpu_tick(cpu_t *cpu) { //    HLT,      if(cpu->flags.HF) { //      ,      if(!cpu->flags.IF) { cpu->running = false; } return; } //       uint16_t nw = cpu_nextw(cpu); uint8_t op = ((nw >> 8) & 0xFF); uint8_t ob = ((nw >> 4) & 0x0F); uint8_t oa = ((nw >> 0) & 0x0F); //     //   opd_t opdB = { 0 }; opd_t opdA = { 0 }; //    cpu_getop(cpu, &opdB, ob); cpu_getop(cpu, &opdA, oa); //        -  uint16_t B = opdB.value; uint16_t A = opdA.value; uint32_t R = 0xFFFFFFFF; //    bool clearf = true; //       ? //   ! switch(op) { //     . ,   ,    R } //   if(clearf) { cpu->flags.EF = false; cpu->flags.GF = false; cpu->flags.LF = false; } //  ,  32-   16-  //  0xFFFF0000,   0xFFFF << 16 //        32-  if(R != 0xFFFFFFFF) { cpu_setop(cpu, &opdB, (R & 0xFFFF)); cpu->rex = ((R >> 16) & 0xFFFF); cpu->flags.CF = (cpu->rex != 0); cpu->flags.ZF = (R == 0); } return; } 

接下来要做什么?


为了找到这个问题的答案,我将模拟器从C重写到C ++了五次,反之亦然。


但是,现在可以确定主要目标:


  • 固定常规中断(而不是仅调用函数并禁止接收其他中断,而要​​进行函数调用并将新的中断添加到队列中)。
  • 螺丝设备以及与之通信的方式,操作码的好处是256。
  • 教书 不要在Habr上写下任何异端 处理器以200 MHz的特定时钟速度运行。

结论


我希望这个“文章”对某人有用,有人会促使他们写类似的东西。


我的馅饼可以在github上查看。


另外,关于恐怖,我为该模拟器的旧版本提供了汇编程序(不,甚至不要尝试,模拟器至少会抱怨ROM格式错误)

Source: https://habr.com/ru/post/zh-CN457144/


All Articles