Table of Contents

MIPS Assembly: Data, Registers, and Mimicking Scope

Olivia Gallucci at Black Hat 2023. Used in a post about MIPS assembly data, registers, and mimicking scope

MIPS data and registers

Microprocessor without Interlocked Pipeline Stages (MIPS) is a popular reduced instruction set computer (RISC) architecture used in various processors, including those found in embedded systems, workstations, and supercomputers. It is also the architecture most computer organization courses use to teach assembly, so its registers are usually the first ones you memorize.

This post starts with a register cheat sheet you can keep open while you code. Then it explains what each register family is for, how to fake scope by saving registers to the stack, and how load and store instructions move data between memory and registers.

MIPS register cheat sheet
NameNumberUsePreserved across calls?
$zero$0Always 0n/a
$at$1Reserved for the assemblern/a
$v0-$v1$2-$3Return valuesNo
$a0-$a3$4-$7ArgumentsNo
$t0-$t7$8-$15TemporariesNo (caller saves)
$s0-$s7$16-$23Saved valuesYes (callee saves)
$t8-$t9$24-$25More temporariesNo (caller saves)
$k0-$k1$26-$27Reserved for the OS kerneln/a
$gp$28Global pointerYes
$sp$29Stack pointerYes
$fp$30Frame pointerYes
$ra$31Return address (written by jal)Yes
Based on Patterson and Hennessy’s MIPS register conventions.

MIPS registers 

MIPS is a load/store architecture: arithmetic and logic instructions only work on values in registers, so data has to be loaded into a register before you can operate on it. Registers are small amounts of memory inside the central processing unit (CPU). MIPS has 32 registers that are each 32 bits long, which means they can hold a 32-bit address. They are named from 0 to 31. 

When writing about registers, use a $ in front of it. Thus, “they are named from 0 to 31” should be written as “they are named from $0 to $31.” If $ is not in front of the register, the assembler will assume it is a constant.

🌸👋🏻 Enjoying this post? Get the next issue of [ret]2read. Once a month, I’ll email you whatever’s on my mind, usually Apple security research, offensive security, open source software freedom, and operating system internals.

Memory vs registers 

Although registers have memory and can hold 32-bit addresses, they do not have memory addresses. Only things stored in memory have memory addresses. In other words, you can’t have a memory address to a register because registers are not located in memory. 

CPU and memory interaction. Used in a post about MIPS assembly data, registers, and mimicking scope

Inside high-level languages like C, all variables have memory addresses and you can have pointers to memory addresses. In contrast, you cannot point to MIPS assembly registers, nor do MIPS variables (aka, operands) have memory addresses. Instead, the programmer uses the register name (e.g., $t0) to specify a register, and the CPU examines the instruction operands, which are represented with a 5-bit code.

Learn more: Computer Memory and Performance.

Why registers are faster 

The speed of memory is slow compared to registers. The only instructions that use memory operands are load and store (i.e., lw, lh, lb, lbu, lhu, sw, sh, and sb); these are slow. All other instructions use register operands, making their execution faster. 

Families  

Registers are grouped into families. Families imply how the register should be used in standard usage. It makes your code easier to read when you use the alias name rather than the register name because you will know what family it belongs to. I found a helpful video on MIPS families that explains this concept in more detail. 

Chart 

The color of the name column shows the family of the register.

MIPS assembly registers and names. MIPS procedure call with arguments screenshot. Used in a post about MIPS assembly data, registers, and mimicking scope

Source

Zero registers 

$zero always contains a value of 0. If you try to write to zero, the hardware throws out the new value, so that 0 remains at $zero. Thus, trying to write to $zero or using $zero as a destination address is useless.

#
# 0 = 0 + 5 
#

addi $zero, 5 

However, you can use it with other other registers. When you read $zero, a numerical value of 0 will always be returned. 

#
# $t0 = $zero + $t5 
#

li $t5, 6

#
# 6 = 0 + 6
#
add $t0, $zero, $t5 

$zero can be used anywhere.

Reserved

$at is reserved for the assembler as a temporary value. In general, programmers should not use $at because it is reserved. It is not impossible to use, it is just the assembler overwrites it if you use an instruction that requires the use of $at. 

Return values 

The V registers ($v0 and $v1) are for expression evaluation and function results. You can return up to two values because there are two V registers. Writing a new value overwrites whatever was there, so you don’t need to clear $v0 first. Just don’t assume it still holds your value after you call another function. 

Arguments 

The A registers ($a0 – $a3) are used to store arguments before jumping to a procedure or function. 

#
# inside the main function
#

li $a0, 1
li $a1, 2
li $a2, 3
li $a3, 4

#
# passes 1, 2, 3, and 4 into the function 
# jal saves the return address in $ra
#

