私は仮想マシンのプログラミングに興味があります.virtualboxやvmwareほど派手なものではありませんが、ciscであろうとriscであろうと、Zilog、SPARC、MIPS、または80686アーキテクチャモデルなどの単純なアーキテクチャをエミュレートできるものです。
これを行うことで、同じ種類のエミュレーターを作成するのは比較的簡単になると思います。これを他の何よりも経験のために使用することに興味があります (私の最初の C プロジェクトなので、他に何か)。
私は仮想マシンのプログラミングに興味があります.virtualboxやvmwareほど派手なものではありませんが、ciscであろうとriscであろうと、Zilog、SPARC、MIPS、または80686アーキテクチャモデルなどの単純なアーキテクチャをエミュレートできるものです。
これを行うことで、同じ種類のエミュレーターを作成するのは比較的簡単になると思います。これを他の何よりも経験のために使用することに興味があります (私の最初の C プロジェクトなので、他に何か)。
特定のタイプのアプリケーションに関する情報を取得する良い方法 (およびあなたの場合、c のイディオムを取得する良い方法) は、同じタイプのオープン ソース プロジェクトの構造と詳細を調べることです。自分自身のプロジェクトをゼロから開始するために、単にのぞいて、簡単に確認してから「忘れる」ことにする人もいるかもしれませんが、すべての場合において、この種の訪問は有益です。
「シンプルなアーキテクチャ」と Zilog についておっしゃっていたので、Z80 プロセッサがぴったりだと思いました。さまざまな理由から、Z80 エミュレーターのジャンルには多くの現在および過去のプロジェクトがあります。ところで、理由の 1 つは、多くの古いスロット タイプのビデオ コンソールが Z80 で実行されていたため、ノスタルジックなゲーマーが古いお気に入りを実行するエミュレーターを作成するよう促したことです ;-)
そのようなプロジェクトの例は、完全な Z80 エミュレーターとC/PMの両方を含むYAZE-AGです。全体が C で書かれています。また、比較的成熟しており (バージョン 2.x)、アクティブです。これは非常に小さなチーム (おそらく 1 つの ;-)) の作業だと思います。
幸運を!
CPUを設計してエミュレートするなら、
コアを準備します。つまり、レジスタのクラスを作成します。フラグ用に 1 つ書き込みます。メモリ コントローラを記述します。
オペコードの種類について考えてみましょう。また、単語の長さはどれくらいですか?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# です。
本Elements of Computing Systems をチェックすることをお勧めします。この本を進めながら、基本的な論理ゲートから仮想コンピューターを構築します。この本を読み終える頃には、基本的なオペレーティング システム、コンパイラなど
を入手できます。オンラインで入手できるソース コードも、Java の上にコンピュータのアーキテクチャを実装しています。
一般的な演習は、簡単な電卓を作成することです。限られた数の操作 (通常は 4つ) と 1 つのデータ型 (数字) しかありませ* / + -
ん。おそらく、どのように機能するかについて十分に理解しているでしょう。これにより、デバッグがはるかに簡単になります。
単純であるにもかかわらず、いくつかの基本的な VM の問題に対処する必要があります。コマンドのシーケンスを解析し、作業中の複数のオブジェクトを保存し、出力を処理する必要があります。
偶然にも、電卓 IC は CPU の前身であるため、このアプローチは歴史的な観点からも理にかなっています。
いくつかの考え:
他のプログラミング言語の知識がなく、アセンブラーを十分に理解していない限り、これはかなり挑戦的な最初の C プロジェクトです。それでも、頑張ってください!
実際の Z-80 マシンで実行されたソフトウェアを見つけて、それを最終テストとして使用できる可能性があるため、Zilog 時代のものがよいでしょう。
私が最初に書いた実際のプログラム (1 ページ クラスの割り当て以外) は、高校で使用していた HP2100A ミニコンピューターのエミュレーターでした。私は C の前身である B でそれを書きましたが、これが最初の C プログラムにとって難しすぎるとは思いません。どちらかといえば、単純すぎるかもしれません。もちろん、80686 のようなものは Z-80 よりもはるかに困難ですが、QEMU、VirtualBox などによって既に行われています。
これの最も難しい部分は、マシンを外部世界に接続する割り込みシステム全体です。
LLVM について調べて、本当に VM とエミュレーターのどちらを作成するかを決定することをお勧めします。
これは製品の推奨ではなく、観察です...
まず、デイテルとデイテルの本を手に入れます。(Cで実行する場合は、おそらくこれです)教えている言語に関係なく、仮想マシンの作成に関する1つの章と、仮想マシンのアセンブラーコードを作成するための手順が常に含まれているようです。
編集-追加
(あなたが何を書きたいのか誤解している場合に備えて、購入する前に図書館でチェックしますが)