Simulator
This page describes the machine a Spimbot program runs on. Assembly language describes the assembler, and MMIO registers describes the game's devices.
Each program runs on its own little-endian MIPS32 Release 2 CPU with a floating-point unit, and every instruction takes one cycle.
Memory map
#| Region | Addresses | Contents |
|---|---|---|
| Text | 0x00400000 to the end of .text |
The startup code, then the program's .text |
| Small data | 0x10000000-0x1000ffff |
Data addressed from $gp, and .extern storage |
| Data | 0x10010000 to the end of .data |
The program's .data |
| Heap | The end of the static data to the break | Grows and shrinks with sbrk |
| Stack | 0x7ff00000-0x7fffffff |
1 MiB, growing down |
| Kernel text | The .ktext code, between 0x80000000 and 0x8fffffff |
The exception handler, from 0x80000180 |
| Kernel data | 0x90000000-0x9000ffff |
64 KiB, starting with .kdata |
| Memory-mapped I/O | 0xffff0000-0xffffffff |
The game's devices |
Small data, data and heap together take at most 4 MiB from 0x10000000. The break starts at the end of the static data or at 0x10010000, whichever is higher, rounded up to a multiple of 8. Any other address has nothing behind it, and an access there is a bus error.
Startup
#Execution begins at __start, a routine the assembler places at the start of .text. Your own text follows it, from 0x00400028 unless you give .text an address. __start calls main with argc in $a0, argv in $a1 and envp in $a2. When main returns, __start calls the exit syscall, so the exit code is 0. Use exit2 (17) for another code. Under tournament rules, syscalls do nothing, so a main that returns runs on into the first instruction of your text.
In a match, argc is 0. spimbot exec passes the first file's path as argv[0], then the arguments after --, with an empty environment.
At __start, $sp is 0x7fffeff8, or the address of argc below the arguments, 8-byte aligned, and $gp is 0x10008000. The other general-purpose registers, HI, LO, the floating-point registers and FCSR are 0, except $a0-$a2 when there are arguments. main starts with $sp 16 bytes lower and $ra pointing back into __start.
Delay slots
#By default a branch or jump takes effect at once, and a loaded value is ready for the next instruction. Matches and the debugger always run this way. spimbot exec has two options for hardware timing:
--delayed-branchesruns the instruction after each branch or jump (its delay slot) before the transfer. A branch-likely that is not taken skips its slot.jaland the other linking instructions save the address after the slot. A branch, jump oreretin a delay slot raises Reserved Instruction. The assembler leaves each slot as you write it.--delayed-loadsmakes a loaded value reach its register one instruction late.
An exception or interrupt in a delay slot sets Cause.BD, and EPC points at the branch, which runs again on return.
Coprocessor 0
#mfc0 and mtc0 reach the coprocessor 0 registers, and mtc0 changes only a register's writable fields:
| Register | Name | Contents |
|---|---|---|
| 8 | BadVAddr | The address of the last address error |
| 9 | Count | Increments every cycle. 0 at reset |
| 11 | Compare | The timer's target. Writing it clears the timer interrupt. 0 at reset |
| 12 | Status | The fields below |
| 13 | Cause | The fields below |
| 14 | EPC | The address eret returns to |
| 15, select 1 | EBase | The exception vector base, 0x80000000 at reset |
The Status bits, 0x3000ff00 at reset:
| Bit | Field | Meaning |
|---|---|---|
| 0 | IE | Interrupts enabled. Clear at reset |
| 1 | EXL | An exception is being handled |
| 8-15 | IM0-IM7 | Interrupt mask, one bit per Cause.IP line. Set at reset |
| 26 | FR | 64-bit floating-point registers. Clear at reset |
| 28 | CU0 | Coprocessor 0 usable. Set at reset |
| 29 | CU1 | Floating-point unit usable. Set at reset |
The Cause bits, 0 at reset:
| Bit | Field | Meaning |
|---|---|---|
| 2-6 | ExcCode | The last exception's code |
| 8-9 | IP0-IP1 | Software interrupts. Writable |
| 10-15 | IP2-IP7 | Hardware interrupt lines. Read-only |
| 23 | IV | Interrupts use the vector at EBase + 0x200. Writable |
| 28-29 | CE | The unit of a Coprocessor Unusable exception |
| 30 | TI | Timer interrupt pending |
| 31 | BD | The exception happened in a delay slot |
Exceptions
#| Code | Exception | Name in JSON | Raised by |
|---|---|---|---|
| 0 | Interrupt | interrupt |
A pending interrupt that Status lets through |
| 4 | Address error on load | address_load |
A misaligned load or instruction fetch |
| 5 | Address error on store | address_store |
A misaligned store |
| 6 | Bus error on instruction fetch | instruction_bus_error |
A fetch from an address with nothing behind it |
| 7 | Bus error on data access | data_bus_error |
A load or store at an address with nothing behind it, a device that rejects an access, or a syscall buffer outside memory |
| 8 | Syscall exception | syscall |
A syscall with an unknown number in $v0 |
| 9 | Break instruction | breakpoint |
break |
| 10 | Reserved instruction | reserved_instruction |
A word that is no instruction, an instruction for missing hardware, or a branch in a delay slot |
| 11 | Coprocessor N unusable | coprocessor_unusable |
A floating-point instruction with Status.CU1 clear |
| 12 | Arithmetic overflow | overflow |
Signed overflow in add, addi or sub |
| 13 | Trap | trap |
A trap instruction whose condition holds, including the divide-by-zero check in three-operand div, divu, rem and remu, and the overflow check in mulo and mulou |
| 15 | Floating-point exception | floating_point |
A floating-point exception enabled in FCSR |
On an exception, the CPU writes the code to Cause.ExcCode, and the unit to Cause.CE for Coprocessor Unusable. If Status.EXL is clear, it sets EPC to the faulting instruction and sets or clears Cause.BD. Address errors also set BadVAddr to the faulting address. The CPU then sets Status.EXL and jumps to EBase + 0x180, which is 0x80000180 by default, or to EBase + 0x200 for an interrupt with Cause.IV set. eret clears Status.EXL and jumps to EPC.
An exception inside the handler jumps to the handler again and leaves EPC and Cause.BD unchanged, so a handler that faults every time never returns. Under tournament rules, any exception other than an interrupt crashes the bot.
The default exception handler
#Unless your program has a .ktext segment, the assembler adds a default handler at 0x80000180. It saves and restores $at, $v0 and $a0, and overwrites $k0 and $k1.
- On an interrupt, it clears the pending lines' bits in Status.IM, so an interrupt nothing handles stops arriving.
- On Coprocessor 1 unusable, it sets Status.CU1 and runs the instruction again.
- On a bus error or address error on instruction fetch, it prints the exception and exits with code 0.
- On any other exception, it prints the exception and resumes after the faulting instruction. When Cause.BD is set, it resumes after the branch and its delay slot.
Exception 7 [Bad data address] occurred and ignored
Exception 6 [Bad instruction address] occurred; exiting
Your own exception handler
#A .ktext segment replaces the default handler entirely. This one skips any faulting instruction:
.ktext 0x80000180
mfc0 $k0, $13 # Cause
andi $k0, $k0, 0x7c # ExcCode, shifted left 2
beqz $k0, interrupt
mfc0 $k0, $14 # EPC
addiu $k0, $k0, 4 # skip the faulting instruction
mtc0 $k0, $14
eret
interrupt:
# acknowledge the device that raised the interrupt here
eret
The handler can use $k0 and $k1. A handler that uses pseudo-instructions needing $at must save $at first and restore it before eret.
Interrupts
#Cause.IP holds eight interrupt lines. IP0 and IP1 are software interrupts, which you raise by writing Cause. IP2 to IP7 come from the devices and stay raised until the device lowers them. The game's lines are in Interrupts. IP7 is also the timer: when Count reaches Compare, Cause.TI and IP7 rise until you write Compare.
The CPU takes an interrupt before an instruction when Status.IE is set, Status.EXL is clear, and a raised line has its bit set in Status.IM. EPC is the instruction that would have run next. At reset every IM bit is set and IE is clear, so you enable interrupts by setting IE with ei or mtc0.
wait stalls the CPU, with cycles passing, until it can take an interrupt. EPC is then the instruction after the wait. A wait with interrupts disabled waits forever.
Bus errors and alignment
#| Instructions | Alignment |
|---|---|
lb, lbu, sb, lwl, lwr, swl, swr |
Any |
lh, lhu, sh |
2 |
lw, sw, ll, sc, lwc1, swc1, lwxc1, swxc1, instruction fetch |
4 |
ldc1, sdc1, ldxc1, sdxc1 |
8 |
A misaligned load or fetch raises Address error on load (4), and a misaligned store raises Address error on store (5). An aligned access to an address with nothing behind it raises Bus error on instruction fetch (6) or Bus error on data access (7). An access from 0xffff0000 up that no device accepts is also a data bus error.
Self-modifying code
#The text segments are writable, and a store into code takes effect on the next fetch of that word. Code also runs from data, heap, stack and kernel data.
Floating-point unit
#The floating-point unit has 32 registers, condition codes 0 to 7, and IEEE 754 arithmetic.
Status.FR (bit 26) is clear at reset, and each register then shows 32 bits. A double takes an even register and the odd one after it, with the low word in the even register, and an odd register in a 64-bit operand raises Reserved Instruction. The .l and .ps formats need FR set and otherwise raise Reserved Instruction. With FR set, every register holds 64 bits. Assemble with --fp64 to name odd registers in 64-bit operands, as Bare mode and 64-bit registers describes.
FCSR
#cfc1 and ctc1 reach FCSR as control register 31:
| Bits | Field | Meaning |
|---|---|---|
| 0-1 | RM | Rounding mode: 0 to nearest with ties to even, 1 toward zero, 2 toward positive infinity, 3 toward negative infinity |
| 2-6 | Flags | Sticky flags: Inexact, Underflow, Overflow, Divide by zero, Invalid |
| 7-11 | Enables | Exceptions that trap, in the same order |
| 12-17 | Cause | The last operation's exceptions, in the same order, then Unimplemented Operation |
| 23 | FCC0 | Condition code 0 |
| 24 | FS | Flush tiny results to zero |
| 25-31 | FCC1-FCC7 | Condition codes 1 to 7 |
An operation that raises an enabled exception raises Floating-point exception (15) and leaves the destination and Flags unchanged. Otherwise its exceptions add to Flags.
Syscalls
#syscall performs the service whose number is in $v0:
$v0 |
Name | Arguments | Result |
|---|---|---|---|
| 1 | print_int |
$a0: integer |
Prints $a0 in signed decimal |
| 2 | print_float |
$f12: single |
Prints as C's %.8f |
| 3 | print_double |
$f12: double |
Prints as C's %.18g |
| 4 | print_string |
$a0: string address |
Prints the zero-terminated string |
| 5 | read_int |
$v0: the integer on the next input line |
|
| 6 | read_float |
$f0: the single on the next input line |
|
| 7 | read_double |
$f0: the double on the next input line |
|
| 8 | read_string |
$a0: buffer, $a1: buffer size |
Reads at most $a1 - 1 bytes, through the end of the line, and adds a zero byte |
| 9 | sbrk |
$a0: bytes, signed |
$v0: the old break, or -1 |
| 10 | exit |
Ends the program with exit code 0 | |
| 11 | print_char |
$a0: character |
Prints the low byte of $a0 |
| 12 | read_char |
$v0: the next input byte, sign-extended |
|
| 13 | open |
$a0: path, $a1: flags, $a2: mode |
$v0: a file descriptor, or -1 |
| 14 | read |
$a0: descriptor, $a1: buffer, $a2: length |
$v0: bytes read, 0 at the end of the file, or -1 |
| 15 | write |
$a0: descriptor, $a1: buffer, $a2: length |
$v0: bytes written, or -1 |
| 16 | close |
$a0: descriptor |
$v0: 0, or -1 |
| 17 | exit2 |
$a0: exit code |
Ends the program with exit code $a0 |
| 34 | print_hex |
$a0: integer |
Prints $a0 in unpadded lowercase hexadecimal |
Any other number raises Syscall exception (8). A string, path or buffer at an address with nothing behind it raises Bus error on data access (7) before the syscall reads input or writes anything. Under tournament rules, every syscall does nothing.
Console input
#Input comes from the Console panel in the debugger and from standard input in spimbot exec. A read waits for a whole line, or for the end of the input. spimbot run has no input, so a read there waits forever.
read_int,read_floatandread_doubleread a number after any leading whitespace and use up the whole line. A line with no number, or the end of the input, gives 0.read_intreads decimal integers.read_stringkeeps the newline when it fits, and the rest of a longer line stays for the next read.read_chargives 10 ('\n') at the end of the input.readfrom descriptor 0 reads at most one line, and gives 0 at the end of the input.
sbrk
#sbrk moves the break by $a0 bytes, rounds the new break up to a multiple of 8, and returns the old break. A negative $a0 shrinks the heap. It returns -1 and changes nothing when the break would drop below the end of the static data or pass the 4 MiB data limit.
Files
#The file syscalls work on real files, with paths relative to the directory spimbot runs in. Descriptor 0 reads console input, and descriptors 1 and 2 write console output. The open flags have their Linux values: O_RDONLY (0x0), O_WRONLY (0x1), O_RDWR (0x2), O_CREAT (0x40), O_EXCL (0x80), O_TRUNC (0x200) and O_APPEND (0x400). $a2 gives the permission bits of a file that open creates. Any failure returns -1.
Running a program alone
#spimbot exec runs a program on this machine without the game, so every access from 0xffff0000 up is a data bus error. Console output goes to standard output and input comes from standard input. The command exits with the program's exit code, and fails if the program runs past its cycle limit. exec lists the options.