我对编程虚拟机很感兴趣,没有像虚拟机或vmware那样华丽,但可以模仿一个简单的架构,无论是cisc还是risc,比如Zilog,SPARC,MIPS或80686架构模型.
我想通过这样做,制作一个相同类型的模拟器会相对简单,我只是对使用它来获得经验感兴趣(作为我的第一个C项目,我宁愿在C中做这个比在还要别的吗).
获取有关特定类型应用程序的信息的好方法(在您的情况下也是获取c语言的好方法)是通过查看相同类型的开源项目的结构和细节.人们可能决定只是偷看,简要回顾然后"忘记",以便从头开始一个人自己的项目,但在所有情况下这种类型的访问都是有益的.
既然你提到了"简单架构"和Zilog,我认为Z80处理器可能是一个很好的匹配.由于各种原因,Z80仿真器类型中有许多当前和过去的项目.顺便说一句,其中一个原因是许多老式插槽式视频控制台在Z80上运行,这促使怀旧游戏玩家编写模拟器来运行他们的旧收藏;-)
YAZE-AG就是这样一个项目的一个例子,它既包括完整的Z80仿真器,也包括C/PM.整个事情用C语写.它也相对成熟(版本2.x)并且活跃.我猜这是一个非常小的团队的工作(可能是一个;-)).
祝好运!
如果您正在设计CPU并对其进行仿真,
准备好核心.意思是,为寄存器编写类.写一个标志.写一个内存控制器.
想想操作码的类型.还有,单词的长度是多少?它是一个16位CPU吗?8位?
您想使用什么类型的内存访问?DMA?HDMA?
您想支持哪种类型的中断?CPU是一个学习平台吗?它只是一个CPU和一些内存,还是它实际上有设备连接到它?(声音,视频等).
以下是我正在处理的模拟器中的一些代码(公共域).已经工作了几天.到目前为止大约有3200行代码(大多数是microcode.cs,由于它的大小为2600行而未在此处发布).
using System; namespace SYSTEM.cpu { // NOTE: Only level-trigger interrupts are planned right now // To implement: // - microcode // - execution unit // - etc // This is the "core"; think of the CPU core like a building. You have several departments; flags, memory and registers // Microcode is external class core { public cpu_flags flags; public cpu_registers registers; public cpu_memory memory; public core(byte[] ROM, byte[] PRG) { flags = new cpu_flags(); registers = new cpu_registers(); memory = new cpu_memory(ROM, PRG); return; } } } using System; namespace SYSTEM.cpu { class cpu_flags { // SYSTEM is not a 6502 emulator. The flags here, however, are exactly named as in 6502's SR // They do NOT, however, WORK the same as in 6502. They are intended to similar uses, but the only identity is the naming. // I just like the 6502's naming and whatnot. // This would otherwise be a register in SYSTEM.cpu_core.cpu_registers. SR, with the bits used correctly. // This would be less readable, code-wise, so I've opted to dedicate an entire CLASS to the status register // Though, I should implement here a function for putting the flags in a byte, so "SR" can be pushed when servicing interrupts public bool negative, // set if the high bit of the result of the last operation was 1 // bit 7, then so on overflow, // says whether the last arithmetic operation resulted in overflow (NOTE: No subtraction opcodes available in SYSTEM) // NO FLAG brk, // break flag, set when a BREAK instruction is executed // NO FLAG (would be decimal flag, but I don't see why anyone would want BCD. If you want it, go implement it in my emulator; in software) // i.e. don't implement it in SYSTEM; write it in SYSTEM ASM and run it in SYSTEM's DEBUGGER irq, // whether or not an interrupt should begin at the next interrupt period (if false, no interrupt) zero, // says whether the last arithmetic operation resulted in zero carry; // set when alpha rolls from 0xFFFF to 0x0000, or when a 1 is rotated/shifted during arithmetic public cpu_flags() { negative = true; // all arithmetic registers are FFFF by default, so of course they are negative overflow = false; // obviously, because no arithmetic operation has been performed yet brk = false; irq = true; // interrupts are enabled by default of course zero = false; // obviously, since all arith regs are not zero by default carry = false; // obviously, since no carry operation was performed return; } // Explain: // These flags are public. No point putting much management on them here, since they are boolean // The opcodes that SYSTEM supports, will act on these flags. This is just here for code clarity/organisation } } using System; // This implements the memory controller // NOTE: NO BANK SWITCHING IMPLEMENTED, AND NOT PLANNED AT THE MOMENT, SO MAKE DO WITH TEH 64 // SYSTEM has a 16-bit address bus (and the maximum memory supported; 64K) // SYSTEM also has a 16-bit data bus; 8-bit operations are also performed here, they just use the low bits // 0x0000-0x00FF is stack // 0xF000-0xFFFF is mapped to BIOS ROM, and read-only; this is where BIOS is loaded on startup. // (meaning PROGRAM ROM can be up to 4096B, or 4K. Normally this will be used for loading a BIOS) // Mapping other PROGRAM ROM should start from 0x0100, but execution should start from 0xF000, where ROM/BIOS is mapped // NOTE: PROGRAM ROM IS 32K, and mapped from 0x0100 to 0x80FF // ;-) namespace SYSTEM.cpu { class cpu_memory { // to implement: // device interaction (certain addresses in ROM should be writeable by external device, connected to the controller) // anything else that comes to mind. // Oh, and bank switching, if feasible private byte[] RAM; // As in the bull? ... public cpu_memory(byte[] ROM, byte[] PRG) { // Some code here can be condensed, but for the interest of readability, it is optimized for readability. Not space. // Checking whether environment is sane... SYSTEM is grinning and holding a spatula. Guess not. if(ROM.Length > 4096) throw new Exception("****SYSINIT PANIC****: BIOS ROM size INCORRECT. MUST be within 4096 BYTES. STOP"); if (PRG.Length > 32768) throw new Exception("****SYSINIT PANIC**** PROGRAM ROM size INCORRECT. MUST be within 61184 BYTES. STOP"); if(ROM.Length != 4096) // Pads ROM to be 4096 bytes, if size is not exact { // This would not be done on a physical implementation of SYSTEM, but I feel like being kind to the lazy this.RAM = ROM; ROM = new byte[4096]; for(int i = 0x000; i < RAM.Length; i++) ROM[i] = this.RAM[i]; } if(PRG.Length != 32768) // Pads PRG to be 61184 bytes, if size is not exact { // again, being nice to lazy people.. this.RAM = PRG; PRG = new byte[32768]; for(int i = 0x000; i < RAM.Length; i++) PRG[i] = RAM[i]; } this.RAM = new byte[0x10000]; // 64K of memory, the max supported // Initialize all bytes in the stack, to 0xFF for (int i = 0; i < 0x100; i++) this.RAM[i] = 0xFF; // This is redundant, but desired, for my own undisclosed reasons. // LOAD PROGRAM ROM AND BIOS ROM INTO MEMORY for (int i = 0xf000; i < 0x10000; i++) // LOAD BIOS ROM INTO MEMORY { this.RAM[i] = ROM[i - 0xf000]; // yeah, pretty easy actually } // Remember, 0x0100-0x80FF is for PROGRAM ROM for (int i = 0x0100; i < 0x8100; i++) // LOAD PROGRAM ROM INTO MEMORY { this.RAM[i] = PRG[i - 0x100]; // not that you knew it would be much different } // The rest, 0x8100-0xEFFF, is reserved for now (the programmer can use it freely, as well as where PRG is loaded). // still read/writeable though return; } // READ/WRITE: // NOTE: SYSTEM's cpu is LITTLE ENDIAN // WHEN DOUBLE-READING, THE BYTE-ORDER IS CONVERTED TO BIG ENDIAN // WHEN DOUBLE-WRITING, THE BYTE TO WRITE IS BIG ENDIAN, AND CONVERTED TO LITTLE ENDIAN // CPU HAS MAR/MBR, but the MEMORY CONTROLLER has ITS OWN REGISTERS for this? // SINGLE OPERATIONS public byte read_single(ref cpu_registers registers, ushort address) // READ A SINGLE BYTE { // reading from any memory location is allowed, so this is simple registers.memoryAddress = address; return registers.memoryBuffer8 = this.RAM[registers.memoryAddress]; } public ushort read_double(ref cpu_registers registers, ushort address) // READ TWO BYTES (converted to BIG ENDIAN byte order) { ushort ret = this.RAM[++address]; ret <<= 8; ret |= this.RAM[--address]; registers.memoryAddress = address; registers.memoryBuffer16 = ret; return registers.memoryBuffer16; } public void write_single(ref cpu_registers registers, ushort address, byte mbr_single) // WRITE A SINGLE BYTE { if (address < 0x0100) return; // block write to the stack (0x0000-0x00FF) if (address > 0xEFFF) return; // block writes to ROM area (0xF000-0xFFFF) registers.memoryAddress = address; registers.memoryBuffer8 = mbr_single; this.RAM[registers.memoryAddress] = registers.memoryBuffer8; return; } public void write_double(ref cpu_registers registers, ushort address, ushort mbr_double) // WRITE TWO BYTES (converted to LITTLE ENDIAN ORDER) { // writes to stack are blocked (0x0000-0x00FF) // writes to ROM are blocked (0xF000-0xFFFF) write_single(ref registers, ++address, (byte)(mbr_double >> 8)); write_single(ref registers, --address, (byte)(mbr_double & 0xff)); registers.memoryBuffer16 = mbr_double; return; } public byte pop_single(ref cpu_registers registers) // POP ONE BYTE OFF STACK { return read_single(ref registers, registers.stackPointer++); } public ushort pop_double(ref cpu_registers registers) // POP TWO BYTES OFF STACK { ushort tmp = registers.stackPointer++; ++registers.stackPointer; return read_double(ref registers, tmp); } // PUSH isn't as easy, since we can't use write_single() or write_double() // because those are for external writes and they block writes to the stack // external writes to the stack are possible of course, but // these are done here through push_single() and push_double() public void push_single(ref cpu_registers registers, byte VALUE) // PUSH ONE BYTE { registers.memoryAddress = --registers.stackPointer; registers.memoryBuffer8 = VALUE; this.RAM[registers.memoryAddress] = registers.memoryBuffer8; return; } public void push_double(ref cpu_registers registers, ushort VALUE) // PUSH TWO BYTES { this.RAM[--registers.stackPointer] = (byte)(VALUE >> 8); this.RAM[--registers.stackPointer] = (byte)(VALUE & 0xff); registers.memoryAddress = registers.stackPointer; registers.memoryBuffer16 = VALUE; return; } } } using System; namespace SYSTEM.cpu { // Contains the class for handling registers. Quite simple really. class cpu_registers { private byte sp, cop; // stack pointer, current opcode // private ushort pp, ip, // program pointer, interrupt pointer mar, mbr_hybrid; // memory address and memory buffer registers, // store address being operated on, store data being read/written // mbr is essentially the data bus; as said, it supports both 16 and 8 bit operation. // There are properties in this class for handling mbr in 16-bit or 8-bit capacity, accordingly // NOTE: Paged memory can be used, but this is handled by opcodes, otherwise the memory addressing // is absolute // NOTE: sp is also an address bus, but used on the stack (0x0000-0x00ff) only // when pushing to the stack, or pulling, mbr gets updated in 8-bit capacity // For pulling 16-bit word from stack, shifting register 8 left is needed, otherwise the next // POP operation will override the result of the last // Alpha is accumulator, the rest are general purpose public ushort alphaX, bravoX, charlieX, deltaX; public cpu_registers() { sp = 0xFF; // stack; push left, pop right // stack is from 0x0000-0x00ff in memory pp = 0xf000; // execution starts from 0xf000; ROM is loaded // from 0xf000-0xffff, so 4KB of ROM. // 0xf000-0xffff cannot be written to in software; though this disable // self-modifying code, effectively. ip = pp; // interrupt pointer starts from the same place as pp alphaX = bravoX = charlieX = deltaX = 0xffff; cop = 0x00; // whatever opcode 0x00 is, cop is that on init mar = mbr_hybrid = 0x0000; return; } // Registers: public ushort memoryAddress // no restrictions on read/write, but obviously it needs to be handled with care for this register { // This should ONLY be handled by the execution unit, when actually loading instructions from memory set { mar = value; } get { return mar; } } // NOTE: 8-bit and 16-bit address bus are shared, but address bus must have all bits written. // when writing 8-bit value, byte-signal gets split. Like how an audio/video splitter works. public byte memoryBuffer8 // treats address bus as 8-bit, load one byte { set { // byte is loaded into both low and high byte in mbr (i.e. it is split to create duplicates, for a 16-bit signal) mbr_hybrid &= 0x0000; mbr_hybrid |= (ushort)value; mbr_hybrid <<= 0x08; mbr_hybrid |= (ushort)value; } get { return (byte)mbr_hybrid; } } public ushort memoryBuffer16 // treats address bus as 16-bit, load two bytes { set { mbr_hybrid &= 0x0000; mbr_hybrid |= value; } get { return mbr_hybrid; } } public byte stackPointer // sp is writable, but only push/pull opcodes { // should be able to write to it. There SHOULD set { sp = value; } // be opcodes for reading from it get { return sp; } } public byte currentOpcode { set { cop = value; } get { return cop; } } public ushort programPointer // says where an instruction is being executed from { set { pp = value; } get { return pp; } } public ushort interruptPointer // says where the next requested interrupt should begin { // (copied into PP, after pushing relevant registers) set { ip = value; } get { return ip; } } public byte status(cpu_flags flags) // status word, containing all flags { byte ret = 0; if (flags.negative) ret |= 0x80; if (flags.overflow) ret |= 0x40; if (flags.brk) ret |= 0x10; if (flags.irq) ret |= 0x04; if (flags.zero) ret |= 0x02; if (flags.carry) ret |= 0x01; return ret; } } } using System; using System.Collections.Generic; namespace SYSTEM.cpu { class cpu_execution { public core processor; // the "core", detailing the CPU status, including memory, memory controller, etc public cpu_microcode microcode; // the microcode unit (note, microcode is plug and play, you could use something else here) public cpu_execution(byte[] ROM, byte[] PRG) // initialize execution unit and everything under it { processor = new core(ROM, PRG); microcode = new cpu_microcode(); return; } public void fetch() // fetch current instruction { processor.registers.currentOpcode = processor.memory.read_single(ref processor.registers, processor.registers.programPointer); return; } public void execute() // execute current instruction { processor = microcode.use(processor); return; } } }
模拟操作码的microcode.cs不包括在这里,因为它是2600行代码.
所有这些都是C#.
我建议查阅"计算系统元素 "一书.在阅读本书的过程中,您将从基本逻辑门开始构建虚拟计算机.当您完成本书时,您将拥有一个基本的操作系统,编译器等
.源代码可在线获得,它还在Java之上实现了计算机的体系结构.
一个常见的练习是建立一个简单的计算器.它只有有限数量的操作(通常为4 * / + -
),一种数据类型(数字),您可能非常了解它应该如何工作.这使调试变得更容易.
尽管简单,但您必须处理一些基本的VM问题.您需要解析一系列命令,存储您正在处理的多个对象,并处理输出.
巧合的是,计算器IC是CPU的先驱,因此从历史的角度来看,这种方法也很有意义.