From 9aa02bebf295ce9436451e0ce85db7717a6c9f81 Mon Sep 17 00:00:00 2001 From: David Phillips Date: Sun, 4 Aug 2019 00:13:59 +1200 Subject: Add initial emulator implementation This emulator provides a rough way for binaries designed for this CPU to be executed in a virtual/emulated CPU for testing purposes. This patch also adds a small test setup for automated assembly, execution and checking of register postconditions for programs. --- .gitignore | 1 + Makefile | 5 +- debug.h | 10 ++ emulator.c | 293 ++++++++++++++++++++++++++++++++++ input/input_bin.c | 1 + input/input_bin.h | 1 + instruction.h | 3 +- test/Makefile | 1 + test/emul/002-nop.asm | 7 + test/emul/003-small-loop.asm | 10 ++ test/emul/run-emul.sh | 68 ++++++++ test/full-pipeline/005-small-loop.asm | 3 +- 12 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 debug.h create mode 100644 emulator.c create mode 100644 test/emul/002-nop.asm create mode 100644 test/emul/003-small-loop.asm create mode 100755 test/emul/run-emul.sh diff --git a/.gitignore b/.gitignore index 1e1fca8..523d976 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ *.bin assembler disassembler +emulator asmcat bincat diff --git a/Makefile b/Makefile index b6d1e6d..99d04ae 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ -EXECUTABLES = assembler disassembler asmcat bincat +EXECUTABLES = assembler disassembler emulator asmcat bincat ASM_OBJECTS = assembler.o lex.o parse.o output/output_bin.o util.o DISASM_OBJECTS = disassembler.o input/input_bin.o output/output_asm.o util.o +EMUL_OBJECTS = input/input_bin.o util.o ASMCAT_OBJECTS = asmcat.o lex.o parse.o output/output_asm.o util.o BINCAT_OBJECTS = bincat.o input/input_bin.o output/output_bin.o util.o @@ -16,6 +17,8 @@ assembler: $(ASM_OBJECTS) disassembler: $(DISASM_OBJECTS) +emulator: $(EMUL_OBJECTS) + asmcat: $(ASMCAT_OBJECTS) bincat: $(BINCAT_OBJECTS) diff --git a/debug.h b/debug.h new file mode 100644 index 0000000..ce92cec --- /dev/null +++ b/debug.h @@ -0,0 +1,10 @@ +#ifndef DEBUG_H +#define DEBUG_H + +#ifdef DEBUG +#define debug(x...) printf(x) +#else +#define debug(x...) +#endif + +#endif /* DEBUG_H */ diff --git a/emulator.c b/emulator.c new file mode 100644 index 0000000..660ddc6 --- /dev/null +++ b/emulator.c @@ -0,0 +1,293 @@ +#include +#include +#include +#include + +#include "lex.h" +#include "parse.h" +#include "instruction.h" +#include "input/input_bin.h" + +//#define DEBUG +#include "debug.h" + +/* Macro for safe wrap-around RAM access */ +#define RAM_AT(ctx, offs) (ctx->ram[offs % ctx->ram_size]) + + +struct emul_context { + uint8_t *ram; + size_t ram_size; + uint16_t pc; + bool zf; + bool cf; + uint16_t registers[REG_COUNT]; +}; + +int should_jump(struct emul_context *ctx, enum JCOND cond) { + switch (cond) { + case JB_UNCOND: return 1; + case JB_NEVER: return 0; + case JB_ZERO: return (ctx->zf); + case JB_NZERO: return !(ctx->zf); + case JB_CARRY: return (ctx->cf); + case JB_NCARRY: return !(ctx->cf); + case JB_CARRYZ: return (ctx->zf || ctx->cf); + case JB_NCARRYZ:return (!ctx->zf && !ctx->cf); + default: + assert(0); + } +} + +int execute_r(struct emul_context *ctx, struct instruction i) +{ + uint16_t res = 0; + uint16_t *d = &ctx->registers[i.inst.r.dest]; + uint16_t *l = &ctx->registers[i.inst.r.left]; + uint16_t *r = &ctx->registers[i.inst.r.right]; + + switch (i.inst.r.oper) { + case OPER_ADD: + res = *l + *r; + break; + case OPER_SUB: + res = *l - *r; + break; + case OPER_SHL: + res = *l << *r; + break; + case OPER_SHR: + res = *l >> *r; + break; + case OPER_AND: + res = *l & *r; + break; + case OPER_OR: + res = *l | *r; + break; + case OPER_XOR: + res = *l ^ *r; + break; + case OPER_MUL: + res = *l * *r; + break; + default: + return 1; + } + + ctx->zf = (res == 0); + /* FIXME set cf */ + + if (i.inst.r.dest != REG_0 && i.inst.r.dest != REG_H) { + *d = res; + } + return 0; +} + +int execute_i(struct emul_context *ctx, struct instruction i) +{ + uint16_t res = 0; + uint16_t *d = &ctx->registers[i.inst.i.dest]; + uint16_t *l = &ctx->registers[i.inst.i.left]; + uint16_t imm = i.inst.i.imm.value; + + switch (i.inst.i.oper) { + case OPER_ADD: + res = *l + imm; + break; + case OPER_SUB: + res = *l - imm; + break; + case OPER_SHL: + res = *l << imm; + break; + case OPER_SHR: + res = *l >> imm; + break; + case OPER_AND: + res = *l & imm; + break; + case OPER_OR: + res = *l | imm; + break; + case OPER_XOR: + res = *l ^ imm; + break; + case OPER_MUL: + res = *l * imm; + break; + default: + return 1; + } + + ctx->zf = (res == 0); + /* FIXME set cf */ + + if (i.inst.r.dest != REG_0 && i.inst.r.dest != REG_H) { + *d = res; + } + return 0; +} + +int execute_jr(struct emul_context *ctx, struct instruction i) +{ + if (should_jump(ctx, i.inst.jr.cond)) + ctx->pc = ctx->registers[i.inst.jr.reg]; + return 0; +} + +int execute_ji(struct emul_context *ctx, struct instruction i) +{ + if (should_jump(ctx, i.inst.ji.cond)) + ctx->pc = i.inst.ji.imm.value; + return 0; +} + +int execute_b(struct emul_context *ctx, struct instruction i) +{ + if (should_jump(ctx, i.inst.b.cond)) + ctx->pc -= (int16_t)i.inst.b.imm.value; + + return 0; +} + +int execute_single(struct emul_context *ctx) +{ + int ret = 0; + struct instruction i = { 0 }; + int (*f)(struct emul_context*, struct instruction) = NULL; + ret = disasm_single(&i, ctx->pc, + RAM_AT(ctx, ctx->pc ) << 8 | RAM_AT(ctx, ctx->pc + 1), + RAM_AT(ctx, ctx->pc + 2) << 8 | RAM_AT(ctx, ctx->pc + 3)); + if (ret <= 0) { + printf("disasm_single returned %d\n", ret); + return ret ? ret : -1; + } + + ctx->pc += ret; + switch(i.type) { + case INST_TYPE_R: + f = execute_r; + break; + case INST_TYPE_NI: + case INST_TYPE_WI: + f = execute_i; + break; + case INST_TYPE_JR: + f = execute_jr; + break; + case INST_TYPE_JI: + f = execute_ji; + break; + case INST_TYPE_B: + f = execute_b; + break; + default: + fprintf(stderr, "Unhandled instruction '0x%x' at 0x%x (%d), stop.\n", + ctx->ram[ctx->pc], ctx->pc, ctx->pc); + return 1; + } + + ret = f(ctx, i); + + return ret; +} + +int emulator_run(uint8_t *ram, size_t ram_size, size_t bytes_used) +{ + int ret = 0; + struct emul_context ctx = {0}; + ctx.ram = ram; + ctx.ram_size = ram_size; + ctx.registers[REG_H] = ~(uint16_t)0; + + for (ctx.pc = 0; ctx.pc < ctx.ram_size && ctx.pc < bytes_used;) { + if ((ret = execute_single(&ctx))) { + return ret; + } + debug("pc:%d\n", ctx.pc); + } + + if (ctx.pc >= bytes_used) { + debug("Fell off the bottom of the given program, stopping.\n"); + } else { + debug("Fell off the bottom of memory, stopping.\n"); + } + + printf( + "Registers:\n" + "pc: 0x%x (%d)\n" + "$0: 0x%x (%d)\n" + "$1: 0x%x (%d)\n" + "$2: 0x%x (%d)\n" + "$3: 0x%x (%d)\n" + "$4: 0x%x (%d)\n" + "$5: 0x%x (%d)\n" + "$6: 0x%x (%d)\n" + "$H: 0x%x (%d)\n", + ctx.pc, ctx.pc, + ctx.registers[REG_0], ctx.registers[REG_0], + ctx.registers[REG_1], ctx.registers[REG_1], + ctx.registers[REG_2], ctx.registers[REG_2], + ctx.registers[REG_3], ctx.registers[REG_3], + ctx.registers[REG_4], ctx.registers[REG_4], + ctx.registers[REG_5], ctx.registers[REG_5], + ctx.registers[REG_6], ctx.registers[REG_6], + ctx.registers[REG_H], ctx.registers[REG_H] + ); + return 0; +} + +void print_help(const char *argv0) +{ + fprintf(stderr, "Syntax: %s \n", argv0); +} + +int main(int argc, char **argv) +{ + int error_ret = 1; + int ret = 0; + const char *path_in = NULL; + FILE *fin = NULL; + + if (argc < 2) { + print_help(argv[0]); + return 1; + } + + if (strcmp(argv[1], "-q") == 0) { + if (argc != 4) { + print_help(argv[0]); + return 0; + } + error_ret = 0; + path_in = argv[2]; + } else { + path_in = argv[1]; + } + + if ((fin = fopen(path_in, "r")) == NULL) { + fprintf(stderr, "Error opening %s: ", path_in); + perror("fopen"); + return error_ret; + } + + uint8_t ram[65536] = { 0 }; + uint16_t bytes_used = 0; + size_t nread = 0; + while((nread = fread(ram + bytes_used, 1, 128, fin))) { + bytes_used += nread; + } + + if (!feof(fin)) { + perror("fread"); + return error_ret; + } + fclose(fin); + + debug("Read %d bytes of program into memory\n", bytes_used); + if ((ret = emulator_run(ram, sizeof(ram), bytes_used))) + return error_ret && ret; + + return 0; +} diff --git a/input/input_bin.c b/input/input_bin.c index 8f4c827..eafcca1 100644 --- a/input/input_bin.c +++ b/input/input_bin.c @@ -151,6 +151,7 @@ static int disasm_file(FILE *f) /* just used up 4 bytes, and couldn't read more. break out*/ goto read_eof; } + /* FALLTHROUGH */ case 2: /* have just read 2 bytes: shift down and pack new in */ inst = extra; diff --git a/input/input_bin.h b/input/input_bin.h index 00e296c..613f280 100644 --- a/input/input_bin.h +++ b/input/input_bin.h @@ -1,6 +1,7 @@ #ifndef INPUT_BIN_H #define INPUT_BIN_H +size_t disasm_single(struct instruction *i, uint16_t pc, uint16_t inst, uint16_t extra); int input_bin(FILE *f, struct instruction **i, size_t *i_count); #endif /* INPUT_BIN_H */ diff --git a/instruction.h b/instruction.h index 4ee68ed..add7c49 100644 --- a/instruction.h +++ b/instruction.h @@ -94,7 +94,8 @@ enum REG { REG_4 = 4, REG_5 = 5, REG_6 = 6, - REG_H = 7 + REG_H = 7, + REG_COUNT }; /** diff --git a/test/Makefile b/test/Makefile index c2407c7..cb0f3ee 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,2 +1,3 @@ test: ./full-pipeline/run-full-pipeline.sh + ./emul/run-emul.sh diff --git a/test/emul/002-nop.asm b/test/emul/002-nop.asm new file mode 100644 index 0000000..31538ba --- /dev/null +++ b/test/emul/002-nop.asm @@ -0,0 +1,7 @@ +; POST $1 = 0x0 +; POST $2 = 0x0 +; POST $3 = 0x0 +; POST $4 = 0x0 +; POST $5 = 0x0 +; POST $6 = 0x0 +nop diff --git a/test/emul/003-small-loop.asm b/test/emul/003-small-loop.asm new file mode 100644 index 0000000..ae2f81f --- /dev/null +++ b/test/emul/003-small-loop.asm @@ -0,0 +1,10 @@ +; POST $1 = 0x2 +; POST $2 = 0x0 +; POST $3 = 0x28 +ldi $1, 2 +ldi $2, 20 +ldi $3, 0 +loop: + add $3, $3, $1 + subi $2, $2, 1 + bnz loop diff --git a/test/emul/run-emul.sh b/test/emul/run-emul.sh new file mode 100755 index 0000000..3b5ae69 --- /dev/null +++ b/test/emul/run-emul.sh @@ -0,0 +1,68 @@ +#!/bin/bash -e + +# +# Script for running all of the automated tests that involve the assembler +# and emulator. +# These tests are assembly files with postconditions for registers specified +# in special syntax within comments. They contain code to run, and the post- +# conditions are checked in order to determine the test result. +# + +fail() { + echo -e '[\e[1;31mFAIL\e[0m] '"$1:" "$2" +} + +pass() { + echo -e '[\e[1;32mPASS\e[0m] '"$1" +} + +clean() { + echo "Removing work dir $WORK" + rm -r "$WORK" +} + +WORK=$(mktemp -d) +pushd $(dirname "$0") >/dev/null +export ASM="$PWD/../../assembler" +export EMUL="$PWD/../../emulator" +has_failure=0 + +for asmfile in *.asm ; do + binfile="$WORK/$(sed -e 's/\.asm$/.bin/' <<< "$asmfile")" + outfile="$WORK/$(sed -e 's/\.asm$/.out/' <<< "$asmfile")" + # Assemble test code + if ! "$ASM" "$asmfile" "$binfile" ; then + fail "$asmfile" "test assembly failed" + continue + fi + + "$EMUL" "$binfile" > "$outfile" + + # Each postcondition line must hold true, and forms a separate test to + # help track down failures + (echo '; POST $0 = 0' ; + echo '; POST $H = 0xFFFF' ; + grep '^;\s\+POST\s\+' "$asmfile" ) | while read line ; do + reg=$(awk -F= '{print $1}' <<< "$line" | awk '{print $(NF)}') + val=$(awk -F= '{print $2}' <<< "$line"| awk '{print $1}') + subtest="${asmfile}:POST-${reg}" + # Scrape output of emulator for register value + actual=$(grep "$reg" "$outfile" | awk '{print $2}') + if [[ "$actual" -eq "$val" ]]; then + pass "$subtest" + else + fail "$subtest" "postcondition (expect $val, got $actual)" + has_failure=1 + fi + done + +done +popd >/dev/null + +if [[ "$failure" != "0" && "$NO_CLEAN" == "1" ]] ; then + echo "Warning: Leaving work dir $WORK in place. Please remove this yourself" +else + clean +fi + +exit "$has_failure" diff --git a/test/full-pipeline/005-small-loop.asm b/test/full-pipeline/005-small-loop.asm index 3f2dc5f..5c47e51 100644 --- a/test/full-pipeline/005-small-loop.asm +++ b/test/full-pipeline/005-small-loop.asm @@ -1,6 +1,7 @@ ldi $1, 2 -ldi $2, 100 +ldi $2, 20 ldi $3, 0 loop: add $3, $3, $1 + subi $2, $2, 1 bnz loop -- cgit v1.1