Skip to content
CS 233 // Spimbot
Pages

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-branches runs the instruction after each branch or jump (its delay slot) before the transfer. A branch-likely that is not taken skips its slot. jal and the other linking instructions save the address after the slot. A branch, jump or eret in a delay slot raises Reserved Instruction. The assembler leaves each slot as you write it.
  • --delayed-loads makes 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_float and read_double read 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_int reads decimal integers.
  • read_string keeps the newline when it fits, and the rest of a longer line stays for the next read.
  • read_char gives 10 ('\n') at the end of the input.
  • read from 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.