COMP1521 788 words·4 min read

04. MIPS Data and Memory

MIPS Data and Memory#

Leaving the Registers#

Registers are excellent for small, temporary values, but they cannot hold an array, a string, or an entire struct. Those objects live in memory. MIPS therefore separates two jobs:

  • arithmetic and comparisons operate primarily on registers;
  • load and store instructions move values between memory and registers.

Loading does not remove a value from memory; it copies its bits into a register. Storing copies the low part of a register into memory.

Memory Is Byte-Addressed#

MIPS memory can be imagined as a one-dimensional array of bytes. Every byte has a unique address. A multi-byte value occupies several consecutive addresses.

C-like value Typical size in the course MIPS environment
char / byte 1 byte
short / half-word 2 bytes
int / word 4 bytes
pointer 4 bytes

An address is itself a 32-bit value, so a pointer fits in a general-purpose register. The object pointed to normally remains in memory.

Note

- Mipsy's simplified memory layout
Mipsy places user code in a text region, global data in a data region, and function-call storage in a stack which grows towards lower addresses. Its model is intentionally friendlier than a full operating system. The official MIPS guide documents the current segment addresses and permissions.

Global and Local Values#

A global object is placed in the .data section:

MIPS
	.data
counter:
	.word	0			# int counter = 0;

Small local values are often kept in registers while possible. Values whose address is needed, large objects, and values which cannot remain in registers may instead live in memory. MIPS Functions explains stack storage and register preservation.

Defining Data#

Assembler directives reserve and optionally initialise bytes:

Directive Effect
.byte 7 emit one byte
.half 7 emit a two-byte half-word
.word 42 emit a four-byte word
.space 16 reserve 16 uninitialised bytes
.ascii "abc" emit three character bytes
.asciiz "abc" emit the characters and a terminating zero byte
.align 2 align the next object to a multiple of 4 bytes in mipsy

Directives do not execute at runtime. They tell the assembler what bytes should exist in a section.

MIPS
	.data
letter:
	.byte	'Q'
answer:
	.word	42
numbers:
	.word	1, 3, 5, 7, 9
message:
	.asciiz	"hello"
buffer:
	.space	64

Load and Store Instructions#

Size Load Store
1 byte lb sb
2 bytes lh sh
4 bytes lw sw

The width should match the object. Using lw for a character would read the character and three neighbouring bytes as one word, assuming the address is aligned at all.

Signed and Unsigned Loads#

lb and lh sign-extend their result to fill the 32-bit register. If the highest bit of the loaded byte or half-word is 1, the upper register bits become 1, preserving a negative two's-complement value. Their unsigned counterparts lbu and lhu instead fill the upper bits with zeroes.

MIPS
	lb	$t0, 0($t2)		# load a signed int8_t-like value
	lbu	$t1, 0($t2)		# load a uint8_t-like value

Stores do not need signed and unsigned forms: sb simply writes the low eight bits, while sh writes the low sixteen. Signedness matters when the bits are interpreted after loading.

Loading and Updating a Word#

C
int counter = 0;
counter++;
MIPS
	la	$t0, counter		# $t0 = &counter
	lw	$t1, 0($t0)		# $t1 = counter
	addi	$t1, $t1, 1		# $t1++
	sw	$t1, 0($t0)		# counter = $t1

	.data
counter:
	.word	0

The CPU cannot increment the memory object directly with addi. It loads, computes, then stores.

Mipsy also accepts convenient label forms:

MIPS
	lw	$t1, counter
	addi	$t1, $t1, 1
	sw	$t1, counter

The explicit address-in-a-register form is usually better preparation for arrays, pointers, and structs.

Effective Addresses#

The common memory operand has the form:

CODE
offset(base_register)

The effective address is:

CODE
value in base_register + constant byte offset
MIPS
	lw	$t0, 8($t1)		# load a word from address $t1 + 8
	sb	$t2, 3($t3)		# store one byte at address $t3 + 3

