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. Understanding MIPS data and registers is essential for programming and optimizing MIPS-based systems. The data and registers play a critical role in the MIPS architecture and determine how the processor operates.

In this blog post, we will explore the fundamentals of MIPS data and registers, including their types, uses, and functionalities. 

MIPS registers 

All data in MIPS programs must be in registers. 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.

🌸👋🏻 Let’s take this to your inbox. You’ll receive occasional emails about whatever’s on my mind—offensive security, open source, academics, boats, software freedom, you get the idea.

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: Post coming soon.

MIPs registers 

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. 


Registers 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. 


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


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.


$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 validation and function results. You can return up to two values because there are two V registers. However, you should remember to zero out (li $v0, 0) values before storing something in it in case it held a previous value. 


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 

j function 

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

move  $t0, $a0
move  $t1, $a1
move  $t2, $a2
move  $t3, $a3

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 (global variables). 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 calling 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) do not change. They are callee-saved registers and are preserved across function calls. Although MIPS does not have scope, you can think of these similar to a global variable. 

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. 

Let’s take this to your inbox.

You’ll receive occasional emails about whatever’s on my mind—offensive security, open source, academics, boats, software freedom, you get the idea.

Mast. Used on CTA 1 on Olivia Gallucci . com

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 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


    # 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).

One thing to note is that you cannot perform this with $s registers, but you can use the $T registers in this fashion. (Note: Beginner MIPS programmers only 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 register
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 bits

Table 2.2.2 Hennessy


In conclusion, understanding MIPS data and registers is essential for efficient programming and optimization of MIPS-based systems. The data and registers play a critical role in the MIPS architecture, determining how the processor operates and the type of data that can be processed. By knowing the different types of data and registers available in MIPS, programmers can select the appropriate data type for the task at hand, optimize their programs, and ensure that their MIPS-based systems run smoothly without errors or performance issues.

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


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 an honors student at the Rochester Institute of Technology. She writes about security, open source software, and professional development.