Back in 1984 D. G. Jones had the brilliant idea to create a programming game inspired by how some viruses manage to take over a system and their internal memory.
At its very core the game consists of having two or more programs (written by the contestants) that will be executed by the Virtual Machine in an arena (a sandbox memory space).
We will call those programs the "players" from now on since they will be the ones playing. Their creators won't have any possibility to interact with them or the arena during the match.
At the start of the game each player will have one process, and this process will be placed at the start of each player memory space (the current position of a process is called program counter, or PC).
The players will be able to create new processes using specific instructions, it is worth noting though that every instruction takes a certain amount of cycles to be executed, and creating a new process is the most expensive one.
Every once in a while the VM will check if the players executed such instruction and, if they didn't, all the processes created by this player will be instantly killed.
The VM will use some rules to decrease the elapsed time since the last check down to zero, this means the games can never be infinite and all processes will be killed at one point.
- If there is any error in the source file the Assembler should exit with an error code, print a message on `stderr` (the more specific the better) and do not create any `file.cor`.
- The last program passed will be the first one executed during the cycle.
- During a cycle the VM will load the instruction at the current PC and wait N cycles before to execute it (N being the cost of the instruction).
- Only when executing the instruction the VM will check for the parameters it may have and it will execute it only if the parameters are correct, otherwise it will print an error on `stderr` and continue to execute.
- If an instruction has incorrect parameters the PC will be moved forward according to the size of the parameters.
- If the instruction doesn't exist in the instruction set the PC will be moved forward of 1 byte.
- When a new process is forked it will be placed at the end of the processes and start its execution at the start of the next cycle (which means it will be the first one being executed on the next cycle).
- The entire execution is deterministic, so with the same inputs you must always have the same outputs.
At the end of the game the vm should print `cycle [X]: The winner is player [X]: [NAME]!`.
> If nobody executed a valid `live` statement the end message should be `cycle [X]: Nobody wins!`.
#### Circular memory space of the arena
The memory where the players will fight is circular, this means if we want to move forward from the last address in memory (for example `4095`) we will arrive at address `0`.
It implies that moving backward of one position from `0` will bring us to the address `4095`.
> Having circular memory guarantees players they will never overflow the memory space they are playing in.
Every `CYCLE_TO_DIE` the VM will check for every player if it signaled it was alive at least once, if it didn't all processes created by this player will be immediately killed.
- If during the last life loop there were at least `NBR_LIVE` successfully executed by the players.
- If it has been `MAX_CHECKS` life loops since it was decremented last time.
> Notice that some smart players may trick another player to start making `live` statements for them, this virtually means a player can still make `live` statements after all its processes were killed.
-`live`: says to the VM the player with the id matching the opposite of the first parameter is alive. If the first parameter is `-2` it means player 2 is alive.
-`ld`: loads the first parameter into the registry passed as second parameter. (If the first parameter is Indirect, the VM will apply `% IDX_MOD` to it)
-`st`: writes the value in the registry passed as the first parameter into the second parameter. (If the second parameter is Indirect, the VM will apply `% IDX_MOD` to it)
-`add`: sums the first two arguments and writes the result into the third one.
-`sub`: subtract the first two arguments and writes the result into the third one.
-`and`, `or`, `xor`: apply the respective bitwise operation to the first two parameters and saves it in the third one. (If the first and/or second parameters are Indirect, the VM will apply `% IDX_MOD` to them)
-`zjmp`: moves the PC adding the value passed as the first parameter. (The VM will always apply `% IDX_MOD` to the first parameter)
-`ldi`: writes the value contained at the address obtained by summing the first two arguments into the register in the third argument. (The VM will apply `% IDX_MOD` to the obtained address)
-`sti`: writes the value of the register passed as the first argument into to address obtained by summing the last two arguments. (The VM will apply `% IDX_MOD` to the obtained address)
-`fork`: creates a new process which will be an exact (deep) copy of the current one except for the PC which will be at the address specified in the argument. (The VM will apply `% IDX_MOD` to the first argument)
-`lld`, `lldi`, `lfork`: those instructions are the long versions of `ld`, `ldi` and `fork`, which means the addresses won't be truncated by `IDX_MOD`. (Since those instructions are "long" the modulo operation won't be applied on them)
> Some instructions have to truncate the addresses they are given by applying modulo `IDX_MOD`.
> This feature prevent players from reaching spaces in memory that are too far from the current PC, which means processes won't be able to attack each other straight away but will need to move by doing smaller steps, this help having more balanced games.
Some instruction will sum up direct values and/or use them as addresses to lookup into the arena memory space. For this reason we know that a full 32 bits integer won't be necessary and to save space we do save direct values on only two bytes in those cases.
You have `Had Idx` column in the Instruction Set table, when it is true you must save and read direct parameters on two bytes, when false they will be saved on four bytes as usual.
#### The Carry flag
In all processors you will find a carry which is a flag that notify some edge case states to allow special operations, ensure security and serve many others functionalities.
The Carry of the processes in the VM is in that sense a simplified version of this concept.
The following commands will modify the carry of the process executing them: `ld`, `add`, `sub`, `and`, `or`, `xor`.
The signature, also called magic, of a file is a particular value written at the start of a binary. This signature has the role to help the OS understand which kind of file it is and how to execute it. For the `.cor` files the signature is provided in the config file.
### Your player
While the main focus for now is to create an Asm and a VM, you should provide a basic player that should be able to fight and win against the `ameba.s`, this player will be tested in the provided VM during evaluation.
The specification requires a lot of constants, it would be tedious to define them in the subject every time they are mentioned.
For this reason we provide a [config file](data/config.md) which includes all the required constants. Some of them are specific to the Asm, some others to the VM, the vast majority will be helpful to both.
> While the file is language agnostic, it would be very easy to translate it to a specific language. We suggest you to do so in order to centralize the configuration, it will help to debug and extend easily your programs if needed.
- The `live` statement notify the VM the player is alive. Notice the specific value of the argument is irrelevant in this context since the `sti` instruction override it before the `live` is executed.
- At the start of each line there is the position of the first byte of the line in hexadecimal.
- This means that for the second line `10` hex is actually 16 in decimal notation.
- The right panel shows a `.` if the byte is not a valid ASCII character.
- Repeated lines are aggregated under `*`, this is why we pass from `10` to `80`.
- Each `xx` is a byte (a byte is 8 bits, `ff` in hexadecimal is `1111 1111` in binary, `255` in decimal).
Let's read it through:
- The first 4 bytes `00 ea 83 f3` are the program signature (also called the magic), this is a 32 bits integer, represented as four 8 bits slices.
- Then the next 128 bits are for the name of the program, the first five `61 6d 65 62 61` are the actual name, the others are zeros because we don't actually need it.
- Then we have 4 bytes `00 00 00 17` (so 23) which will be a 32 bits integer with the size in bytes of the program to be executed in the arena of the VM (so only the size of the instructions, without the name, description, signature and so on).
- Then we have the description which works exactly the same as the name but it will adds four bytes padding at the end of it.
- A VM and an Assembler binaries to use as references.
- A Dockerfile to run the binaries in a container.
- Instructions on how to execute the references.
- Some players to use as tests.
> The provided VM has an extra flag `-v` which you can use to print the state of the VM at every cycle, this should greatly help you during development and debugging.
While the functioning of the VM and the Assembler may seems quite strange and obscure, it is a perfect scenario to reproduce a CPU and understand what a computer really does at its core.
In this project you are indeed creating a Turing Complete machine largely inspired by Von Neumann architecture (which is still how today's processors works).