The offset is measured in bytes, even for lw. lw $t0, 4($t1) accesses the next word after the word at 0($t1).

Addresses, Pointers, and Dereferencing#

Consider:

C
int answer = 42;
int *p = &answer;
int i = *p;
*p = 27;

In MIPS:

MIPS
	la	$t0, answer		# int *p = &answer;
	lw	$t1, 0($t0)		# int i = *p;
	li	$t2, 27
	sw	$t2, 0($t0)		# *p = 27;

	.data
answer:
	.word	42

la performs address-taking; lw and sw perform dereferencing. Keep the distinction explicit:

C MIPS idea
&answer la the label's address
*p as a read load from 0(p_register)
*p = value store to 0(p_register)

Alignment#

An object's required alignment is determined by its type and the platform's ABI; it is not generally equal to the object's total size. In the course MIPS environment, a two-byte half-word is aligned to a multiple of two and a four-byte word used by lw or sw is aligned to a multiple of four. A struct normally inherits the strictest alignment required by any of its fields, which is why a 60-byte struct containing words needs four-byte alignment rather than 60-byte alignment.

This can break when a string or byte array appears before a word:

MIPS
	.data
name:
	.asciiz	"hello"			# 6 bytes including terminator
count:
	.space	4			# may begin unaligned

Safer options include defining the word with .word, reordering objects, or explicitly aligning it:

MIPS
	.data
name:
	.asciiz	"hello"
	.align	2			# next address is divisible by 2^2 = 4
count:
	.space	4

Hexadecimal addresses ending in 0, 4, 8, or C are divisible by four.

One-Dimensional Arrays#

An array is a sequence of equal-sized elements. If base is the address of its first element, then:

CODE
address of array[i] = base + i * sizeof(element)

Byte Array#

For char letters[], each element is one byte:

MIPS
	la	$t0, letters		# base
	li	$t1, 3			# i
	add	$t2, $t0, $t1		# &letters[i]
	lb	$t3, 0($t2)		# letters[i]

	.data
letters:
	.byte	'a', 'z', 'b', 'f', 'G'

Word Array#

For int numbers[], each element occupies four bytes:

MIPS
	la	$t0, numbers		# base
	li	$t1, 3			# i
	mul	$t2, $t1, 4		# byte_offset = i * sizeof(int)
	add	$t3, $t0, $t2		# &numbers[i]
	lw	$t4, 0($t3)		# numbers[i]

	.data
numbers:
	.word	16, 4, 1, 9, 2

A common mistake is using i directly as the offset for an int array. That produces an unaligned or incorrect address because MIPS offsets are bytes, not elements.

Looping Through an Array#

C
int sum = 0;
for (int i = 0; i < LENGTH; i++) {
	sum += numbers[i];
}
MIPS
LENGTH = 5

	li	$t0, 0			# int sum = 0;
	li	$t1, 0			# int i = 0;
loop_sum__cond:
	bge	$t1, LENGTH, loop_sum__end
	mul	$t2, $t1, 4		# byte_offset = i * 4;
	lw	$t3, numbers($t2)	# int value = numbers[i];
	add	$t0, $t0, $t3		# sum += value;
	addi	$t1, $t1, 1
	b	loop_sum__cond
loop_sum__end:

Pointer Arithmetic#

C automatically scales pointer arithmetic by the pointed-to type:

C
char *c = address;
c++;                 // address increases by 1

int *p = address;
p++;                 // address increases by 4

MIPS registers have no pointer type, so you perform that scaling yourself. Increment a character pointer by 1, and an integer pointer by 4.

Index-based loops are often easier to debug because the logical index and computed byte offset remain separate. Pointer loops can still be useful, particularly for strings:

MIPS
	la	$t0, string		# char *p = string;
loop_string__cond:
	lb	$t1, 0($t0)		# char c = *p;
	beq	$t1, 0, loop_string__end
	# use c
	addi	$t0, $t0, 1		# p++;
	b	loop_string__cond