jal function 

# ############
# ... code ...
# ############

function: 
move  $t0, $a0
move  $t1, $a1
move  $t2, $a2
move  $t3, $a3
jr    $ra

The call uses jal (not j) so the function can return with jr $ra.

Note that if you need to pass more than four arguments to a routine, you can pass them on the stack. 

MIPS procedure call with arguments screenshot. Used in a post about MIPS assembly data, registers, and mimicking scope

Source: MIPS Programming Tutorial: Passing more than 4 arguments to a procedure 

Temporary registers

T registers ($t0 – $t9) are for temporary storage. They are caller-saved registers and can be overwritten by called routines. In other words, the values in these registers are not preserved across function calls. 

T registers are meant to be used immediately, similar to a local variable in other programming languages. Thus, if you want to save them, you must push them onto the stack (see this video on saving MIPS registers to the stack) before calling a function because the called function is allowed to change temporary registers. 

The two families that programmers interact with most are T and S. 

Saved registers

S registers ($s0 – $s7) keep their values across function calls from the caller’s point of view. They are callee-saved registers: any function that wants to use one must save its old value to the stack first and restore it before returning. Although MIPS does not have scope, you can think of these similar to a global variable. 

🌸 Want an ad like this in [ret]2read? Book an issue →

Sponsor

Example Academy: Reverse engineer macOS on ARM64, live March 9–12

Four days of hands-on labs, from reading ARM64 assembly to patching Mach-O binaries and tracing XPC services, taught by working reverse engineers. 16 seats. [ret]2read readers get 15% off with code ASSEMBLY.

Sponsors write this section. They have no input on anything else in [ret]2read.

Operating system – reserved

The operating system owns its own memory and context. Sometimes interrupts can occur, where the OS automatically saves the current program counter and jumps to the address of the interrupt. Effectively, interrupts a type of exception that causes the program’s execution to be interrupted. 

The goal of the K registers ($k0 and $k1) is to ensure interrupts happen quickly. 

K registers are “reserved for the assembler and operating system and should not be used by user programs or compilers” (Patterson and Hennessy). 

Global area pointer 

The global pointer register ($gp) points to global data, specifically the middle of a 64K block of memory in the static data segment” (Patterson and Hennessy). The global pointer allows you to manipulate data that has been allocated. 

Stack pointer

The stack pointer register ($sp) points to the top of the stack, also known as the last location on the stack. In other words, the stack pointer allows you to keep track of what stack in the program you are on. Sometimes you will need to manipulate (increment or decrement) the stack pointer in a push/pop fashion, to store temporary variables. 

🌸👋🏻 Enjoying this post? Get the next issue of [ret]2read. Once a month, I’ll email you whatever’s on my mind, usually Apple security research, offensive security, open source software freedom, and operating system internals.

Frame pointer 

The frame pointer ($fp) points at location in the stack and it is used for building standard stack frames. It has the same value throughout the execution of a function, so all local data may be accessed through hard-coded offsets from the frame pointer. 

Return address 

The return address ($ra) is the return address from a procedure call. This register is written by the jal instruction. In short, when a function is called, the return address is saved in $ra. 

Scope

Scope does not exist in MIPS assembly, but you can simulate scope by saving an old variable value somewhere, calling a function and manipulating the variable’s value, and then restoring the original value after the manipulation is done. It will appear as if the register did not change. 

# 
# example code to save and restore registers
#

#
# assume we have a variable stored in $t0
#

li $t0, 10              # initialize $t0 with a value of 10

#
# call a function that modifies the value of $t0
#

jal ModifyValue

# ###########
# ... imaginary code body ...
# ###########

#
# function to modify the value of $t0
#

ModifyValue:

    #
    # save the original value of $t0 on the stack
    #
    
    addiu $sp, $sp, -4  # adjust the stack pointer
    sw $t0, 0($sp)      # store the value of $t0 on the stack

    addi $t0, $t0, 5    # add 5 to the value of $t0
    
    #
    # restore the original value of $t0 from the stack
    #
    
    lw $t0, 0($sp)      # load the value of $t0 from the stack
    addiu $sp, $sp, 4   # adjust the stack pointer back
    jr $ra              # return from the function

Fake scope in MIPS assembly by saving and restoring registers (Gist).

This save-and-restore pattern is exactly what the calling convention requires for $s registers: a function that uses one must save and restore it like this. With $t registers it is optional, because by convention the caller, not the called function, is responsible for saving them. (Note: Beginner MIPS programmers mostly interact with $S and $T registers.)

Data transferring and storage

