A small virtual machine written in C++, complete with an assembler and a debugger.
This is a virtual machine that simulates execution of a nonexistent 16-bit CPU. It includes tools for assembling machine code from assembly code, a dissasembler, and a graphical debugger.
micro_vm uses SCons to build. Use the scons
command to build, and scons -c
to clean up. This is also wrapped up in a Makefile, but you need to have SCons installed in order to build.
You will also need to install the ncurses development headers; on Debian-based Linux distros this can be found in the ncurses-dev
package.
Once you have built the VM, you can execute the test program by running ./cpu out.bin
. It will present a blank prompt. Enter 2 numbers and the VM will add them together:
./cpu out.bin
2
4
6
More test programs can be found in the progs/ directory
The CPU is a 16-bit little-endian RISC processor with a few high-level instructions for interacting with console input and output.
A
(8 bit)X
= I[High]Y
= I[Low]FP
(16-bit)SP
(8-bit offset from FP)IP
(16-bit)F
(flags)- Zero
- Carry
- Sign
SP
points to next available element in stack (uninitialized). Therefore, it should never be accessed directly by program code.
The instruction set is pretty standard and should look familiar to most assembly programmers. The specification listed beside the instruction is C syntax, with a * indicating a memory address dereference (so for example, *I
refers to the memory location at the address stored in register I).
A numeric suffix indicates the byte size an operand literal.
- Operations
BAD
: InvalidNOP
: NothingADD
:A = A + *X:Y
ADD1
:A = A + Literal
AND
:A = A & *X:Y
AND1
:A = A & Literal
OR
:A = A | *X:Y
ORL
:A = A | Literal
CMPL
:A = ~A
INCA
:++A
INCX
:++X
INCY
:++Y
INCI
:++I
DECA
:--A
DECX
:--X
DECY
:--Y
DECI
:--I
DIV
:I = I / A; A = I % A
MUL
:I = A * Y
(8-bit operands, 16-bit result)
- Compare
CMP
:Flags
=A
Compared to*X:Y
CMP1
:Flags
=A
Compared toLiteral
- Memory
LDA
:A = *X:Y
LDA1
:A = Literal
LDX1
:X = Literal
LDY1
:Y = Literal
DREF
:I = *I
LAS1
:A = SP[Literal]
LIS1
:I = SP[Literal]
STOR
:*I = A
TXA
:A = X
TYA
:A = Y
TAX
:X = A
TAY
:Y = A
TSPI
:I = FP + SP
LDI2
:I = Literal
- Stack manipulation
PSHA
:*SP = A; ++SP
PSHX
:*SP = X; ++SP
PSHY
:*SP = Y; ++SP
PSH1
:*SP = Lit8; ++SP
PSH2
:*SP = Lit16; SP += 2
POPA
:--SP; A = *(FP + SP)
POPX
:--SP; X = *(FP + SP)
POPY
:--SP; Y = *(FP + SP)
POP1
:SP -= Lit
POP
:--SP
LAL1
:A = FP[Literal]
(LoadA
from local)LAP1
:A = FP[-5 - Literal]
(LoadA
from parameter)LIP1
:I = FP -5 - Literal
(LoadI
from parameter address)LIL1
:I = FP + Literal
(LoadI
from local address)STL1
:FP[Literal] = A
- Flow Control
JMP2
:IP = Literal
JGE2
:If F[Zero] || !F[Sign], IP = Literal
JG2
:If !F[Sign], IP = Literal
JL2
:If F[Sign], IP = Literal
JLE2
:If F[Zero] || F[Sign], IP = Literal
JE2
:If F[Zero], IP = Literal
JNE2
:If !F[Zero], IP = Literal
JC2
:If F[Carry], IP = Literal
CAL2
:*(FP+SP)=FP; *(FP+SP+2)=SP; *(FP+SP+3)=IP; FP+=SP+5; SP = 0; IP = Literal
(Call subroutine)RET
:IP = FP[-2]; SP = FP[-3]; FP = FP[-5]
(Return from subroutine)
- I/O
PRNT
: PrintA
as ASCII byteKEYB
: Read key intoA
. \0 is end of input
- Flags
CC
: Clear Carry
- Debug
BRK
: Debug break (NOP
if no debugger)
Assembles input assembly code into a machine code file (out.bin
) and a debug metadata file (out.dbg
)
Usage: ./asm input.asm
Disassembles machine code into human-readable assembly code and outputs it to stdout.
Usage: ./disasm out.bin
Runs a program interactively on a debugger GUI (written in ncurses).
Usage: ./debug out
(Notice no '.bin')
The enter key steps the program forward, and the 'r' steps the program backwards.
main:
PSH1 13
CAL2 fib
POP
PSHA
CAL2 printint8
POP
PSH2 endl
CAL2 puts
POP1 2
RET
fib:
LAP1 1
CMP1 1
JG2 .recur
RET
.recur:
CC
DECA
PSHA
CAL2 fib
POPX
PSHA
TXA
DECA
PSHA
CAL2 fib
POP
TSPI
DECI
ADD
POP1 1
RET
%file lib.asm