Simulator Example Synacor Port
The Synacor Challenge is a fun programming exercise with a number of puzzles built into it.
The first puzzle is writing an interpreter for their custom ISA. The CPU is quite basic: it's 16-bit with only 8 registers and a limited set of instructions. This means the port will never grow new features.
Implementing it here ends up being quite useful: it acts as a simple constrained "real world" example for people who want to implement a new simulator for their own architecture. We demonstrate all the basic fundamentals (registers, memory, branches, and tracing) that all ports should have.
This port does not use CGEN, so it is not a good reference for that.
Source Layout
Take note that example-synacor is the unique port name and is used everywhere for indexing. Normally this would be the common architecture name that is being simulated (e.g. arm).
This represents the barest skeleton of a functional port. Only the default Virtual Environment is supported.
* sim/
configure.ac: Every port is registered in here with its unique port name.
example-synacor/: Every port has a directory to hold all of its unique source files.
Makefile.in: The build file that lists all the custom source files.
interp.c: Common glue functions between the port and the rest of the GNU Simulator APIs.
sim-main.c: The main port-specific simulation logic. Exposes only a few functions for the interp.c module to call.
sim-main.h: The main port-specific definitions logic. This exact filename is required.
testsuite/
example-synacor/: Every port has a directory to hold all of its unique test files. Tests must be included with every port!
allinsn.exp: Dejagnu glue code to load all the tests. This exact filename isn't required (all *.exp are loaded), but it is strongly recommended to start with this for consistency.
testutils.inc: Common assembler macros to share code between tests. A common pattern across ports to avoid copying & pasting boiler plate logic, but its usage is not a requirement at all.
*.s: The actual tests. Basic assembler files (@file{*.s}) are preferred because they can be assembled & linked using only other projects usually shipped with the GNU Simulator (i.e. @ref{Top,as} and @ref{Top,ld}). Higher level languages (e.g. @file{*.S} or @file{*.c}) usually require a full compatible toolchain, compiler & C library for bare-metal targets, be installed independently.
Synacor Architecture
This section is not required to write a new port. It is provided as a reference to help better understand the example-synacor internals if needed.
* Architecture
- three storage regions
- memory with 15-bit address space storing 16-bit values
- eight registers
- an unbounded stack which holds individual 16-bit values
- all numbers are unsigned integers 0..32767 (15-bit)
all math is modulo 32768; 32758 + 15 => 5
* Binary Format
- each number is stored as a 16-bit little-endian pair (low byte, high byte)
- numbers 0..32767 mean a literal value
- numbers 32768..32775 instead mean registers 0..7
- numbers 32776..65535 are invalid
- programs are loaded into memory starting at address 0
- address 0 is the first 16-bit value, address 1 is the second 16-bit value, etc
* Execution
- After an operation is executed, the next instruction to read is immediately after the last argument of the current operation.
- If a jump was performed, the next operation is instead the exact destination of the jump.
- Encountering a register as an operation argument should be taken as reading from the register or setting into the register as appropriate.
* Hints
- Start with operations 0, 19, and 21.
Here's a code for the challenge website: jTTockJlJiOC
- The program "9,32768,32769,4,19,32768" occupies six memory addresses and should:
- Store into register 0 the sum of 4 and the value contained in register 1.
- Output to the terminal the character with the ascii code contained in register 0.
Instructions
This section is not required to write a new port. It is provided as a reference to help better understand the example-synacor internals if needed.
These are the supported instructions and their encodings. It's the instruction name followed by its opcode (encoding), followed by its arguments (if any).
Name |
Encoding |
Description |
halt |
0 |
Stop execution and terminate the program. |
set |
1 A B |
Set register A to the value of B. |
push |
2 A |
Push A onto the stack. |
pop |
3 A |
Remove the top element from the stack and write it into A; empty stack = error. |
eq |
4 A B C |
Set A to 1 if B is equal to C; set it to 0 otherwise. |
gt |
5 A B C |
Set A to 1 if B is greater than C; set it to 0 otherwise. |
jmp |
6 A |
Jump to A. |
jt |
7 A B |
If A is nonzero, jump to B. |
jf |
8 A B |
If A is zero, jump to B. |
add |
9 A B C |
Assign into A the sum of B and C (modulo 32768). |
mult |
10 A B C |
Store into @var{A} the product of @var{B} and @var{C} (modulo 32768). |
mod |
11 A B C |
Store into A the remainder of B divided by C. |
and |
12 A B C |
Stores into A the bitwise and of B and C. |
or |
13 A B C |
Stores into A the bitwise or of B and C. |
not |
14 A B |
Stores 15-bit bitwise inverse of B in A. |
rmem |
15 A B |
Read memory at address B and write it to A. |
wmem |
16 A B |
Write the value from B into memory at address A. |
call |
17 A |
Write the address of the next instruction to the stack and jump to A. |
ret |
18 |
Remove the top element from the stack and jump to it; empty stack = halt. |
out |
19 A |
Write the character represented by ascii code A to the terminal. |
in |
20 A |
Read a character from the terminal and write its ascii code to A; it can be assumed that once input starts, it will continue until a newline is encountered; this means that you can safely read whole lines from the keyboard and trust that they will be fully read. |
noop |
21 |
No operation. |