This is an 8-bit virtual machine with 16-bit addressing. It is RISC-style with no direct operations on memory, instead operations are done on one of 16 8-bit registers with 6 16-bit registers mapped over them.
An ASM language is defined and implemented for this VM.
CMake and a C build toolchain are required to build. On a debian-based system install build-essential and cmake packages.
# setup makefiles (run once)
$ cmake -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" -S . -B ./cmake-build-release
# build project
$ cmake --build ./cmake-build-release --target uVMUsage is printed with no options given.
$ ./cmake-build-release/uVM
Usage:
uVM program.bin # load at 0x0000
uVM -a 0x100 file1.bin -a 0x200 file2.bin # multiple files at absolute addresses
uVM -p 0x100 program.bin # set PC to 0x100
uVM -c file.asm [-o output.bin] # assemble into binary formatCompiling and running asm files can be achieved with the following.
$ ./cmake-build-release/uVM -c programs/basic.asm -o programs/basic.bin
Assembling 'programs/basic.asm' -> 'programs/basic.bin'
$ /cmake-build-release/uVM programs/basic.bin
Loaded 'programs/basic.bin' at 0x0000 (272 bytes)
VM Halted with code 0
PC: 0110 carry: 0 halted: 1 haltcode: 0000 (0)
R0: 00 (000) R4: 02 (002) R8: 00 (000) RC: 00 (000)
R1: 01 (001) R5: 00 (000) R9: 00 (000) RD: 00 (000)
R2: 64 (100) R6: 00 (000) RA: 00 (000) RE: 00 (000)
R3: 00 (000) R7: 00 (000) RB: 00 (000) RF: 00 (000)
RXB: 00 (00000) RXE: 00 (00000)
RXC: 00 (00000) RXF: 00 (00000)
RXD: 00 (00000)- 8-bit registers 16 registers (
R0–RF), 8-bit each. - 16-bit registers 16 registers (
RX0-RXF) mapped over the 8-bit registers.RX0-RX9with low bytes mapped toRXA-RXFmapped to pairs of 8-bit registers
- R0/RX0 always reads as 0. Writes to R0 are a no-op.
- PC (program counter), 16-bit. Increments by 2 each instruction fetch. Must be word-aligned.
- carry, 1-bit flag used by ADC and SBC.
Some instructions can use two 8-bit registers as one 16-bit register for addressing.
The upper 12 registers form six extended 16-bit address registers (big-endian, high byte : low byte):
| Alias | Registers |
|---|---|
| RXA | R4 : R5 |
| RXB | R6 : R7 |
| RXC | R8 : R9 |
| RXD | RA : RB |
| RXE | RC : RD |
| RXF | RE : RF |
vm_get_rx() returns the 16-bit value of a register: 0x0-0x9 return the 8-bit variant, with 0xA-0xF returning the above registers.
- 64KB address space (0x0000 - 0xFFFF).
- All operations work on registers only, no direct memory-to-memory operations.
- Memory access is via hook functions:
readAddrandwriteAddron the VM struct.
All instructions are two bytes (big-endian). The upper four bits are the opcode. Three operand formats share the lower 12 bits:
| Format | Layout (16 bits) | Description |
|---|---|---|
| Q | [op:4][rd:4][imm8:8] |
Destination register + 8-bit immediate |
| S | [op:4][rd:4][rx:4][ry:4] |
Destination + two source registers |
| T | [op:4][rd:4][rx:4][imm4:4] |
Destination + source register + 4-bit immediate |
| V | [op:4][imm12:12] |
12-bit immediate |
Operand terms
exitcode- sets the vm exitcoderd/rx/ry- 8-bit registerrdx-rdwith extended register supportimm4- immediate value 4-bitimm4_s- immediate value 4-bitimm4_s_x2- immediate value 4-bit multiplied by twotest- a conditional jump typeimm8- immediate value 8-bitimm8_s- signed immediate value 8-bitimm8_s_x2- signed immediate value 8-bit multiplied by twoimm12- immediate value 12-bitimm12_s- signed immediate value 12-bitimm12_s_x2- signed immediate value 12-bit multiplied by twocarry- carry flag
All jumps are in words/instructions.
| Op | Mnemonic | Format | Operation | Description |
|---|---|---|---|---|
0 |
HLT | V | exitcode ← imm12 |
Halt the VM. Sets halted = true, and the exit code to imm12 |
1 |
LDA | T | rd ← [rdx + imm4] |
Load byte from memory at 16-bit address rdx + imm4 into rd |
2 |
STA | T | [rdx + imm4] ← rx |
Store rx to memory at 16-bit address rdx + imm4 |
3 |
LDI | Q | rd ← imm8 |
Load imm8 into rd |
4 |
ADD | S | rd ← rx + ry |
8-bit add. Clears carry |
5 |
ADC | S | rd ← rx + ry + carry |
8-bit add with carry. Sets carry on overflow |
6 |
SUB | S | rd ← rx - ry |
8-bit subtract. Clears carry |
7 |
SBC | S | rd ← rx - ry - carry |
8-bit subtract with borrow. Propagates borrow bit-by-bit into carry |
8 |
NOT | T | rd ← ~rx |
Bitwise NOT of rx |
9 |
AND | S | rd ← rx & ry |
Bitwise AND of rx and ry |
A |
SHL | T | rd ← rx << imm4 |
Logical left shift by imm bits |
B |
SHR | T | rd ← rx >> imm4 |
Logical right shift by imm bits |
C |
JMP | V | PC ← PC + imm12_s_x2 |
Relative jump. Update PC with imm12_s_x2 |
D |
JPF | Q | PC ← rdx + imm8_s_x2 |
Far jump. Set PC to the value in rdx with imm8_s_x2 value |
E |
JNZ | Q | PC ← PC + imm8_s_x2 if rd != 0 |
Conditional jump. Add imm8_s_x2 to PC if rd is not zero |
F |
JPC | T | PC ← PC + 2 if !test(rd,rx) |
Conditional jump. Skips the next instruction if test passes for values in rd and rx |
Test types, used for comparing values in rd and rx.
Tests are mapped into a 4-bit value and can be in any combination
3 2 1 0
| | | |
| | | +-- Equality
| | +------ Less Than
| +---------- Greater Than
+-------------- Negate Result
Setting bits 0–3 will pass for any value, negating it will not pass for any value, similarly not setting any bits will not pass for any value.
| Mnemonic | Number | Binary | Test |
|---|---|---|---|
| EQ | 1 | 0001 | Equals |
| ~EQ | 9 | 1001 | Not equals |
| LT | 2 | 0010 | Less than |
| ~LT | 10 | 1010 | Not less than |
| LTE | 3 | 0011 | Less than or equal |
| ~LTE | 11 | 1011 | Not less than or equal |
| GT | 4 | 0100 | Greater than |
| ~GT | 12 | 1100 | Not greater than |
| GTE | 5 | 0101 | Greater than or equal |
| ~GTE | 13 | 1101 | Not greater than or equal |
Errors are reported via the error hook and halt the VM:
| Code | Constant | Condition |
|---|---|---|
| 1 | VM_ERR_MISALIGN |
PC is not word-aligned on instruction fetch. |
| 2 | VM_ERR_UNKNOWN_OP |
Opcode is not recognized (0–F). Redundant, exists for completeness. |
| 3 | VM_ERR_OUT_OF_BOUNDS |
Memory access address >= 0x10000. |
| 4 | VM_ERR_JUMP_SELF |
An jump instruction jumped to itself |
vm_step()fetches one 2-byte instruction fromreadAddr(PC).- Decodes the opcode and operand format.
- Executes the operation, writing the result to
rdviavm_put_r(). - Increments PC by 2 (except for jumps which set PC directly).
- The main loop calls
vm_step()repeatedly untilvm->haltedis true.
Instruction changes
- Replace
ANDinstruction withBITfor bitwise operation, not havingORis too much of a limitation- change to T format
- imm4 used to determine bitwise op on rd and rx
- rd is modified by value in rx
- operations
ANDORXORMOD
ASM
- allow variables in STA/LDA address offset
- allow referencing variable high/low byte with
:h/:l
VM
- Add device support
- Console
- File