#include #include #include #include #include #include #include #include #include "dcpu16.h" /* * emulates the DCPU16 system from http://0x10c.com/doc/dcpu-16.txt * * I couldn't remember ever implementing an emulator before, so this * happened. As such, consider this a toy in progress. * There are likely many improvable aspects. * * Justin Wind * 2012 04 05 - implementation started * 2012 04 06 - first functionality achieved * 2012 04 09 - minor cleanups * 2012 04 10 - moved cli to separate module * 2012 04 12 - added basic callback support for address accesses * * TODO * change api to print into buffers rather than stdio * drop checks for assigning to literals -- it won't affect anything anyhow * refactor opcode functiontables into switch statements */ static const char * const src_id_ = "$Id$"; #define OPCODE_BASIC_BITS 4 #define OPCODE_OPERAND_BITS 6 static const char regnames_[] = "ABCXYZIJ"; /* some default warning and debug reporting functions, which can be overridden by clients */ #define WARN(...) do { if (warn_cb_) warn_cb_(__VA_ARGS__); } while (0) static inline void warn_(char *fmt, ...) __attribute__((format(printf, 1, 2))); static inline void warn_(char *fmt, ...) { va_list ap; fprintf(stderr, "[warning] "); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fprintf(stderr, "\n"); fflush(stderr); } static void (*warn_cb_)(char *fmt, ...) = warn_; void dcpu16_warn_cb_set(void (*fn)(char *fmt, ...)) { warn_cb_ = fn; } #ifdef DEBUG #define TRACE(...) do { if (trace_cb_) trace_cb_(__VA_ARGS__); } while (0) static inline void trace_(char *fmt, ...) __attribute__((format(printf, 1, 2))); static inline void trace_(char *fmt, ...) { va_list ap; fprintf(stdout, "[debug] "); va_start(ap, fmt); vfprintf(stdout, fmt, ap); va_end(ap); fprintf(stdout, "\n"); fflush(stdout); } #else /* DEBUG */ #define TRACE(...) do {} while(0) #endif /* DEBUG */ static void (*trace_cb_)(char *fmt, ...) = #ifdef DEBUG trace_ #else /* DEBUG */ NULL #endif ; void dcpu16_trace_cb_set(void (*fn)(char *fmt, ...)) { trace_cb_ = fn; } /* acct_event_ * invokes callbacks for specified event */ static inline void acct_event_(struct dcpu16 *vm, dcpu16_acct_event_ ev, DCPU16_WORD addr) { struct dcpu16_acct_cb *cb = vm->cb_table_; size_t i; for (i = 0; i < vm->cb_table_entries_; i++) { if ( (cb[i].mask & ev) ) cb[i].fn(ev, addr); } } /* value_decode_ * sets *v to be the destination of the value * advances d->pc if necessary * workv is buffer to use to accumulate literal value before use, one exists for either potential instruction operand * e_addr is for accounting callback * returns true if destination points to literal (id est *v should ignore writes) */ static unsigned int value_decode_(struct dcpu16 *d, DCPU16_WORD value, DCPU16_WORD *work_v, DCPU16_WORD **v, DCPU16_WORD *e_addr) { DCPU16_WORD nextword; unsigned int retval = 0; assert(value <= 0x3f); /* does this value indicate a literal */ if (value >= 0x1f) retval = 1; if (value <= 0x07) { /* register */ *v = d->reg + value; TRACE(">> %c (0x%04x)", regnames_[value], **v); } else if (value <= 0x0f) { /* [register] */ *v = &(d->ram[ d->reg[(value & 0x07)] ]); TRACE(">> [%c] [0x%04x] (0x%04x)", regnames_[value&0x07], d->reg[value&0x07], **v); *e_addr = d->reg[(value & 0x07)]; } else if (value <= 0x17) { /* [next word + register] */ nextword = d->ram[ d->pc++ ]; d->cycle++; *v = &(d->ram[ nextword + d->reg[(value & 0x07)] ]); TRACE(">> [nextword + %c] [0x%04x + 0x%04x] (0x%04x)", regnames_[(value & 0x07)], nextword, d->reg[(value & 0x07)], **v); *e_addr = nextword + d->reg[(value & 0x07)]; } else switch (value) { case 0x18: /* POP / [sp++] */ *v = &(d->ram[ d->sp++ ]); TRACE(">> POP [0x%04x] (0x%04x)", d->sp - 1, **v); *e_addr = d->sp - 1; break; case 0x19: /* PEEK / [sp] */ *v = &(d->ram[ d->sp ]); TRACE(">> PEEK [0x%04x] (0x%04x)", d->sp, **v); *e_addr = d->sp; break; case 0x1a: /* PUSH / [--sp] */ *v = &(d->ram[ --d->sp ]); TRACE(">> PUSH [0x%04x] (0x%04x)", d->sp + 1, **v); *e_addr = d->sp + 1; break; case 0x1b: /* SP */ *v = &(d->sp); TRACE(">> SP (0x%04x)", **v); break; case 0x1c: /* PC */ *v = &(d->pc); TRACE(">> PC (0x%04x)", **v); break; case 0x1d: /* O */ *v = &(d->o); TRACE(">> O (0x%04x)", **v); break; case 0x1e: /* [next word] / [[pc++]] */ nextword = d->ram[ d->pc++ ]; d->cycle++; *v = &(d->ram[ nextword ]); TRACE(">> [nextword] [0x%04x] (0x%04x)", nextword, **v); *e_addr = nextword; break; case 0x1f: /* next word (literal) / [pc++] */ nextword = d->ram[ d->pc++ ]; d->cycle++; *work_v = nextword; *v = work_v; TRACE(">> nextword (0x%04x)", **v); break; default: /* 0x20-0x3f: literal values 0x00-0x1f */ *work_v = value & 0x1f; *v = work_v; TRACE(">> literal (0x%04x)", **v); } return retval; } #define OPCODE_NAME_LEN 16 struct opcode_entry { unsigned short value; char name[OPCODE_NAME_LEN]; void (*impl)(struct dcpu16 *, DCPU16_WORD, DCPU16_WORD); }; /* messy boilerplate for opcode handlers */ #define OP_IMPL(x) static void op_##x(struct dcpu16 *d, DCPU16_WORD val_a, DCPU16_WORD val_b) #define OP_TYPE(op_type) DCPU16_WORD *a, *b;\ unsigned int lit_a;\ DCPU16_WORD ev_a_addr = 0, ev_b_addr = 0;\ do {\ lit_a = value_decode_(d, val_a, &d->reg_work_[0], &a, &ev_a_addr);\ op_type;\ } while (0) #define OP_NBI_ (void)val_b, (void)b, (void)ev_b_addr #define OP_BASIC_ (void)value_decode_(d, val_b, &d->reg_work_[0], &b, &ev_b_addr) #define OP_BASIC(x) OP_TYPE(OP_BASIC_) #define OP_NBI(x) OP_TYPE(OP_NBI_) /* accounting helpers, these fire off the related callbacks for memory reads, memory writes, and execution of reserved instructions */ #define ACCT_R(addr) do { acct_event_(d, DCPU16_ACCT_EV_READ, addr); } while (0) #define ACCT_W(addr) do { acct_event_(d, DCPU16_ACCT_EV_WRITE, addr); } while (0) #define ACCT_ILL(addr) do { acct_event_(d, DCPU16_ACCT_EV_NOP, addr); } while (0) /* extended opcodes */ /* N.B. this next function currently decodes values -- id est, it is an opcode processing terminus; however, if 'future instruction set expansion' happens, this will probably need to behave more like the OP_IMPL(_nbi_) function which invoked it, if those instructions have zero or differently styled operands. */ OP_IMPL(nbi__reserved_) { OP_NBI(nbi__reserved_); /* reserved for future expansion */ DCPU16_WORD future_opcode = (d->ram[d->pc] >> (OPCODE_BASIC_BITS + OPCODE_OPERAND_BITS)); WARN("reserved future opcode 0x%04x invoked", future_opcode); ACCT_ILL(d->pc); } OP_IMPL(nbi_jsr) { OP_NBI(nbi_jsr); /* pushes the address of the next instruction to the stack, then sets PC to a */ ACCT_R(ev_a_addr); d->ram[ --d->sp ] = d->pc; d->pc = *a; d->cycle += 2; ACCT_W(d->sp + 1); } OP_IMPL(nbi__reserved2_) { OP_NBI(nbi__reserved2_); /* reserved */ WARN("reserved nbi opcode invoked"); ACCT_ILL(d->pc); } static const struct opcode_entry opcode_nbi_entries[] = { {0x0, "(reserved)", op_nbi__reserved_}, {0x1, "JSR", op_nbi_jsr}, {0x2, "(reserved)", op_nbi__reserved2_}, {0x0, "", NULL} }; #define OPCODE_NBI_MAX (((sizeof(opcode_nbi_entries)) / (sizeof(struct opcode_entry))) - 1) /* basic opcodes */ /* N.B. the following function does not decode values, as the nbi instructions only have one operand. */ OP_IMPL(_nbi_) { /* non-basic instruction */ /* don't do normal value decoding here */ DCPU16_WORD nbi_opcode = val_a; const struct opcode_entry *e = opcode_nbi_entries; e = opcode_nbi_entries + ( (nbi_opcode < OPCODE_NBI_MAX) ? nbi_opcode : (OPCODE_NBI_MAX - 1) ); assert(e->impl != NULL); TRACE(">> %s 0x%04x", e->name, val_b); e->impl(d, val_b, 0); } OP_IMPL(set) { OP_BASIC(set); /* sets a to b */ ACCT_R(ev_b_addr); ACCT_W(ev_a_addr); /* only set non-literal target */ if (!lit_a) { *a = *b; } d->cycle += 1; } OP_IMPL(add) { OP_BASIC(add); /* sets a to a+b, sets O to 0x0001 if there's an overflow, 0x0 otherwise */ unsigned int acc = *a + *b; ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (!lit_a) { *a = acc; } d->o = (acc > 0xffff); d->cycle += 2; ACCT_W(ev_a_addr); } OP_IMPL(sub) { OP_BASIC(sub); /* sets a to a-b, sets O to 0xffff if there's an underflow, 0x0 otherwise */ unsigned int acc = *a - *b; ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (!lit_a) { *a = acc; } d->o = (acc > 0xffff); d->cycle += 2; ACCT_W(ev_a_addr); } OP_IMPL(mul) { OP_BASIC(mul); /* sets a to a*b, sets O to ((a*b)>>16)&0xffff */ unsigned int acc = *a * *b; ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (!lit_a) { *a = acc; } d->o = acc >> 16; d->cycle += 2; ACCT_W(ev_a_addr); } OP_IMPL(div) { OP_BASIC(div); /* sets a to a/b, sets O to ((a<<16)/b)&0xffff. if b==0, sets a and O to 0 instead. */ ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (*b == 0) { if (!lit_a) { *a = 0; } d->o = 0; } else { unsigned int acc = *a / *b; if (!lit_a) { *a = acc; } acc = (*a << 16) / *b; d->o = acc; } d->cycle += 3; ACCT_W(ev_a_addr); } OP_IMPL(mod) { OP_BASIC(mod); /* sets a to a%b. if b==0, sets a to 0 instead. */ ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (*b == 0) { if (!lit_a) { *a = 0; } } else { if (!lit_a) { *a = *a % *b; } } d->cycle += 3; ACCT_W(ev_a_addr); } OP_IMPL(shl) { OP_BASIC(shl); /* sets a to a<>16)&0xffff */ unsigned int acc = *a << *b; ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (!lit_a) { *a = acc; } d->o = acc >> 16; d->cycle += 2; ACCT_W(ev_a_addr); } OP_IMPL(shr) { OP_BASIC(shr); /* sets a to a>>b, sets O to ((a<<16)>>b)&0xffff */ unsigned int acc = *a >> *b; ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (!lit_a) { *a = acc; } d->o = (*a << 16) >> *b; d->cycle += 2; ACCT_W(ev_a_addr); } OP_IMPL(and) { OP_BASIC(and); /* sets a to a&b */ ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (!lit_a) { *a = *a & *b; } d->cycle += 1; ACCT_W(ev_a_addr); } OP_IMPL(bor) { OP_BASIC(bor); /* sets a to a|b */ ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (!lit_a) { *a = *a | *b; } d->cycle += 1; ACCT_W(ev_a_addr); } OP_IMPL(xor) { OP_BASIC(xor); /* sets a to a^b */ ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (!lit_a) { *a = *a ^ *b; } ACCT_W(ev_a_addr); d->cycle += 1; } OP_IMPL(ife) { OP_BASIC(ife); /* performs next instruction only if a==b */ ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (*a == *b) { /* */ } else { d->skip_ = 1; d->cycle++; } d->cycle += 2; } OP_IMPL(ifn) { OP_BASIC(ifn); /* performs next instruction only if a!=b */ ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (*a != *b) { /* */ } else { d->skip_ = 1; d->cycle++; } d->cycle += 2; } OP_IMPL(ifg) { OP_BASIC(ifg); /* performs next instruction only if a>b */ ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if (*a > *b) { /* */ } else { d->skip_ = 1; d->cycle++; } d->cycle += 2; } OP_IMPL(ifb) { OP_BASIC(ifb); /* performs next instruction only if (a&b)!=0 */ ACCT_R(ev_b_addr); ACCT_R(ev_a_addr); if ((*a & *b) != 0) { /* */ } else { d->skip_ = 1; d->cycle++; } d->cycle += 2; } static const struct opcode_entry opcode_basic_entries[] = { {0x0, "(nbi)", op__nbi_}, {0x1, "SET", op_set }, {0x2, "ADD", op_add }, {0x3, "SUB", op_sub }, {0x4, "MUL", op_mul }, {0x5, "DIV", op_div }, {0x6, "MOD", op_mod }, {0x7, "SHL", op_shl }, {0x8, "SHR", op_shr }, {0x9, "AND", op_and }, {0xa, "BOR", op_bor }, {0xb, "XOR", op_xor }, {0xc, "IFE", op_ife }, {0xd, "IFN", op_ifn }, {0xe, "IFG", op_ifg }, {0xf, "IFB", op_ifb }, {0x0, "", NULL } }; static inline void dump_operand_value_(DCPU16_WORD value, DCPU16_WORD nextword) { if (value <= 0x07) { printf(" %c", regnames_[value]); } else if (value <= 0x0f) { printf(" [%c]", regnames_[value & 0x07]); } else if (value <= 0x17) { printf(" [0x%04x + %c]", nextword, regnames_[value & 0x07]); } else switch (value) { case 0x18: printf(" POP"); break; case 0x19: printf(" PEEK"); break; case 0x1a: printf(" PUSH"); break; case 0x1b: printf(" SP"); break; case 0x1c: printf(" PC"); break; case 0x1d: printf(" O"); break; case 0x1e: printf(" [0x%04x]", nextword); break; case 0x1f: printf(" 0x%04x", nextword); break; default: printf(" 0x%02x", value - 0x20); } } /* split a word into the parts of an instruction, and determine how many words it takes up in total */ static inline void instruction_decode_(struct dcpu16 *d, DCPU16_WORD addr, DCPU16_WORD *opcode, DCPU16_WORD *a, DCPU16_WORD *b, DCPU16_WORD *instr_len) { *opcode = d->ram[addr] & ((1 << OPCODE_BASIC_BITS) - 1); *a = (d->ram[addr] >> OPCODE_BASIC_BITS) & ((1 << OPCODE_OPERAND_BITS) - 1); *b = (d->ram[addr] >> (OPCODE_BASIC_BITS + OPCODE_OPERAND_BITS)) & ((1 << OPCODE_OPERAND_BITS) - 1); if (instr_len) { *instr_len = 1; /* both basic and nbi opcodes use their b operand */ if ( (*b >= 0x10 && *b <= 0x17) || *b == 0x1e || *b == 0x1f ) *instr_len += 1; /* but only basic uses a */ if (*opcode && ((*a >= 0x10 && *a <= 0x17) || *a == 0x1e || *a == 0x1f) ) *instr_len += 1; } } /* dcpu16_disassemble_print print the words of the instruction at addr, followed by its assembly representation returns the length of the instruction in words */ DCPU16_WORD dcpu16_disassemble_print(struct dcpu16 *d, DCPU16_WORD addr) { DCPU16_WORD opcode, a, b, instr_len, i; const struct opcode_entry *e; unsigned int indent = 0; unsigned int partial = 0; if (!d) return 0; /* Check the previous instruction, to see if this one should be indented. This check isn't foolproof, as preceeding addresses could be data which happen to match instructions.. */ for (i = 3; i; i--) { instruction_decode_(d, addr - i, &opcode, &a, &b, &instr_len); if (instr_len > i) partial++; if (instr_len == i && opcode >= 0xc) { indent++; break; } } /* now get what we're really interested in */ instruction_decode_(d, addr, &opcode, &a, &b, &instr_len); if (opcode) e = opcode_basic_entries + opcode; else e = opcode_nbi_entries + ( (a < OPCODE_NBI_MAX) ? a : (OPCODE_NBI_MAX - 1) ); /* show the raw words */ printf("%04x", d->ram[addr]); for (i = 1; i < instr_len; i++) { printf(" %04x", d->ram[addr + i]); } /* align things neatly, show the instruction */ printf("%s%s ;%s%s%s", instr_len < 3 ? " " : "", instr_len < 2 ? " " : "", partial ? "*" : " ", indent ? " " : "", e->name); /* show the operands */ i = 0; if (opcode) { dump_operand_value_(a, d->ram[addr + 1]); if ((a >= 0x10 && a <= 0x17) || a == 0x1e || a == 0x1f) addr++; printf(","); } if (opcode || a) dump_operand_value_(b, d->ram[addr + 1]); return instr_len; } /* execute the next instruction */ void dcpu16_step(struct dcpu16 *d) { DCPU16_WORD opcode, a, b, instr_len; const struct opcode_entry *e; if (!d) return; /* PC is advanced while decoding the operands by the opcode functions. Things like this could be organized a little better.. */ instruction_decode_(d, d->pc, &opcode, &a, &b, NULL); d->pc++; /* all instructions take at least one word */ for (e = opcode_basic_entries; e->impl; e++) { if (e->value == opcode) { TRACE(">> %s 0x%04x, 0x%04x", e->name, a, b); e->impl(d, a, b); break; } } /* and jump over next instr if needed */ if (d->skip_) { instruction_decode_(d, d->pc, &opcode, &a, &b, &instr_len); d->pc += instr_len; d->skip_ = 0; TRACE("++ SKIPPED %x words", instr_len); } } /* print the current state of the machine shows current cycle count, registers, and next instruction */ void dcpu16_state_print(struct dcpu16 *d) { unsigned int i; if (!d) return; printf(" "); for (i = 0; i < 8; i++) printf(" %c:0x%04x", regnames_[i], d->reg[i]); printf("\n"); printf("(0x%08llx) %2s:0x%04x %2s:0x%04x %2s:0x%04x [%2s]:", d->cycle, "O", d->o, "SP", d->sp, "PC", d->pc, "PC"); dcpu16_disassemble_print(d, d->pc); printf("\n"); } /* dcpu16_dump_ram * print raw ram contents from start to stop */ void dcpu16_dump_ram(struct dcpu16 *d, DCPU16_WORD start, DCPU16_WORD end) { unsigned int i, j; const unsigned int n = 8; /* words per line */ if (!d) return; for (i = start, j = 0; i <= end; i++, j++) { if (j % n == 0) printf("0x%04x:\t", i); printf(" %04x%s", d->ram[i], (j % n) == (n - 1) ? "\n" : ""); } if ((j % n) != (n - 1)) printf("\n"); } /* dcpu16_acct_add * Register callback fn to be triggered whenever event matching any events * in bitwise mask occur. */ int dcpu16_acct_add(struct dcpu16 *vm, dcpu16_acct_event_ mask, void (*fn)(dcpu16_acct_event_, DCPU16_WORD)) { struct dcpu16_acct_cb cb; cb.mask = mask; cb.fn = fn; if (vm->cb_table_entries_ == vm->cb_table_allocated_) { size_t new_entries = vm->cb_table_allocated_ + 32; void *tmp_ptr = realloc(vm->cb_table_, new_entries * sizeof *(vm->cb_table_)); if (tmp_ptr == NULL) { fprintf(stderr, "%s():%s", "realloc", strerror(errno)); return -1; } vm->cb_table_ = tmp_ptr; vm->cb_table_allocated_ += 32; } memcpy(vm->cb_table_ + vm->cb_table_entries_, &cb, sizeof cb); vm->cb_table_entries_++; return 0; } /* dcpu16_reset * resets a dcpu16 instance to initial state */ void dcpu16_reset(struct dcpu16 *d) { if (!d) return; memset(d, 0, sizeof *d); } /* dcpu16_new * allocate a new dcpu16 instance */ struct dcpu16 *dcpu16_new(void) { struct dcpu16 *vm; vm = calloc(1, sizeof *vm); if (vm == NULL) WARN("%s: %s(%zu): %s", __func__, "calloc", strerror(errno)); return vm; } /* dcpu16_delete * release a dcpu16 instance */ void dcpu16_delete(struct dcpu16 **vm) { if (!vm || !*vm) return; free(*vm); *vm = NULL; }