MIPS Registers

The MIPS architecture has 32 general-purpose registers, $HI and $LO registers for multiplication and division, and two coprocessors with registers of their own: coprocessor 1 holds the 32 floating point registers, coprocessor 0 the registers an exception writes. The registers panel of the editor shows each of the three as its own tab.

General purpose registers

$zero ($0)

Always contains the value 0. Any write attempt to this register is silently ignored.

$at ($1)

Assembly temporary, reserved for assembler internal use. It is used by macro instructions like li or la to break them into multiple real instructions. You can take control of it using the .set noat directive, but after that, macro instructions that rely on it will stop working.

$v0 - $v1 ($2 - $3)

Used to return non-floating point values from a subroutine. If the return value fits in 32 bits, only $v0 is used; for 64-bit values, the high word goes in $v1. $v0 also holds the system call number before a syscall instruction.

$a0 - $a3 ($4 - $7)

Arguments, used to pass the first four non-floating point arguments to a subroutine. Additional arguments are passed on the stack.

$t0 - $t7 ($8 - $15)

Temporary registers, their values are not preserved across subroutine calls. The caller is responsible for saving them if needed.

$s0 - $s7 ($16 - $23)

Saved registers, subroutines must preserve their values across calls, either by not using them or by saving and restoring them on the stack.

$t8 - $t9 ($24 - $25)

Additional temporary registers, same conventions as $t0-$t7. Note that $t9 has a special role in PIC code: by convention it holds the address of the called function, allowing the callee to compute $gp.

$k0 - $k1 ($26 - $27)

Reserved for OS/interrupt handler use. They can be overwritten at any time by an interrupt or trap handler, so user code should never rely on their values.

$gp ($28)

Global pointer, used for two distinct purposes. In PIC code (Linux shared libraries), it points to the GOT (Global Offset Table), a table of pointers that the dynamic loader fills at runtime with the real addresses of external symbols. In non-PIC code (embedded systems), it points to the center of a compact region of small global/static variables, allowing them to be accessed with a single instruction using a signed 16-bit offset (covering ±32KB, 64KB total).

$sp ($29)

Stack pointer, points to the top of the stack. It is explicitly adjusted by the callee on subroutine entry and exit.

$fp ($30)

Frame pointer, also known as $s8. Used by a subroutine to track the stack frame when the stack pointer cannot be used directly, for example when the stack size is not known at compile time (e.g. when using alloca()).

$ra ($31)

Return address, automatically written by jal with the address of the instruction following the call. The subroutine returns by executing jr $ra. Functions that themselves call other subroutines must save $ra on the stack first, since jal would otherwise overwrite it.

FPU (coprocessor 1)

Floating point numbers live in a coprocessor with registers and instructions of its own: add.s adds two single precision values the way add adds two integers, and no arithmetic instruction reads a register from each file. Crossing between them is a job of its own. mtc1 and mfc1 name one general register and one floating point register and move the bits between them without converting anything, while cvt.s.w and cvt.w.s convert between an integer and a float once the value is inside the coprocessor.

This simulator has no li.s or li.d pseudo-instruction. A constant is either written in the data section as a .float or a .double and loaded with l.s or l.d, or assembled as a bit pattern in a general register and moved across with mtc1.

$f0 - $f31 (32 bits each)

The floating point registers. Each one holds a single precision value, which is what the .s instructions read and write. By convention $f12 and $f14 pass the first two floating point arguments to a subroutine and $f0 returns the result, and the syscalls that read and print floats use the same registers.

Double precision pairs ($f0, $f2, $f4 … $f30)

A double precision value is 64 bits, so it occupies a pair of registers: the even one holds the low word and the odd one above it holds the high word. Only the even register is ever named, so l.d $f4, value fills both $f4 and $f5, and add.d $f0, $f2, $f4 reads and writes three pairs. Naming an odd register in a .d instruction is an error. This is also why the double format of the registers panel leaves the odd rows blank.

Condition flags (0 - 7)

A floating point comparison writes no register. It writes one of eight condition flags, and bc1t and bc1f branch on one of them, which is how a float comparison reaches a branch. The two operand form c.lt.s $f0, $f2 writes flag 0, the flag that bc1t label reads when it is given no number; the three operand form c.lt.s 3, $f0, $f2 writes flag 3, and bc1t 3, label is the branch that reads it. Eight flags mean eight comparisons can be kept apart at once. The registers panel shows them in the row above the registers of the file.

CP0 (coprocessor 0)

Coprocessor 0 is the part of the processor that deals with exceptions: a bad memory address, an overflow on add, a break. The machine writes these registers itself, at the moment it stops what the program was doing and jumps to the handler, which is whatever the program assembled at the exception vector with .ktext 0x80000180. A program with no handler there stops instead, with the message the simulator prints. The handler reads these registers with mfc0, as in mfc0 $k0, $13, to find out what happened and where, and writes them back with mtc0, as in mtc0 $k0, $14, which is how it steps the return address past the instruction that faulted before eret returns to it.

This simulator implements the four registers below, and it raises exceptions only: nothing in it delivers an interrupt, so the interrupt bits are values to read and write rather than something that changes what runs.

$8 (vaddr)

The address that caused the exception, when it was a memory one: the address the lw or sw failed on. It keeps whatever it held after any other kind of exception, so it is only worth reading once the cause says the exception was an address error.

$12 (status)

The interrupt mask and the enable bits. It starts at 0x0000FF11: every one of the eight interrupt levels unmasked, user mode, and interrupts enabled. The bit this simulator writes itself is bit 1, the exception level, which taking an exception sets and eret clears on the way back, so the register reads 0x0000FF13 inside a handler. A handler is free to change the mask and the enable bit with mtc0, as it would on a real processor, but since nothing here delivers an interrupt the change is only visible in the register.

$13 (cause)

Why the exception happened. Bits 2 to 6 hold the exception code, the number behind the message the simulator prints, so an address error on a load reads as 0x10, which is code 4 shifted up by two. The bits above them report pending interrupts on a real processor and stay zero here, because nothing in this simulator raises one. A handler that serves several causes reads this register first and branches on the code.

$14 (epc)

The address of the instruction that was interrupted. A handler that means to let the program continue returns to it, normally after adding 4 so that the instruction which trapped is not run a second time.