summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Phillips <david@sighup.nz>2019-08-04 00:13:59 +1200
committerDavid Phillips <david@sighup.nz>2019-08-04 00:13:59 +1200
commit9aa02bebf295ce9436451e0ce85db7717a6c9f81 (patch)
treecf95f8e06600c3359a7db6c6b08e6e055acf0d4c
parent89917ead30321ff430fa2eab8e8d96ef4178d994 (diff)
downloadtoy-cpu-assembler-9aa02bebf295ce9436451e0ce85db7717a6c9f81.tar.xz
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.
-rw-r--r--.gitignore1
-rw-r--r--Makefile5
-rw-r--r--debug.h10
-rw-r--r--emulator.c293
-rw-r--r--input/input_bin.c1
-rw-r--r--input/input_bin.h1
-rw-r--r--instruction.h3
-rw-r--r--test/Makefile1
-rw-r--r--test/emul/002-nop.asm7
-rw-r--r--test/emul/003-small-loop.asm10
-rwxr-xr-xtest/emul/run-emul.sh68
-rw-r--r--test/full-pipeline/005-small-loop.asm3
12 files changed, 400 insertions, 3 deletions
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 <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <assert.h>
+
+#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 <in.bin>\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