Good day.
For a long time there was a desire to write an emulator of some kind of processor.
And what could be better than inventing a bicycle?
The name of the bike - V16, from gluing the word Virtual and, in fact, bit.
And you need to start, of course, with a description of the processor.
At the very beginning, I planned to write the DCPU-16 emulator, but there are more than enough such wonders on the Internet, so I decided to stop only at "licking" the most basic from DCPU-16 1.1.
IN b, a
AND OUT b, a
.V16 has two sets of general-purpose registers: main and alternative.
The processor can work with only one set, so you can switch between sets using the XCR
instruction.
All instructions have a maximum length of three words and are fully defined first.
The first word is divided into three values: low byte - opcode, high byte in the form of two 4-bit values - the description of the operands.
Interrupts here are nothing more than a table with addresses to which the processor duplicates the CALL
instruction. If the value of the address is zero, then the interrupt does nothing, just reset the HF flag.
Value range | Description |
---|---|
0x0 ... 0x3 | Register as value |
0x4 ... 0x7 | Register as value by address |
0x8 ... 0xB | Register + constant as value at |
0xC | Constant as value at |
0xD | Constant as value |
0xE | Register RIP as read-only value |
0xF | Register RSP as value |
An example of pseudocode and words in which all this should be translated:
MOV RAX, 0xABCD ; 350D ABCD MOV [RAX], 0x1234 ; 354D 1234
V16 can execute one instruction in 1, 2 or 3 cycles. Each access to RAM is one separate cycle. The instruction is not a beat!
A set of registers. There are only four registers, but the situation is improved by the fact that there are two such sets in the processor. Switching takes place using the XCR
instruction.
typedef struct Regs { uint16_t rax, rbx; //Primary Accumulator, Base Register uint16_t rcx, rdx; //Counter Register, Data Register } regs_t;
Flags. Unlike DCPU-16, V16 has conditional jumps, subroutine calls and returns from there. At the moment, the processor has 8 flags, 5 of which are condition flags.
// , stdbool.h typedef struct Flags { bool IF, IR, HF; bool CF, ZF; bool EF, GF, LF; } flags_t;
Actually, the processor itself. It also describes the table of interrupt addresses, which can be called descriptors and find another reference to 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;
Operand. When receiving values, we need to first read, then change, and then write the value to where we got it from.
typedef struct Opd { uint8_t code : 4; uint16_t value; uint16_t nextw; } opd_t;
When all structures are described, the need arises for functions that will give these structures the magic power of the extinguished code.
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 *); // ,
Also, I did not mention a large listing with opcodes, but this is not necessary and necessary only to understand what is happening in this whole mess.
There are also calls to static functions that are only intended to be called from 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; }
In trying to find the answer to this question, I rewrote the emulator from C to C ++ five times and back.
However, the main goals can be identified now:
I hope that someone this "article" will be useful, someone pushes to write something like that.
My kulichiki can be viewed on github .
Also, oh, horror, I have an assembler for the old version of this emulator (No, don't even try, the emulator will at least complain about the wrong ROM format)
Source: https://habr.com/ru/post/457144/