loop_string__end:

Note

Checkpoint

  • Memory is byte-addressed, so every load or store begins with an address.
  • The instruction width (lb, lh, or lw) must match the object being accessed.
  • Array indexing becomes address arithmetic: base address plus index multiplied by element size.

Two-Dimensional Arrays#

C stores a normal two-dimensional array in row-major order: all of row 0, then all of row 1, and so on. Memory remains one-dimensional, so the two indices must be flattened.

CODE
element_index = row * N_COLS + col
byte_offset   = element_index * sizeof(element)
address       = base + byte_offset
CODE
Conceptual grid: int a[3][4]

row 0: [0][0] [0][1] [0][2] [0][3]
row 1: [1][0] [1][1] [1][2] [1][3]
row 2: [2][0] [2][1] [2][2] [2][3]
                  ^ a[2][1]

Flattened row-major indices:
row 0 -> 0, 1, 2, 3
row 1 -> 4, 5, 6, 7
row 2 -> 8, 9, 10, 11

a[2][1]:
element index = 2 * 4 + 1 = 9
byte offset   = 9 * 4 = 36

For int matrix[N_ROWS][N_COLS], with row in $t0 and column in $t1:

MIPS
	mul	$t2, $t0, N_COLS	# row * N_COLS
	add	$t2, $t2, $t1		# row * N_COLS + col
	mul	$t2, $t2, 4		# byte offset
	lw	$t3, matrix($t2)		# matrix[row][col]

For a byte matrix, omit the final multiplication by four and use lb or sb.

Tip

Separate the formula
When debugging, inspect row * N_COLS, then the flattened element index, then the byte offset, then the final address. A single giant expression is shorter to write but harder to verify.

Structs#

A struct is a sequence of fields at known offsets from one base address. Suppose:

C
struct student {
	int zid;
	char first[20];
	char last[20];
	int program;
	char alias[10];
};

The fields begin at:

Field Offset Size
zid 0 4
first 4 20
last 24 20
program 44 4
alias 48 10

The raw fields occupy 58 bytes. Because the struct contains four-byte integers, its total size is padded to a multiple of four: 60 bytes. Tail padding ensures that every element of an array of these structs also begins correctly aligned.

Define constants rather than scattering unexplained numbers:

MIPS
STUDENT_ZID_OFFSET = 0
STUDENT_FIRST_OFFSET = 4
STUDENT_LAST_OFFSET = 24
STUDENT_PROGRAM_OFFSET = 44
STUDENT_ALIAS_OFFSET = 48
SIZEOF_STUDENT = 60

If $t0 contains struct student *s:

MIPS
	lw	$t1, STUDENT_ZID_OFFSET($t0)	# s->zid
	lw	$t2, STUDENT_PROGRAM_OFFSET($t0)	# s->program
	addi	$t3, $t0, STUDENT_FIRST_OFFSET	# &s->first[0]

For an array students[i], first calculate i * SIZEOF_STUDENT, add the array base, then use the field offset.

CODE
address of students[i].program
= base + i * SIZEOF_STUDENT + STUDENT_PROGRAM_OFFSET

Common Mistakes#

  • Confusing an address with the value stored at that address.
  • Using la when lw is required, or loading a value when its address is required.
  • Forgetting that load/store offsets are measured in bytes.
  • Using lw/sw on an unaligned address.
  • Scaling an int array index by 1 instead of 4.
  • Using lw for a byte or lb for an entire word.
  • Modifying a loaded value but never storing it back.
  • Calculating row * N_ROWS + col instead of row * N_COLS + col.
  • Ignoring padding when computing struct size or field offsets.

A Reliable Address Checklist#

Whenever memory code looks wrong, state these five things explicitly:

  1. What object am I accessing?
  2. What is its base address?
  3. What is the logical index or field?
  4. What byte offset does that imply?
  5. Which width—byte, half-word, or word—must I load or store?

If those answers are correct, the instruction is usually straightforward.

Sources and Further Reading#