Unfortunately, 32 registers are not enough storage space for most programs. Thus, programmers put data into memory using load and store (LS) instructions. Depending on the size of the data being loaded or stored, different instructions are used.

  • Byte (8 bits): A single byte of data.
  • Halfword (16 bits): Two bytes of data.
  • Word (32 bits): Four bytes of data, which is the size of a register in MIPS.
  • Doubleword (64 bits): Eight bytes of data.

Variables in C 

Now that we covered memory and registers, let’s examine data and operators. 

In high-level languages, you store data in variables. Variables have three traits: type, name, and location in the stack memory where a value lies. 

//
// include the standard input/output library for 
// functions like printf
//

#include <stdio.h>

//
// main function
// aka. the entry point of the program
//

int main() {

    //
    // declare a pointer variable named boop
    //

    int *boop;
    
    //
    // declare an integer variable named value 
    // and initialize it to 42
    //

    int value = 42;
    
    //
    // assign the memory address of value 
    // to the pointer boop
    //

    boop = &value;

    //
    // print the memory location boop points to
    //
    //   when you run the code, the output might look something 
    //   like this (though the exact memory address will vary):
    //   
    //       Memory location boop points to: 0x7ffee2a99a14
    //

    printf("Memory location boop points to: %p\n", (void *)boop);

    //
    // return from function 
    // 0 indicates successful execution to the OS
    //

    return 0;
}

How to find a where a variable points to in memory (Gist).

The code will print the memory location that the pointer boop is pointing to. This memory location is the address of the integer variable value that was assigned to boop.

When you run the code, the output might look something like this (though the exact memory address will vary):

Memory location boop points to: 0x7ffee2a99a14

In this example output, 0x7ffee2a99a14 is the hexadecimal representation of the memory address where the value variable is stored. Keep in mind that the specific memory address will vary each time you run the program due to differences in memory allocation. 

Variables in MIPS

In MIPS assembly, variable names are represented by registers, which do not have types. Thus, variables in MIPS assembly do not have types. However, the data stored in them do, like byte, address, and integer (unsigned and signed).

For example, load byte (lb) is used to read a byte from memory into a register, and store byte (sb) is used to save a byte from a register into memory.

#
# $s1 = Memory[$s2 + 20]
#

lb $s1, 20($s2)

#
# Memory[$s2 + 20] = $s1
#

sb $s1, 20($s2)

LS instructions  

There are many different load and store instructions. Combined, these instructions are known as “transfer” instructions, and they are often used when dealing with data structures like arrays or structures.

This information is loaded and stored in order to perform operations by the arithmetic logic unit, which may be covered in a future post. 

Instruction ExampleMeaningComments Format 
load wordlw $s1, 20($s2)$s1 = Memory[$s2 + 20]Word from memory registerI
store wordsw $s1, 20($s2)Memory[$s2 + 20] = $s1Word from register to memory I
load halflh $s1, 20($s2)$s1 = Memory[$s2 + 20]Halfword memory to registerI
load half unsignedlhu $s1, 20($s2)$s1 = Memory[$s2 + 20]Halfword memory to registerI 
store half sh $s1, 20($s2)Memory[$s2 + 20] = $s1Halfword register to memoryI
load bytelb $s1, 20($s2)$s1 = Memory[$s2 + 20] Byte from memory to registerI
load byte unsignedlbu $s1, 20($s2)$s1 = Memory[$s2 + 20]Byte from memory to register I
store bytesb $s1, 20($s2)Memory[$s2 + 20] = $s1Byte from register to memoryI
load linked wordll $s1, 20($s2)$s1 = Memory[$s2 + 20]Load word as 1st half of atomic swap I
store condition word sc $s1, 20($s2)Memory[$s2 + 20] = $s1; $s1 = 0 or 1 Store word as 2nd half of atomic swap I
load upper immediatelui $s1, 20 $s1 = 20 * 2^16 Loads constant in upper 16 bitsI 

Table 2.2.2 Hennessy

Conclusion

To recap: MIPS only computes on registers, so every program is a loop of loading values from memory, working on them in registers, and storing results back. Use $t registers for short-lived values, $s registers for anything that must survive a function call (and save them if your function uses them), $a and $v registers to pass arguments and results, and the stack to fake scope when you run out of registers.

If you enjoyed this post on MIPS Data and Registers, consider reading Computer Memory and Performance. 

Citations 

Patterson, David A., and John L. Hennessy. Computer Organization and Design MIPS Edition: The Hardware/Software Interface. Morgan Kaufmann, 2020.

Portrait of Olivia Gallucci in garden, used in LNP article.

Written by Olivia Gallucci

Olivia is senior security engineer, certified personal trainer, and freedom software advocate. She writes about offensive security, open source software, and professional development.

Discover more from [ret]2read

A Monthly OS Internals Publication by Olivia Gallucci. Subscribe now to keep reading and get access to the full archive.

Continue reading